feat(ml): initialise le pipeline d'entrainement LightGBM (ADR 0005)
ML / Lint, typage et tests (push) Successful in 2m2s
ML / Lint, typage et tests (push) Successful in 2m2s
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""Construction des features pour le modele de consommation.
|
||||
|
||||
Module partage entre l'entrainement et le futur scoring (cf. `docs/ML-START.md`) : la fonction
|
||||
qui construit les features doit rester strictement identique des deux cotes, sous peine de
|
||||
"train/serve skew" silencieux (le modele recoit en production des features qui ne ressemblent
|
||||
plus a ce qu'il a appris).
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import pandas as pd
|
||||
|
||||
# Cible de l'entrainement : consommation en kWh, jamais consumption_kw (absent des lectures
|
||||
# historiques CSV, cf. `apps/backend/app/etl/historical_import.py`).
|
||||
TARGET_COLUMN = "consumption_kwh"
|
||||
|
||||
# Decalages horaires utilises pour les lags et moyennes glissantes : une heure avant, un jour
|
||||
# avant (meme heure), une semaine avant (meme heure, meme jour) - saisonnalites usuelles d'une
|
||||
# consommation energetique horaire.
|
||||
LAG_HOURS: Sequence[int] = (1, 24, 168)
|
||||
ROLLING_WINDOWS_HOURS: Sequence[int] = (24, 168)
|
||||
|
||||
STATIC_FEATURE_COLUMNS: Sequence[str] = ("site_type", "capacity_kw")
|
||||
|
||||
CALENDAR_FEATURE_COLUMNS: Sequence[str] = (
|
||||
"hour",
|
||||
"day_of_week",
|
||||
"month",
|
||||
"is_weekend",
|
||||
"is_working_hours",
|
||||
)
|
||||
|
||||
WEATHER_COLUMNS: Sequence[str] = (
|
||||
"temperature_celsius",
|
||||
"humidity_percent",
|
||||
"solar_irradiance_wm2",
|
||||
)
|
||||
|
||||
|
||||
def build_features(frame: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Construit la matrice de features a partir de lectures brutes triees par site.
|
||||
|
||||
`frame` doit porter au minimum : `site_id`, `timestamp`, `consumption_kwh`,
|
||||
`is_working_hours`, les trois colonnes meteo, et les colonnes statiques de site
|
||||
(`site_type`, `capacity_kw`). Une ligne par `(site_id, timestamp)`, sans doublon.
|
||||
|
||||
Piege : la meteo n'entre dans les features que decalee (lag/moyenne glissante), jamais a
|
||||
l'instant cible. A l'entrainement comme au scoring, la meteo au moment predit n'est pas une
|
||||
mesure mais une prevision que le projet n'a pas — l'utiliser telle quelle romprait le
|
||||
contrat entre entrainement et usage reel (la feature ne serait tout simplement plus
|
||||
disponible en production). Cf. debat d'architecture dans l'issue #89.
|
||||
"""
|
||||
travail = frame.sort_values(["site_id", "timestamp"]).reset_index(drop=True)
|
||||
|
||||
calendrier = _calendar_features(travail["timestamp"])
|
||||
decalees = _lagged_features(travail)
|
||||
|
||||
features = pd.concat(
|
||||
[
|
||||
travail[["site_id", "timestamp"]],
|
||||
travail[list(STATIC_FEATURE_COLUMNS)],
|
||||
calendrier,
|
||||
travail[["is_working_hours"]],
|
||||
decalees,
|
||||
travail[[TARGET_COLUMN]],
|
||||
],
|
||||
axis=1,
|
||||
)
|
||||
|
||||
# `period_minutes` : resolution temporelle de la cible. Les lectures historiques sont toutes
|
||||
# au pas horaire (cf. `dataset_metadata.json`, `frequency: "1h""), donc une constante pour
|
||||
# l'instant. Exposee comme feature plutot que supposee implicitement, pour que le modele
|
||||
# puisse un jour apprendre sur d'autres resolutions sans reentrainement de zero.
|
||||
features["period_minutes"] = 60
|
||||
|
||||
return features
|
||||
|
||||
|
||||
def feature_columns() -> list[str]:
|
||||
"""Liste ordonnee des colonnes d'entree du modele (hors identifiants et cible)."""
|
||||
lag_columns = [f"consumption_kwh_lag_{h}h" for h in LAG_HOURS]
|
||||
rolling_columns = [
|
||||
f"{colonne}_rolling_mean_{fenetre}h"
|
||||
for colonne in (TARGET_COLUMN, *WEATHER_COLUMNS)
|
||||
for fenetre in ROLLING_WINDOWS_HOURS
|
||||
]
|
||||
weather_lag_columns = [f"{colonne}_lag_1h" for colonne in WEATHER_COLUMNS]
|
||||
|
||||
return [
|
||||
*STATIC_FEATURE_COLUMNS,
|
||||
*CALENDAR_FEATURE_COLUMNS,
|
||||
"period_minutes",
|
||||
*lag_columns,
|
||||
*rolling_columns,
|
||||
*weather_lag_columns,
|
||||
]
|
||||
|
||||
|
||||
def _calendar_features(timestamps: pd.Series) -> pd.DataFrame:
|
||||
instants = pd.to_datetime(timestamps)
|
||||
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"hour": instants.dt.hour,
|
||||
"day_of_week": instants.dt.dayofweek,
|
||||
"month": instants.dt.month,
|
||||
"is_weekend": instants.dt.dayofweek.isin([5, 6]).astype(int),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _lagged_features(travail: pd.DataFrame) -> pd.DataFrame:
|
||||
par_site = travail.groupby("site_id", sort=False)
|
||||
colonnes: dict[str, pd.Series] = {}
|
||||
|
||||
for decalage in LAG_HOURS:
|
||||
colonnes[f"{TARGET_COLUMN}_lag_{decalage}h"] = par_site[TARGET_COLUMN].shift(decalage)
|
||||
|
||||
for colonne in (TARGET_COLUMN, *WEATHER_COLUMNS):
|
||||
decale = par_site[colonne].shift(1)
|
||||
for fenetre in ROLLING_WINDOWS_HOURS:
|
||||
colonnes[f"{colonne}_rolling_mean_{fenetre}h"] = decale.groupby(
|
||||
travail["site_id"]
|
||||
).transform(lambda serie, fenetre=fenetre: serie.rolling(fenetre, min_periods=1).mean())
|
||||
|
||||
for colonne in WEATHER_COLUMNS:
|
||||
colonnes[f"{colonne}_lag_1h"] = par_site[colonne].shift(1)
|
||||
|
||||
return pd.DataFrame(colonnes, index=travail.index)
|
||||
Reference in New Issue
Block a user