You've already forked donnees_meteo
Premiers modèles prédictifs
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
32
docs/10 - Modèles non linéaires/index.md
Normal file
32
docs/10 - Modèles non linéaires/index.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Modèles non linéaires (arbres, forêts, gradient boosting)
|
||||
|
||||
Objectif : tester des modèles plus flexibles que les régressions linéaires/logistiques, en restant raisonnables côté ressources. On utilise des forêts aléatoires (_random forest_) et du _gradient boosting_ sur les mêmes horizons (T+10, T+60, T+360, T+1440) pour température, vent et pluie.
|
||||
|
||||
```shell
|
||||
python "docs/10 - Modèles non linéaires/scripts/run_tree_models.py"
|
||||
```
|
||||
|
||||
Le script :
|
||||
|
||||
- lit `data/weather_minutely.csv` et construit les variables dérivées (sin/cos, lags/deltas/moyennes, vent u/v, drapeaux) ;
|
||||
- s’appuie sur la matrice de corrélation décalée (chapitre 5) pour prioriser les variables/lags avec |r| ≥ 0,2, tout en conservant les cibles ;
|
||||
- sous-échantillonne l’apprentissage (1 ligne sur 10) pour contenir le temps de calcul, à garder en tête pour interpréter les scores ;
|
||||
- découpe en _train_/_validation_/_test_ (70/15/15 %) ;
|
||||
- entraîne forêts et gradient boosting pour température/vent (régression) et pluie binaire (classification) ;
|
||||
- exporte `models_tree_regression.csv` et `models_tree_rain.csv` dans `docs/10 - Modèles non linéaires/data/` ;
|
||||
- génère deux figures (validation) dans `docs/10 - Modèles non linéaires/figures/` :
|
||||
- `models_tree_mae_validation.png` (MAE vs horizon pour température et vent)
|
||||
- `models_tree_rain_validation.png` (F1 et Brier vs horizon pour la pluie)
|
||||
|
||||
## Lecture rapide des résultats (validation)
|
||||
|
||||

|
||||

