API publique ecowave.forecasting¶
Module Python in-process vs contrat REST
Cette page documente le module Python ecowave.forecasting importable directement dans un notebook ou un script. Pour le contrat REST externe (service HTTP avec spec OpenAPI 3.1 formelle, implémentable serveur + requêtable client), voir API (contrat REST).
TL;DR
Module Python conteneurisé Docker, sous licence MIT. ProbabilisticForecast est le format pivot — tous les modèles le retournent. Pipeline composable : 6 modèles individuels → benchmark rolling-origin → scoring propre → sidecar JSON → consolidation cross-panel.
Dans cette page¶
- Architecture du package — diagramme des 10 modules
- Format pivot
ProbabilisticForecast— interface commune - Scoring propre — CRPS, coverage, MZ
- 6 modèles — baselines + cluster
- Pipeline benchmark — orchestration + verdict
- Reporting + consolidation — sidecar + page markdown
- Exemple end-to-end Python
Architecture du package¶
flowchart TB
Types[<b>types.py</b><br/>ProbabilisticForecast<br/>format pivot] --> Models
subgraph Models [Modèles forecast]
Baselines[baselines.py<br/>RW, AR(1), ARMA]
HAR[har.py<br/>Corsi 2009]
Fractional[fractional.py<br/>Hosking + GPH]
ARFIMA[arfima_rs.py]
MSM[msm.py]
end
Fractional --> ARFIMA
Models --> Bench[<b>benchmark.py</b><br/>Pipeline rolling-origin]
Scoring[<b>proper_scoring.py</b><br/>CRPS · coverage · MZ] --> Bench
Bench --> Reporting[<b>reporting.py</b><br/>Sidecar JSON + markdown]
Reporting --> Consolidated[<b>consolidated_report.py</b><br/>Cross-panel]
Consolidated --> HubIndex[<b>hub_index.py</b><br/>Live verdict sync]
style Types fill:#fff59d,stroke:#f9a825
style Bench fill:#90caf9,stroke:#1565c0
style HubIndex fill:#a5d6a7,stroke:#388e3c
types.py — Le format pivot¶
ProbabilisticForecast (dataclass frozen)¶
Toutes les fonctions *_forecast retournent un objet de ce type.
| Attribut | Type | Description |
|---|---|---|
horizons |
tuple[int, ...] |
Horizons en cadence steps |
samples |
np.ndarray shape (n_samples, H) |
Monte Carlo paths sur le niveau |
model_name |
str |
Identifiant court ("rw", "har", ...) |
metadata |
dict |
Paramètres estimés, flags de fit |
Méthodes :
mean→np.ndarray (H,): point forecast par horizonquantile(q)→ quantile(s) par horizonat(h)→np.ndarray (n_samples,): slice pour 1 horizonn_samples→ int
Source : types.py.
proper_scoring.py — Scoring rules¶
Fonctions principales¶
empirical_crps(samples: np.ndarray, observation: float) -> float
CRPS empirique via identité Gneiting-Raftery 2007 :
CRPS = E|X − y| − ½ E|X − X'|. Implémentation O(n log n) via la
formule rank-based pour la Gini-mean-difference.
coverage_indicator(samples, observation, level=0.95) -> int # 0 ou 1
tail_coverage_indicator(samples, observation, alpha=0.05, tail="left"|"right") -> int
score_forecast(samples, observation) -> ForecastScores
Bundle ForecastScores : rmse, mae, crps, coverage_95,
tail_coverage_left_5pct, tail_coverage_right_5pct, bias.
mincer_zarnowitz(predictions, realisations) -> MincerZarnowitzResult
F-test joint (α, β) = (0, 1). Retourne alpha, beta,
f_statistic, p_value, n_observations.
Source : proper_scoring.py.
baselines.py — RW, AR(1), ARMA(1, 1)¶
random_walk_forecast(history, horizons, n_samples=1000, seed=0) -> ProbabilisticForecast
ar1_forecast(history, horizons, n_samples=1000, seed=0) -> ProbabilisticForecast
arma11_forecast(history, horizons, n_samples=1000, seed=0) -> ProbabilisticForecast
Métadonnées exposées :
- RW :
sigma_increment,last_level - AR(1) :
intercept,phi,sigma_residual - ARMA(1,1) :
converged(booléen)
Fallbacks : AR(1) explosive (|φ| ≥ 0.999) → RW. ARMA(1,1) non
convergent → AR(1).
Source : baselines.py.
har.py — HAR Corsi 2009¶
HARLagConfig (dataclass frozen)¶
HARLagConfig(short=1, medium=3, long=12)
Validation : 0 < short < medium < long. Defaults monthly. Pour
quarterly utiliser HARLagConfig(1, 2, 4).
har_forecast¶
har_forecast(
history, horizons,
n_samples=1000, seed=0,
lag_config: HARLagConfig | None = None,
) -> ProbabilisticForecast
Métadonnées exposées : intercept, beta_short, beta_medium,
beta_long, sigma_residual, lag_config.
Source : har.py.
fractional.py — Hosking + GPH¶
Primitives réutilisables si vous voulez construire votre propre ARFIMA variant.
hosking_coefficients(d: float, length: int) -> np.ndarray
Récursion ψ_0 = 1, ψ_k = ψ_{k-1} · (k - 1 - d) / k. Validation
-0.499 ≤ d ≤ 0.499.
fractional_difference(series, d, truncate=None) -> np.ndarray
fractional_integrate(series, d, truncate=None) -> np.ndarray
Inverse l'un de l'autre (signe de d flippé).
gph_estimate_d(series, bandwidth_exponent=0.5) -> float
Geweke-Porter-Hudak log-periodogram regression. Clipping automatique
à [-0.499, 0.499].
Source : fractional.py.
arfima_rs.py — ARFIMA + Markov regime-switching¶
ARFIMARSConfig (dataclass frozen)¶
ARFIMARSConfig(
n_regimes=2,
bandwidth_exponent=0.5,
hosking_truncate=None,
switching_variance=True,
)
arfima_rs_forecast¶
arfima_rs_forecast(
history, horizons,
n_samples=1000, seed=0,
config: ARFIMARSConfig | None = None,
) -> ProbabilisticForecast
Pipeline interne : GPH → diff → MarkovRegression → simulation Markov + tirage Gaussien régime-conditionnel → reconstruction des niveaux par récursion inverse Hosking.
Métadonnées exposées : d_estimated, regime_fit_ok,
n_regimes_used, regime_means, regime_sigmas,
transition_matrix, bandwidth_exponent.
Source : arfima_rs.py.
msm.py — MSM Calvet-Fisher 2002¶
MSMConfig + MSMParameters (dataclasses frozen)¶
MSMConfig(
n_components=4, # K, supporté ∈ [2, 6]
use_log_returns=True, # si série > 0
fallback_on_failure=True,
)
MSMParameters(m_0, sigma_bar, b, gamma_1) # is_valid() pour bornes
msm_forecast¶
msm_forecast(
history, horizons,
n_samples=1000, seed=0,
config: MSMConfig | None = None,
) -> ProbabilisticForecast
Pipeline interne : extraction returns → ML par filtre forward Hamilton
sur 2^K états (L-BFGS-B avec box constraints + grille starting points)
→ tirage état initial depuis distribution filtrée terminale →
simulation composantes indépendantes → reconstruction niveau.
Métadonnées exposées : msm_fit_ok, n_components, return_mode
("log" ou "diff"), m_0, sigma_bar, b, gamma_1.
Source : msm.py.
benchmark.py — Pipeline¶
BenchmarkConfig (dataclass frozen)¶
BenchmarkConfig(
horizons=(1, 3, 6, 12),
models=("rw", "ar1", "arma11", "har", "arfima_rs", "msm"),
n_origins=8,
n_samples=200,
test_fraction=0.25,
seed=0,
har_lag_config=HARLagConfig(),
msm_components=4,
min_train_length=64,
)
Constantes utiles¶
CLUSTER_MODEL_NAMES = ("har", "arfima_rs", "msm")
BASELINE_MODEL_NAMES = ("rw", "ar1", "arma11")
ALL_MODEL_NAMES = BASELINE_MODEL_NAMES + CLUSTER_MODEL_NAMES
run_benchmark¶
run_benchmark(
panels: Mapping[str, Mapping[str, np.ndarray]],
config: BenchmarkConfig | None = None,
) -> BenchmarkResults
Signature des panels : {group_code: {variable_code: 1D level series}}.
Variables trop courtes (< min_train_length + max_horizon) sont
silencieusement skippées.
evaluate_acceptance_criterion¶
evaluate_acceptance_criterion(
results: BenchmarkResults,
decision_horizon=12,
beat_threshold=0.5,
baseline_model="rw",
) -> AcceptanceVerdict
Pour chaque variable, retient le best cluster model et son CRPS. Le
verdict passe si pass_rate ≥ beat_threshold.
Dataclasses output¶
ScoreRow— 1 par(group, variable, model, horizon, origin)BenchmarkResults—config,score_rows,failed_evaluations, méthodeper_cell_crps()AcceptanceVerdict—decision_horizon,pass_rate,passes,best_cluster_model_per_variable,cluster_beats_baseline_per_variable
Source : benchmark.py.
reporting.py — JSON + Markdown¶
aggregate_per_cell(results: BenchmarkResults) -> list[dict]
write_benchmark_sidecar(results, verdict, out_path, as_of, horizon_data_code)
render_benchmark_page(results, verdict, out_path, as_of, horizon_data_code)
Schéma sidecar : schema_version=1, voir documenté dans
consolidated_report.py.
Source : reporting.py.
consolidated_report.py — Cross-panel¶
PanelSummary + ConsolidatedSummary¶
Dataclasses frozen, propriétés calculées :
ConsolidatedSummary.total_variablesConsolidatedSummary.total_passingConsolidatedSummary.aggregate_pass_rateConsolidatedSummary.passesConsolidatedSummary.leaderboard()→list[tuple[str, int]]trié
Fonctions¶
consolidate_benchmark_sidecars(
reports_dir: Path,
as_of: str,
panel_codes: Iterable[str] = ("wb", "q", "long", "boe", "bis", "sh"),
beat_threshold: float = 0.5,
) -> ConsolidatedSummary
Schema guard : refuse sidecars schema_version != 1.
Consistency guard : refuse decision horizons hétérogènes.
Gracious : signale missing panels sans fail.
render_consolidated_page(summary, out_path) -> None
Markdown avec verdict global, table par panel, leaderboard, lecture qualitative, section reproduction.
Source : consolidated_report.py.
Exemple end-to-end Python¶
from pathlib import Path
import numpy as np
from ecowave.forecasting.benchmark import (
BenchmarkConfig,
evaluate_acceptance_criterion,
run_benchmark,
)
from ecowave.forecasting.reporting import (
render_benchmark_page,
write_benchmark_sidecar,
)
panels = {
"GROUP_A": {
"VAR1": np.cumsum(np.random.default_rng(0).normal(size=300)),
"VAR2": np.cumsum(np.random.default_rng(1).normal(size=300)),
}
}
config = BenchmarkConfig(
horizons=(1, 3, 12),
models=("rw", "har", "msm"),
n_origins=4,
n_samples=100,
seed=42,
)
results = run_benchmark(panels, config=config)
verdict = evaluate_acceptance_criterion(results, decision_horizon=12)
write_benchmark_sidecar(
results, verdict,
Path("/tmp/sidecar.json"),
as_of="2026-05",
horizon_data_code="custom",
)
render_benchmark_page(
results, verdict,
Path("/tmp/report.md"),
as_of="2026-05",
horizon_data_code="custom",
)
Tests¶
La suite couvre 225 tests dont 63 nouveaux sur le module forecasting :
tests/test_forecasting_types.py (6 tests)
tests/test_forecasting_proper_scoring.py (8 tests)
tests/test_forecasting_baselines.py (7 tests)
tests/test_forecasting_har.py (5 tests)
tests/test_forecasting_fractional.py (9 tests)
tests/test_forecasting_arfima_rs.py (8 tests)
tests/test_forecasting_msm.py (11 tests)
tests/test_forecasting_benchmark.py (11 tests)
tests/test_forecasting_consolidated.py (6 tests)
Lancer en Docker :
docker compose run --rm --entrypoint pytest ecowave tests/test_forecasting_*.py -v