`load_from_csv` gardait un `astype(bool)` sur `is_working_hours`, joué avant `_typer` :
une case vide du CSV arrivait en `NaN` et en ressortait `True`, soit une heure ouvrée
inventée. Le chemin base était corrigé, pas celui-ci, et rien ne le couvrait. La ligne
disparaît, et `_typer` ramène désormais les colonnes de `FLAG_COLUMNS` à `float64` quel
que soit le contenu lu : sans cela le dtype dépendait de l'écriture du fichier (`0`/`1`
contre `True`/`False`) et de la présence d'un trou, et l'égalité de schéma entre les deux
chargeurs que promet ML-START n'était vraie que par accident du jeu de test.
`Seuils.seuil_biais` valait `0` et `_verdict` exigeait `> 0` : la règle était inerte
partout, CLI et DAG compris, et aucun test ne l'exerçait. Elle reste désactivée par
défaut, parce qu'un seuil en kWh ne se transpose pas d'un bureau de 10 kWh à une usine
de 1 000 kWh et qu'aucune valeur n'a été calibrée sur la vraie série, mais `--bias-threshold`
la rend atteignable et l'ADR 0011 porte l'arbitrage. Trois tests couvrent le chemin :
inerte par défaut, dérive au-delà du seuil réglé, et priorité de la MAE sur le biais.
Deux lignes de doc devenues fausses au passage : la signature de `load_recent_from_database`
dans ML-START, qui omettait `until` devenu obligatoire, et la ligne `bias` de 20-backend,
qui laissait croire que la métrique décide du verdict.
La PR #123 (MLflow) est arrivée sur dev entre-temps. Un seul conflit, la liste
.PHONY du Makefile : elle garde `migrate-test` d'ici et `mlflow-up` de dev, les
deux cibles existant chacune de leur côté.
Rien d'autre ne se recoupe : le test de chaîne passait déjà son propre
`--mlflow-tracking-uri` sur un SQLite jetable, et `modele_jetable` entraîne son
Booster sans passer par `train()`, qui journalise dans MLflow sans garde.
`load_recent_from_database` n'avait qu'une borne basse. `build_scoring_frame` repartait donc de
la derniere lecture de toute la table quel que soit `--now` : `target_at` valait toujours
"fin du jeu + 1h", et `_age = instant - derniere_lecture` devenait negatif, ce qui passait le
seuil de peremption sans rien signaler.
Consequence concrete : sur le jeu historique, arrete au 31/12/2024, aucune boucle de rattrapage
ne pouvait produire une prevision dont le realise existe deja. La surveillance de derive livree
par la migration precedente n'aurait donc rien eu a comparer en demonstration.
`until` est desormais obligatoire sur ce chargeur, ce qui interdit de l'oublier, et le mode CSV
filtre symetriquement. En exploitation rien ne change, aucune lecture n'etant posterieure a
l'heure courante.
Trois phrases du depot annonçaient une surveillance de derive inexistante, et une quatrieme
disait qu'aucune base PostgreSQL n'etait joignable pour tester le chargement ML. Les quatre
sont maintenant fausses, donc reecrites plutot que laissees en dette.
- ADR 0011 : ou vit le calcul et pourquoi pas dans `ml/`, les deux dedoublonnages qu'impose la
jointure, et quatre alternatives ecartees avec la contrainte qui les interdit (la metrique
MLflow n'est pas la meme grandeur, `alert` borne ses valeurs et refuse un site nul, Prometheus
n'a pas de collecteur, ne rien persister ne repond pas a la question du jury).
- DAG `derive` quotidien, hors du DAG `alertes` : un echec de derive y ferait croire que la
detection a echoue, et la fenetre de 168 h ne se recalcule pas toutes les heures.
- ML-START : la limite de `--now` est dite au lieu d'etre decouverte en demonstration. Elle ne
decale que l'instant de reference, pas la fenetre de lecture, donc aucun rattrapage ne peut
fabriquer de paires prevu/realise sur un jeu fige.
- 50-cicd : pourquoi le job ML installe aussi le backend (le schema n'a qu'une source), et ce
que coute le filtre de chemins qui l'accompagne.
Le pipeline ML n'avait aucun test touchant PostgreSQL : `ml/README.md` le disait, faute de
base joignable en CI. Le marqueur `integration` de `ml/pyproject.toml` etait declare et porte
par zero test.
- `ml/tests/conftest.py` : deux fixtures d'acces a la base, jamais interchangeables.
`connexion_ml` annule sa transaction, `parc` valide ses ecritures parce que `run_scoring`
ouvre sa propre connexion et ne verrait rien d'autre. Garde sur le nom de base, marque uuid
sur chaque site, nettoyage dans l'ordre des cles etrangeres.
- `test_data_integration.py` : les neuf colonnes du contrat confrontees au schema Alembic
reel, la borne `since`, l'ordre de tri dont dependent des lags positionnels, et le typage
des colonnes entierement nulles.
- `test_score_integration.py` : les contraintes de `prediction` vues depuis le code qui
ecrit, l'empilement volontaire de deux runs, et `run_scoring` de bout en bout sur un
booster reel.
- `apps/backend/tests/test_chaine_ml_api.py` : lance les vrais binaires `enervision_ml.train`
et `.score` en sous-processus, comme les DAGs, puis relit par `GET /api/v1/predictions`.
Marqueur `chaine` distinct : le job `integration` du backend n'a pas l'environnement de ml/.
- `ml.yml` : job `integration`, seul du depot a reunir les deux environnements uv et une base.
Ses `paths` incluent les migrations du backend, sans quoi le schema deriverait du SQL du
pipeline sans que rien ne casse.
- Makefile : `migrate-test`, qui manquait (`enervision_test` n'a jamais recu de table),
`ml-test-integration` et `test-chaine`.
`reading.is_working_hours` est nullable et fait partie des features. Une seule lecture a
NULL dans la fenetre suffisait a rendre la colonne `object` au retour de `pd.read_sql`, et
LightGBM refuse alors de construire son Dataset : "pandas dtypes must be int, float or
bool". La panne n'arrivait qu'au scoring, sur la vraie base, jamais en test.
`_typer` coerce donc aussi cette colonne, comme les colonnes mesurees : NaN vaut valeur
manquante, que LightGBM gere nativement. Effet de bord voulu, les deux chargeurs rendent
enfin le meme dtype : `load_from_csv` faisait un `astype(bool)` qui ecrasait silencieusement
une valeur absente en `False`.
Le document était référencé 11 fois, dont 4 depuis le code (config.py, data.py,
train.py, score.py, features.py), et n'avait jamais été écrit. Deux chemins
contradictoires coexistaient : `../ML-START.md` depuis ml/README.md et
docs/architecture/, `docs/ML-START.md` depuis le code. Le chemin retenu est celui
du code, majoritaire et le seul qu'un lecteur du module rencontre.
Il couvre les trois sections que les renvois annoncent : mécanisme d'accès aux
données et pourquoi ce n'est pas l'API, étapes d'un run de scoring, frontière
entre FastAPI et LightGBM.
Ferme C34 de la grille d'auto-évaluation.