|
||||
|
||||
- Température : le gradient boosting est meilleur que la forêt sur le très court terme (MAE ≈0,13 à +10 min), mais reste derrière les modèles linéaires du chapitre 9 (MAE ≈0,14 à +60 min avec Ridge). La sous‑utilisation des données d’apprentissage (1/10) pèse sur la performance.
|
||||
- Vent : gains modestes, MAE ~0,94 à +10 min (GB) et ~1,19 à +60 min, sans dépassement clair des modèles linéaires précédents.
|
||||
- Pluie : F1 ≈0,85 (forêt) et 0,67 (GB) à +10 min, mais toujours en dessous de la persistance (~0,94) ; le Brier reste modéré (~0,02–0,03). Aux horizons +60/+360/+1440, les scores retombent rapidement.
|
||||
|
||||
## Conclusion provisoire
|
||||
|
||||
Ces modèles non linéaires apportent de la flexibilité mais, avec un apprentissage allégé pour tenir le temps de calcul, ils ne battent pas les baselines ni les modèles linéaires sur les horizons courts. Pour progresser, il faudra soit élargir l’échantillon d’apprentissage (temps de calcul plus long), soit régler finement les hyperparamètres, soit enrichir les features (ou combiner les deux), tout en vérifiant que le gain justifie l’effort.
|
||||
359
docs/10 - Modèles non linéaires/scripts/run_tree_models.py
Normal file
359
docs/10 - Modèles non linéaires/scripts/run_tree_models.py
Normal file
@@ -0,0 +1,359 @@
|
||||
# scripts/run_tree_models.py
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Iterable, Sequence
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier, GradientBoostingRegressor, GradientBoostingClassifier
|
||||
from sklearn.metrics import (
|
||||
mean_absolute_error,
|
||||
mean_squared_error,
|
||||
f1_score,
|
||||
brier_score_loss,
|
||||
average_precision_score,
|
||||
)
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from meteo.dataset import load_raw_csv
|
||||
from model.features import build_feature_dataframe, FeatureSpec, _steps_from_minutes
|
||||
from model.splits import chronological_split
|
||||
|
||||
CSV_PATH = Path("data/weather_minutely.csv")
|
||||
DOC_DIR = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = DOC_DIR / "data"
|
||||
FIG_DIR = DOC_DIR / "figures"
|
||||
|
||||
HORIZONS_MINUTES: tuple[int, ...] = (10, 60, 360, 1440)
|
||||
CONTINUOUS_TARGETS: tuple[str, ...] = ("temperature", "wind_speed")
|
||||
RAIN_TARGET: str = "rain_rate"
|
||||
|
||||
DEFAULT_LAGS_BY_COL: dict[str, Sequence[int]] = {
|
||||
"temperature": (10, 20, 30),
|
||||
"wind_speed": (10, 20, 30),
|
||||
"rain_rate": (10, 20, 30),
|
||||
"humidity": (10, 20, 30),
|
||||
"pressure": (10, 20, 30),
|
||||
"illuminance": (10, 20, 30),
|
||||
"wind_direction": (10, 20, 30),
|
||||
"sun_elevation": (10, 20, 30),
|
||||
}
|
||||
|
||||
USE_CORR_FILTER = True
|
||||
CORR_THRESHOLD = 0.2
|
||||
CORR_PATH = Path("docs/05 - Corrélations binaires avancées/data/correlation_matrix_lagged.csv")
|
||||
LAG_MATRIX_PATH = Path("docs/05 - Corrélations binaires avancées/data/lag_matrix_minutes.csv")
|
||||
TRAIN_SUBSAMPLE_STEP = 10 # prend 1 ligne sur 10 pour accélérer l'entraînement des arbres
|
||||
|
||||
|
||||
def _align_target(
|
||||
df: pd.DataFrame,
|
||||
target_col: str,
|
||||
horizon_minutes: int,
|
||||
base_freq_minutes: int = 10,
|
||||
) -> tuple[pd.DataFrame, pd.Series]:
|
||||
steps = _steps_from_minutes(horizon_minutes, base_freq_minutes)
|
||||
y = df[target_col].shift(-steps)
|
||||
X_full = df.drop(columns=[target_col])
|
||||
X = X_full.select_dtypes(include=["number", "bool"])
|
||||
aligned = pd.concat([X, y.rename("target")], axis=1).dropna()
|
||||
return aligned.drop(columns=["target"]), aligned["target"]
|
||||
|
||||
|
||||
def _regression_scores(y_true: np.ndarray, y_pred: np.ndarray) -> dict[str, float]:
|
||||
return {
|
||||
"mae": float(mean_absolute_error(y_true, y_pred)),
|
||||
"rmse": float(np.sqrt(mean_squared_error(y_true, y_pred))),
|
||||
}
|
||||
|
||||
|
||||
def _classification_scores(y_true: np.ndarray, proba: np.ndarray, threshold: float = 0.5) -> dict[str, float]:
|
||||
y_pred = (proba >= threshold).astype(int)
|
||||
return {
|
||||
"f1": float(f1_score(y_true, y_pred, zero_division=0)),
|
||||
"brier": float(brier_score_loss(y_true, proba)),
|
||||
"ap": float(average_precision_score(y_true, proba)),
|
||||
}
|
||||
|
||||
|
||||
def _load_correlation_and_lag() -> tuple[pd.DataFrame | None, pd.DataFrame | None]:
|
||||
corr_df = pd.read_csv(CORR_PATH, index_col=0) if CORR_PATH.exists() else None
|
||||
lag_df = pd.read_csv(LAG_MATRIX_PATH, index_col=0) if LAG_MATRIX_PATH.exists() else None
|
||||
return corr_df, lag_df
|
||||
|
||||
|
||||
def _select_features_from_corr(
|
||||
corr_df: pd.DataFrame | None,
|
||||
targets: Sequence[str],
|
||||
threshold: float,
|
||||
) -> set[str]:
|
||||
if corr_df is None:
|
||||
return set()
|
||||
selected: set[str] = set()
|
||||
for target in targets:
|
||||
if target not in corr_df.columns:
|
||||
continue
|
||||
corrs = corr_df[target].drop(labels=[target], errors="ignore")
|
||||
strong = corrs[corrs.abs() >= threshold]
|
||||
selected.update(strong.index.tolist())
|
||||
return selected
|
||||
|
||||
|
||||
def _build_lags_from_matrices(
|
||||
lag_df: pd.DataFrame | None,
|
||||
corr_df: pd.DataFrame | None,
|
||||
selected_cols: Iterable[str],
|
||||
default_lags: dict[str, Sequence[int]],
|
||||
threshold: float,
|
||||
) -> dict[str, Sequence[int]]:
|
||||
mapping: dict[str, Sequence[int]] = {}
|
||||
for col in selected_cols:
|
||||
base = list(default_lags.get(col, (10, 20, 30)))
|
||||
extra: set[int] = set()
|
||||
if lag_df is not None and corr_df is not None and col in lag_df.index:
|
||||
corrs = corr_df.loc[col]
|
||||
for tgt, corr_val in corrs.items():
|
||||
if tgt == col:
|
||||
continue
|
||||
if abs(corr_val) < threshold:
|
||||
continue
|
||||
lag_val = lag_df.loc[col, tgt]
|
||||
if pd.notna(lag_val) and lag_val != 0:
|
||||
extra.add(int(abs(round(float(lag_val)))))
|
||||
merged = sorted({*base, *extra})
|
||||
mapping[col] = merged
|
||||
return mapping
|
||||
|
||||
|
||||
def run_regression_models(train_df: pd.DataFrame, val_df: pd.DataFrame, test_df: pd.DataFrame) -> pd.DataFrame:
|
||||
rows: list[dict[str, object]] = []
|
||||
models = [
|
||||
("rf", RandomForestRegressor(
|
||||
n_estimators=25,
|
||||
max_depth=8,
|
||||
min_samples_leaf=3,
|
||||
max_features="sqrt",
|
||||
n_jobs=-1,
|
||||
random_state=42,
|
||||
max_samples=0.25,
|
||||
)),
|
||||
("gbrt", GradientBoostingRegressor(
|
||||
n_estimators=50,
|
||||
learning_rate=0.08,
|
||||
max_depth=3,
|
||||
subsample=0.8,
|
||||
random_state=42,
|
||||
)),
|
||||
]
|
||||
for target_col in CONTINUOUS_TARGETS:
|
||||
for horizon in HORIZONS_MINUTES:
|
||||
X_train, y_train = _align_target(train_df, target_col, horizon)
|
||||
X_val, y_val = _align_target(val_df, target_col, horizon)
|
||||
X_test, y_test = _align_target(test_df, target_col, horizon)
|
||||
|
||||
if y_train.empty or y_val.empty or y_test.empty:
|
||||
continue
|
||||
|
||||
for model_name, model in models:
|
||||
model.fit(X_train, y_train)
|
||||
y_val_pred = model.predict(X_val)
|
||||
y_test_pred = model.predict(X_test)
|
||||
|
||||
val_scores = _regression_scores(y_val, y_val_pred)
|
||||
test_scores = _regression_scores(y_test, y_test_pred)
|
||||
|
||||
rows.append(
|
||||
{
|
||||
"target": target_col,
|
||||
"horizon_min": horizon,
|
||||
"model": model_name,
|
||||
"split": "validation",
|
||||
**val_scores,
|
||||
}
|
||||
)
|
||||
rows.append(
|
||||
{
|
||||
"target": target_col,
|
||||
"horizon_min": horizon,
|
||||
"model": model_name,
|
||||
"split": "test",
|
||||
**test_scores,
|
||||
}
|
||||
)
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def run_rain_models(train_df: pd.DataFrame, val_df: pd.DataFrame, test_df: pd.DataFrame) -> pd.DataFrame:
|
||||
rows: list[dict[str, object]] = []
|
||||
models = [
|
||||
("rf", RandomForestClassifier(
|
||||
n_estimators=40,
|
||||
max_depth=8,
|
||||
min_samples_leaf=3,
|
||||
max_features="sqrt",
|
||||
n_jobs=-1,
|
||||
random_state=42,
|
||||
class_weight="balanced",
|
||||
max_samples=0.25,
|
||||
)),
|
||||
("gbrt", GradientBoostingClassifier(
|
||||
n_estimators=50,
|
||||
learning_rate=0.08,
|
||||
max_depth=3,
|
||||
subsample=0.8,
|
||||
random_state=42,
|
||||
)),
|
||||
]
|
||||
target_col = RAIN_TARGET
|
||||
for horizon in HORIZONS_MINUTES:
|
||||
X_train, y_train = _align_target(train_df, target_col, horizon)
|
||||
X_val, y_val = _align_target(val_df, target_col, horizon)
|
||||
X_test, y_test = _align_target(test_df, target_col, horizon)
|
||||
|
||||
y_train_bin = (y_train > 0).astype(int)
|
||||
y_val_bin = (y_val > 0).astype(int)
|
||||
y_test_bin = (y_test > 0).astype(int)
|
||||
|
||||
if y_train_bin.empty or y_val_bin.empty or y_test_bin.empty:
|
||||
continue
|
||||
|
||||
for model_name, model in models:
|
||||
model.fit(X_train, y_train_bin)
|
||||
proba_val = model.predict_proba(X_val)[:, 1]
|
||||
proba_test = model.predict_proba(X_test)[:, 1]
|
||||
|
||||
val_scores = _classification_scores(y_val_bin, proba_val)
|
||||
test_scores = _classification_scores(y_test_bin, proba_test)
|
||||
|
||||
rows.append(
|
||||
{
|
||||
"target": "rain_binary",
|
||||
"horizon_min": horizon,
|
||||
"model": model_name,
|
||||
"split": "validation",
|
||||
**val_scores,
|
||||
}
|
||||
)
|
||||
rows.append(
|
||||
{
|
||||
"target": "rain_binary",
|
||||
"horizon_min": horizon,
|
||||
"model": model_name,
|
||||
"split": "test",
|
||||
**test_scores,
|
||||
}
|
||||
)
|
||||
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def plot_regression_mae(reg_df: pd.DataFrame, output_path: Path) -> None:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
df = reg_df[reg_df["split"] == "validation"]
|
||||
targets = df["target"].unique()
|
||||
models = df["model"].unique()
|
||||
|
||||
fig, axes = plt.subplots(len(targets), 1, figsize=(8, 4 * len(targets)), sharex=True)
|
||||
if len(targets) == 1:
|
||||
axes = [axes]
|
||||
for ax, target in zip(axes, targets):
|
||||
sub = df[df["target"] == target]
|
||||
for model in models:
|
||||
line = sub[sub["model"] == model].sort_values("horizon_min")
|
||||
ax.plot(line["horizon_min"], line["mae"], marker="o", label=model)
|
||||
ax.set_title(f"MAE {target} (validation)")
|
||||
ax.set_ylabel("MAE")
|
||||
ax.grid(True, linestyle=":", alpha=0.4)
|
||||
axes[-1].set_xlabel("Horizon (minutes)")
|
||||
axes[0].legend()
|
||||
fig.tight_layout()
|
||||
fig.savefig(output_path, dpi=150)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def plot_rain_f1_brier(rain_df: pd.DataFrame, output_path: Path) -> None:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
df = rain_df[rain_df["split"] == "validation"]
|
||||
models = df["model"].unique()
|
||||
|
||||
fig, axes = plt.subplots(2, 1, figsize=(8, 6), sharex=True)
|
||||
for metric, ax in zip(("f1", "brier"), axes):
|
||||
for model in models:
|
||||
line = df[df["model"] == model].sort_values("horizon_min")
|
||||
ax.plot(line["horizon_min"], line[metric], marker="o", label=model)
|
||||
ax.set_title(f"{metric.upper()} pluie (validation)" if metric == "f1" else "Brier pluie (validation)")
|
||||
ax.set_ylabel(metric.upper() if metric == "f1" else "Brier")
|
||||
ax.grid(True, linestyle=":", alpha=0.4)
|
||||
axes[-1].set_xlabel("Horizon (minutes)")
|
||||
axes[0].legend()
|
||||
fig.tight_layout()
|
||||
fig.savefig(output_path, dpi=150)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not CSV_PATH.exists():
|
||||
print(f"⚠ Fichier introuvable : {CSV_PATH}")
|
||||
return
|
||||
|
||||
df_raw = load_raw_csv(CSV_PATH)
|
||||
corr_df, lag_df = _load_correlation_and_lag()
|
||||
selected_from_corr = _select_features_from_corr(corr_df, CONTINUOUS_TARGETS + (RAIN_TARGET,), CORR_THRESHOLD) if USE_CORR_FILTER else set()
|
||||
|
||||
numeric_cols = df_raw.select_dtypes(include=["number", "bool"]).columns
|
||||
if USE_CORR_FILTER and selected_from_corr:
|
||||
selected_cols = [col for col in numeric_cols if col in selected_from_corr or col in CONTINUOUS_TARGETS or col == RAIN_TARGET]
|
||||
else:
|
||||
selected_cols = list(numeric_cols)
|
||||
|
||||
lags_mapping = _build_lags_from_matrices(
|
||||
lag_df,
|
||||
corr_df,
|
||||
selected_cols,
|
||||
default_lags=DEFAULT_LAGS_BY_COL,
|
||||
threshold=CORR_THRESHOLD,
|
||||
)
|
||||
|
||||
feature_spec = FeatureSpec(lags_minutes=lags_mapping)
|
||||
df_feat = build_feature_dataframe(df_raw[selected_cols], feature_spec=feature_spec, target_columns=selected_cols)
|
||||
|
||||
train_df, val_df, test_df = chronological_split(df_feat, train_frac=0.7, val_frac=0.15)
|
||||
if TRAIN_SUBSAMPLE_STEP > 1:
|
||||
train_df = train_df.iloc[::TRAIN_SUBSAMPLE_STEP]
|
||||
print(f"Dataset chargé : {CSV_PATH}")
|
||||
print(f" Train : {len(train_df)} lignes")
|
||||
print(f" Val : {len(val_df)} lignes")
|
||||
print(f" Test : {len(test_df)} lignes")
|
||||
print()
|
||||
|
||||
reg_results = run_regression_models(train_df, val_df, test_df)
|
||||
rain_results = run_rain_models(train_df, val_df, test_df)
|
||||
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
reg_path = DATA_DIR / "models_tree_regression.csv"
|
||||
rain_path = DATA_DIR / "models_tree_rain.csv"
|
||||
reg_results.to_csv(reg_path, index=False)
|
||||
rain_results.to_csv(rain_path, index=False)
|
||||
|
||||
FIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
plot_regression_mae(reg_results, FIG_DIR / "models_tree_mae_validation.png")
|
||||
plot_rain_f1_brier(rain_results, FIG_DIR / "models_tree_rain_validation.png")
|
||||
|
||||
print(f"✔ Résultats régression (arbres/GB) : {reg_path}")
|
||||
print(f"✔ Résultats pluie (arbres/GB) : {rain_path}")
|
||||
print()
|
||||
print("=== Scores régression (validation) ===")
|
||||
print(reg_results[reg_results["split"] == "validation"].to_string(index=False, float_format=lambda x: f"{x:.3f}"))
|
||||
print()
|
||||
print("=== Scores pluie (validation) ===")
|
||||
print(rain_results[rain_results["split"] == "validation"].to_string(index=False, float_format=lambda x: f"{x:.3f}"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user