# Comment le moteur fonctionne · How the engine works

*Une page, pour un lecteur qui n'ouvrira pas le code. Les numéros de section renvoient au notebook principal.*
*One page, for a reader who will not open the code. Section numbers refer to the main notebook.*

---

## FR — Six étapes, dans l'ordre où elles s'exécutent

**1. Lire les colonnes telles qu'elles sont, pas telles qu'elles sont décrites** (sections 1b, 1c).
Chaque colonne Yardi commence par `h` (identifiant, pour joindre) ou `s` (valeur lisible, pour afficher). Nous vérifions chaque champ contre les données :
par exemple, `PromoPay` dans les concessions est un crédit unique par bail, pas un montant mensuel. Le notebook documente chaque interprétation.

**2. Apparier chaque logement avec son propre bail précédent** (section 4).
La clé est `sPropCode` + `sUnitCode`, jamais `sSite` + `sUnitCode` (248 logements entreraient en collision). Deux baux consécutifs du même logement
forment une paire si l'écart entre leurs débuts est entre 0,5 et 2,5 ans. La variation est annualisée : (nouveau ÷ précédent)^(1/années) − 1.
Résultat : 3 179 paires, chacune avec une croissance contractuelle (`sRent`) et une croissance effective (`sRentEffective`).
Chaque exclusion (bail trop court, trop ancien, loyer nul, trou dans la séquence) est comptée et visible.

**3. Séparer ce qui ne bouge pas pareil** (sections 6, 7.3).
Chaque paire est classée renouvellement (`sRenewal = 1`) ou relocation (`sRenewal = 0`), et par immeuble, donc par province : cinq immeubles au Québec
(TAL), un en Ontario (The Met, ligne directrice ontarienne). Les cellules immeuble × type d'événement sont prévues séparément, puis recombinées
selon la part attendue de chacune en 2026.

**4. Prévoir 2026 avec trois lectures fixes, pas un modèle entraîné** (sections 4b, 7.3, 7.4).
Pour chaque cellule, trois estimations sont calculées à partir des baux connus au 31 décembre 2025, puis moyennées à parts égales :
*élan* (la croissance effective de l'an dernier), *passerelle* (la croissance contractuelle convertie en effective, logement par logement, selon l'écart de
concession attendu) et *ancre* (la tendance de long terme). Les cellules peu peuplées sont rapprochées de leur immeuble (rétrécissement `k = 10`).
Les paramètres sont figés dans `ForecastSpec` et dans `05_Modele_parametres_Model_parameters.json`. Le résultat : 4,80 % effectif, 6,38 % contractuel,
sur un rôle de 928 logements dont le bail arrive à échéance en 2026.

**5. Se tester sur le passé, contre des méthodes simples** (section 7.5).
`backtest(leases, année)` rejoue toute la méthode comme si l'on était au 31 décembre de l'année précédente, pour 2020 à 2025, et compare avec ce qui
s'est réellement produit. Erreur moyenne : 1,14 point sur 2020–2025; 1,29 point sur la fenêtre exigée 2023–2025, où une simple moyenne historique
fait 1,14. XGBoost et Ridge (section 9) ont été testés avec une règle de promotion fixée d'avance et ne la remplissent pas.

**6. Expliquer, puis confronter au marché** (sections 7.6, 8b).
Chaque immeuble se réconcilie en renouvellements + relocations + ajustement de concession = hausse effective (une réconciliation entre les deux
prévisions du modèle, pas un effet causal isolé des mois gratuits). Les sources publiques (SCHL, IPC loyers, TAL, Ontario) sont datées à leur publication
et posées à côté de la tendance interne; elles n'entrent pas dans le calcul, parce qu'aucune n'améliore les backtests avec six années d'historique.

