fix(backend,airflow,ml): applique les corrections de revue sur la PR #162
This commit is contained in:
@@ -80,7 +80,7 @@ API Mock est accepté comme définitivement perdu, aucune mesure réelle n'exist
|
||||
période. `mock_api_import` refuse toute fenêtre qui recouvrirait des lectures déjà importées du
|
||||
CSV plutôt que de laisser les deux sources dupliquer silencieusement un même instant, et le
|
||||
pipeline ML déduplique par construction (`DISTINCT ON`, source `csv` préférée) au cas où un
|
||||
recouvrement se produirait malgré tout — voir [40-data.md](40-data.md).
|
||||
recouvrement se produirait malgré tout, voir [40-data.md](40-data.md).
|
||||
|
||||
Les liens de la supervision sont en trait plein depuis le 23/09 (issue #26) : Prometheus scrute
|
||||
`/metrics` avec un jeton, Grafana lit Prometheus et, par un rôle en lecture seule, les tables
|
||||
|
||||
@@ -437,19 +437,23 @@ Les paramètres de ligne de commande disponibles pour l'import sont :
|
||||
```text
|
||||
--start-time
|
||||
--end-time
|
||||
--limit
|
||||
--dry-run
|
||||
```
|
||||
|
||||
**Piège sur `--limit`** : l'API ne renvoie pas un flux à un rythme naturel, elle répartit
|
||||
exactement `limit` lectures, espacées uniformément, sur toute la fenêtre `[start_time, end_time)`
|
||||
demandée. Une fenêtre d'une heure avec `limit=1000` renvoie donc 1000 lectures espacées de 3,6
|
||||
secondes à l'intérieur de cette heure, pas une lecture horaire — vérifié empiriquement en
|
||||
interrogeant directement l'API. Le seul réglage qui produise une lecture par heure, alignée sur
|
||||
l'heure et cohérente avec le grain horaire du reste du schéma (`period_minutes=60`, historique
|
||||
CSV à une ligne par heure), est `limit` = nombre d'heures de la fenêtre. Le DAG `mock_api_import`
|
||||
interroge toujours une fenêtre d'1h (`interval=timedelta(hours=1)`, voir
|
||||
[10-infra.md](10-infra.md)), donc `limit=1`.
|
||||
**Piège sur `limit`, corrigé dans le code plutôt que documenté** : l'API ne renvoie pas un flux à
|
||||
un rythme naturel, elle répartit exactement `limit` lectures, espacées uniformément, sur toute la
|
||||
fenêtre `[start_time, end_time)` demandée (vérifié empiriquement en interrogeant directement
|
||||
l'API). Une fenêtre d'une heure avec `limit=1000`, le réglage d'origine, renvoyait donc 1000
|
||||
lectures espacées de 3,6 secondes à l'intérieur de cette heure, pas une lecture horaire,
|
||||
incompatible avec les lags positionnels de `build_features`. Plutôt que documenter la règle
|
||||
« `limit` = nombre d'heures de la fenêtre » et compter sur chaque appelant pour la respecter,
|
||||
`limit_for_window()` la porte : `import_mock_api_history()` calcule `limit` depuis la fenêtre
|
||||
reçue, refuse une fenêtre qui ne couvre pas un nombre entier d'heures, et refuse un intervalle de
|
||||
plus de 1000 heures (le plafond `limit` de l'API). `--limit` n'existe donc plus côté CLI. Le DAG
|
||||
`mock_api_import` interroge toujours une fenêtre d'1h (`interval=timedelta(hours=1)`, voir
|
||||
[10-infra.md](10-infra.md)) : la fenêtre `[:45, :45)` place chaque lecture à :45, pas à :00 (la
|
||||
première lecture atterrit au début de la fenêtre demandée), un décalage constant sans effet sur
|
||||
les lags positionnels ni sur les jointures en aval.
|
||||
|
||||
### Flux d'ingestion API Mock
|
||||
|
||||
@@ -528,22 +532,43 @@ deux dans `reading`. Trois décisions ferment cette réconciliation :
|
||||
|
||||
- **Le trou temporel est accepté.** Le dataset historique s'arrête au 31/12/2024, et
|
||||
`mock_api_import` n'importe que l'heure précédant chaque déclenchement : rien ne comble
|
||||
automatiquement la période intermédiaire, et rien ne le pourra jamais — aucune mesure réelle
|
||||
n'existe pour ces instants.
|
||||
automatiquement la période intermédiaire, et rien ne le pourra jamais, aucune mesure réelle
|
||||
n'existe pour ces instants. Conséquence pour le ML, pas nouvelle mais que ce trou rend
|
||||
définitive : `build_features()` calcule ses lags par `shift(n)` positionnel, et `train.py`
|
||||
n'écarte que les lignes où `lag_168h` est `NaN`. Pour un site présent dans les deux sources, les
|
||||
168 premières lectures `api_history` qui suivent le trou héritent donc de lags et de moyennes
|
||||
glissantes calculés sur décembre 2024 (et tant que l'ingestion a moins de 7 jours, c'est le cas
|
||||
de toutes les lectures). Même effet, plus ponctuel, pour chaque heure que le DAG manque
|
||||
(`mock_api_import` en échec, Airflow arrêté). Aucun garde-fou ne détecte aujourd'hui un lag
|
||||
calculé sur un écart réel différent de celui attendu ; issue de suivi à ouvrir.
|
||||
- **Le recouvrement est refusé à l'ingestion.** `uq_reading_source` autorise deux lignes au même
|
||||
`(site_id, timestamp)` dès que `source` diffère : rien dans le schéma n'empêche donc un import
|
||||
Mock API manuel avec une fenêtre passée (le script accepte `--start-time`/`--end-time`
|
||||
arbitraires) de dupliquer un point déjà couvert par le CSV. `import_mock_api_history()` appelle
|
||||
`refuse_if_overlaps_historical_dataset()` avant toute écriture : si la fenêtre demandée recouvre
|
||||
au moins une lecture `source='csv'`, l'import est refusé (`ValueError`) plutôt que d'écrire un
|
||||
doublon inter-source silencieux.
|
||||
`refuse_if_overlaps_historical_dataset()` avant toute écriture, y compris en `--dry-run` (le
|
||||
contrôle est en lecture seule) et avant le moindre appel à l'API Mock : si la fenêtre demandée
|
||||
recouvre au moins une lecture `source='csv'`, l'import est refusé (`ValueError`) plutôt que
|
||||
d'écrire un doublon inter-source silencieux. Le contrôle ne porte que sur la fenêtre demandée,
|
||||
pas sur les lectures reçues : `fetch_readings()` écarte donc toute lecture dont le `timestamp`
|
||||
déborde de `[start_time, end_time)`, pour qu'une réponse hors fenêtre (bug du mock, ou hostile)
|
||||
ne puisse pas le contourner. Ce contrôle compare des instants, pas des chaînes : `parse_datetime()`
|
||||
pose `tzinfo=UTC` sur une entrée sans fuseau (même pattern que `_vers_utc()` dans
|
||||
`app/services/reading.py`), sans quoi l'encodeur `timestamptz` d'asyncpg lirait un datetime naïf
|
||||
dans le fuseau local du **processus**, correct dans le conteneur Airflow (UTC) mais décalé pour
|
||||
un import manuel lancé depuis un poste en Europe/Paris.
|
||||
- **Le pipeline ML déduplique en défense.** Le garde-fou ci-dessus protège l'ingestion, pas
|
||||
la lecture : si un recouvrement se produisait malgré tout (import direct en base, contournement
|
||||
du script), `ml/enervision_ml/data.py` ne doit pas casser silencieusement l'hypothèse de
|
||||
`build_features` (« une ligne par `(site_id, timestamp)` »). `load_from_database()` et
|
||||
`load_recent_from_database()` utilisent donc `SELECT DISTINCT ON (site_id, timestamp)`, `source
|
||||
= 'csv'` gagnant sur `'api_history'` en cas d'égalité — l'historique étant une source vérifiée,
|
||||
l'API Mock une entrée hostile (cf. ci-dessus).
|
||||
= 'csv'` gagnant sur `'api_history'` en cas d'égalité, l'historique étant une source vérifiée,
|
||||
l'API Mock une entrée hostile (cf. ci-dessus). **Cette préférence est spécifique au chargeur
|
||||
ML.** `GET /readings` renvoie les deux lignes sans les fusionner, et `DriftRepository` /
|
||||
`ReadingRepository.latest_by_site()` / `.latest_for_site()` départagent par `reading_id` le plus
|
||||
grand (en pratique la ligne insérée en dernier, pas forcément `csv`) : en cas de recouvrement, la
|
||||
dérive comparerait alors une prévision à une valeur différente de celle sur laquelle le modèle a
|
||||
appris. Pas d'incohérence aujourd'hui tant que le recouvrement reste refusé à l'ingestion ; à
|
||||
aligner si ce garde-fou devait un jour être contourné.
|
||||
|
||||
### Qualité des données de l'API Mock
|
||||
|
||||
|
||||
Reference in New Issue
Block a user