**Ce que les 84 tests protègent.** Que la clé d'appariement est la bonne et que les séquences à trou ne sont pas pontées; que l'annualisation et
l'écrêtage font ce qu'ils disent; que rien de postérieur à l'origine ne fuit dans une prévision; que `estimate_2026()` et `backtest()` gardent la
signature du starter; que le notebook est autonome (aucun import de module de l'équipe) et n'affiche aucune ligne du CRM; que la simulation, le tableau
de bord et les challengers reproduisent les mêmes chiffres que le notebook.

---

## EN — Six steps, in the order they run

**1. Read the columns as they are, not as they are described** (sections 1b, 1c).
Every Yardi column starts with `h` (a handle, for joins) or `s` (a readable value, for display). Each field is checked against the data: for example,
`PromoPay` in the concessions table is a one-time credit per lease, not a monthly amount. The notebook documents each interpretation.

**2. Match each apartment with its own previous lease** (section 4).
The key is `sPropCode` + `sUnitCode`, never `sSite` + `sUnitCode` (248 units would collide). Two consecutive leases of the same unit form a pair when
their start dates are 0.5 to 2.5 years apart. The change is annualised: (new ÷ previous)^(1/years) − 1.
Result: 3,179 pairs, each with a contractual growth (`sRent`) and an effective growth (`sRentEffective`). Every exclusion (too short, too old, zero rent,
hole in the sequence) is counted and visible.

**3. Separate what does not move together** (sections 6, 7.3).
Each pair is a renewal (`sRenewal = 1`) or a turnover (`sRenewal = 0`), and belongs to a building, hence a province: five in Quebec (TAL), one in
Ontario (The Met, Ontario guideline). Building × event-type cells are forecast separately, then recombined by their expected 2026 share.

**4. Forecast 2026 with three fixed readings, not a trained model** (sections 4b, 7.3, 7.4).
For each cell, three estimates are computed from the leases known at 31 December 2025, then averaged with equal weights: *momentum* (last year's
effective growth), *bridge* (contractual growth converted to effective, unit by unit, through the expected concession gap) and *anchor* (the long-run
trend). Thin cells are pulled toward their building (shrinkage `k = 10`). The parameters are fixed in `ForecastSpec` and in
`05_Modele_parametres_Model_parameters.json`. Result: 4.80% effective, 6.38% contractual, over a roster of 928 units whose lease ends in 2026.

**5. Test against the past, against simple methods** (section 7.5).
`backtest(leases, year)` replays the whole method as if standing at 31 December of the previous year, for 2020 to 2025, and compares with what
actually happened. Mean error: 1.14 points over 2020–2025; 1.29 points on the required 2023–2025 window, where a plain historical average scores 1.14.
XGBoost and Ridge (section 9) were tested against a promotion rule fixed in advance and do not meet it.

**6. Explain, then confront with the market** (sections 7.6, 8b).
Each building reconciles as renewals + turnovers + concession adjustment = effective increase (a reconciliation between the model's two forecasts,
not an isolated causal effect of free months). Public sources (CMHC, rent CPI, TAL, Ontario) are dated by release and set beside the internal trend;
they do not enter the computation, because none of them improves the backtests with six years of history.

**What the 84 tests protect.** That the matching key is the right one and that holes in a sequence are not bridged; that annualisation and clipping do what
they say; that nothing after the origin leaks into a forecast; that `estimate_2026()` and `backtest()` keep the starter's signatures; that the notebook is
self-contained (no import from a team module) and displays no CRM row; that the simulation, the dashboard and the challengers reproduce the notebook's
figures.

---

## Fichiers · Files

`Code_complet_modules_et_tests_Full_code_and_tests/Competition Submission/` contient les modules Python (`forecast.py` : appariement, cellules, trois
lectures, backtest; `analysis.py` : diagnostics; `extensions.py` : scénarios; `challenger.py` : XGBoost et Ridge; `simulation.py` : tirages par logement;
`app.py` : tableau de bord Streamlit facultatif) et `tests/`. Les deux notebooks qui s'y trouvent sont des copies exécutées des notebooks officiels, sous
le nom qu'attendent les tests. · contains the Python modules (`forecast.py`: matching, cells, three readings, backtest; `analysis.py`: diagnostics;
`extensions.py`: scenarios; `challenger.py`: XGBoost and Ridge; `simulation.py`: unit-level draws; `app.py`: optional Streamlit dashboard) and `tests/`.
The two notebooks inside are executed copies of the official notebooks, under the names the tests expect.

Lancer les tests · Run the tests: `EQUINOXE_DATA_DIR=<dossier des quatre CSV> python -m unittest discover -s tests`
