Évaluation des prévisions probabilistes : CRPS, calibrage PIT et DeepAR
Ce blog a déjà plaidé en faveur de la prévision des distributions plutôt que des points, et a déjà déterminé quoi faire avec l'intervalle une fois que vous l'avez : prédiction conforme pour le dimensionnement des positions en fonction du risque dérive les intervalles sans distribution et la règle de dimensionnement qui les consomme, et l'article Temporal Fusion Transformer fournit un couche de sortie multi-quantile. Ce que ni l'un ni l'autre ne couvre, c'est la partie qui décide si l'un ou l'autre vaut la peine d'être fiable : comment vous évaluez une distribution prédictive et comment vous vérifiez que son incertitude déclarée est honnête.
C’est le sujet de cet article. Trois choses spécifiquement :
- CRPS — la règle de notation appropriée pour les prévisions probabilistes et sa relation exacte avec la perte du flipper que l'article de TFT rapporte déjà.
- L'histogramme PIT — un diagnostic d'étalonnage qui indique comment un modèle est mal calibré, et pas seulement s'il l'est.
- DeepAR — la famille de modèles d'échantillonnage autorégressif, qui apparaît sur ce blog uniquement comme note de bas de page de référence et n'est jamais expliquée.
Un élément de charpente d’abord, car il détermine les machines dont vous avez besoin. Une prévision ponctuelle échoue différemment selon le régime : dans une tendance à faible volatilité, la distribution conditionnelle est étroite et presque symétrique et une prévision ponctuelle est un bon résumé ; avant un événement programmé, il est bimodal et la moyenne conditionnelle se situe exactement là où le prix est le moins susceptible d'atterrir ; dans une crise, la queue gauche domine et la moyenne sous-estime considérablement les inconvénients. Trois routes récupèrent la distribution complète : paramétrique (prédire les paramètres d'une famille supposée : rapide, risque d'erreur de spécification), basée sur des quantiles (prédire une grille fixe : sans hypothèse, discrète) et basée sur des échantillons (chemins de Monte Carlo à partir d'un échantillonnage autorégressif, d'un abandon ou d'ensembles : flexibles, coûteux). Ces deux derniers dominent en finance précisément parce que la répartition change de forme selon ces régimes.
Prévisions quantiles, en bref

La perte du flipper et une grille quantile concrète sont déjà données dans la couche de sortie quantile TFT, je ne reformulerai donc pas la formule. La seule chose qui mérite d’être internalisée est son asymétrie : à La surestimation et la sous-prévision coûtent le même prix et la perte se réduit à MAE, mais à la sous-prévision est pénalisée plus qu’une surestimation. Ce rapport est le mécanisme — c'est ce qui entraîne la valeur ajustée jusqu'à un niveau que seulement 5 % des observations devraient dépasser.
Le problème pratique que l'article publié ne mentionne pas est le croisement de quantile : rien dans une perte par quantile n'empêche . Avec suffisamment de données et une structure partagée, c'est rare, mais cela se produit aux quantiles extrêmes sur des échantillons minces, et cela interrompt silencieusement tout calcul de CRPS ou de couverture en aval. La solution bon marché est une sorte post-hoc du vecteur quantile prédit ; celui de principe est un paramétrage de sortie monotone (prédire plus incréments non négatifs).
Une implémentation minimale à partir de zéro, utile lorsque vous souhaitez des résultats quantiles sans adopter de cadre de prévision :
import torch
import torch.nn as nn
class QuantileRegressionNet(nn.Module):
"""Multi-quantile forecasting network for financial returns."""
def __init__(self, input_dim: int, hidden_dim: int = 128,
quantiles: list[float] = [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]):
super().__init__()
self.quantiles = quantiles
self.backbone = nn.Sequential(
nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.2),
)
self.heads = nn.ModuleList([nn.Linear(hidden_dim, 1) for _ in quantiles])
def forward(self, x: torch.Tensor) -> torch.Tensor:
h = self.backbone(x)
out = torch.cat([head(h) for head in self.heads], dim=-1)
return torch.sort(out, dim=-1).values
def pinball_loss(predictions: torch.Tensor, targets: torch.Tensor,
quantiles: list[float]) -> torch.Tensor:
"""predictions: (batch, n_quantiles); targets: (batch, 1)."""
errors = targets - predictions
tau = torch.tensor(quantiles, device=predictions.device).unsqueeze(0)
return torch.max(tau * errors, (tau - 1) * errors).mean()
La largeur de l'intervalle correspond à la taille de la position, et nous travaillons sur cette cartographie dans son intégralité - y compris le filtre sans échange de ratio de bord - dans prédiction conforme pour un dimensionnement de position conscient du risque ; le l'équivalence avec le ciblage de la volatilité est dérivée dans [ciblage de la volatilité avec les prévisions GARCH] (/en/blog/post/volatility-targeting-garch-strategy).
DeepAR : prévision probabiliste autorégressive

DeepAR (Salinas et al., 2020) emprunte la voie basée sur des échantillons. Plutôt que de prédire directement les quantiles, il utilise un RNN autorégressif pour paramétrer une probabilité à chaque étape, puis dessine des distributions prédictives complètes en faisant avancer cette probabilité.
Architecture. À chaque étape le réseau consomme l'observation précédente (mis à l'échelle par un facteur par série), covariables et des fonctionnalités statiques facultatives. Un LSTM porte l’état :
Une tête dense aux paramètres de vraisemblance — pour la Gaussienne, pour Étudiant-t. La formation maximise sur toutes les séries. Lors de l'inférence, vous échantillonnez à partir de , réinjectez l'échantillon comme entrée suivante et répétez fois pour obtenir trajectoires.
Deux conséquences comptent pour le trading. La probabilité est un choix de modélisation, donc les grosses queues sont quelque chose que vous sélectionnez plutôt que quelque chose que vous espérez — Student-t pour les queues lourdes, ou un mélange gaussien pour le comportement d'événement bimodal. Les prévisions en plusieurs étapes sont cohérentes : chaque chemin d'échantillonnage est une trajectoire plausible qui préserve la corrélation en série, ce dont vous avez besoin pour tout horizon supérieur à une étape. (Le troisième argument de vente habituel : un modèle global réparti dans de nombreuses séries bat modèles par actif - est le même argument sur l'efficacité des données présenté dans l'[article TFT] (/en/blog/post/temporal-fusion-transformer-trading), avec une mise à l'échelle par série et des covariables statiques jouant le rôle des encodeurs statiques.)
DeepAR avec GluonTS
import pandas as pd
from gluonts.dataset.pandas import PandasDataset
from gluonts.torch.model.deepar import DeepAREstimator
from gluonts.torch.distributions import StudentTOutput
from gluonts.evaluation import make_evaluation_predictions, Evaluator
def prepare_crypto_dataset(returns_df: pd.DataFrame, freq: str = "h"):
"""Wide return frame (index=datetime, columns=assets) -> GluonTS dataset.
from_long_dataframe wants LONG format: one row per (timestamp, item_id)
with the target in a column. Passing a DataFrame of dicts does not work.
"""
long_df = (
returns_df.stack()
.rename("target")
.rename_axis(index=["timestamp", "item_id"])
.reset_index()
.dropna(subset=["target"])
)
return PandasDataset.from_long_dataframe(
long_df, target="target", item_id="item_id",
timestamp="timestamp", freq=freq,
)
estimator = DeepAREstimator(
prediction_length=24, # 24 hours ahead
context_length=168, # one week of hourly data
freq="h",
num_layers=2,
hidden_size=64,
dropout_rate=0.1,
lr=1e-3,
batch_size=64,
trainer_kwargs={"max_epochs": 50},
distr_output=StudentTOutput(),
)
predictor = estimator.train(training_data=train_dataset)
forecast_it, ts_it = make_evaluation_predictions(
dataset=test_dataset, predictor=predictor, num_samples=500,
)
forecasts, actuals = list(forecast_it), list(ts_it)
evaluator = Evaluator(quantiles=[0.05, 0.25, 0.5, 0.75, 0.95])
agg_metrics, item_metrics = evaluator(actuals, forecasts)
print(f"mean_wQuantileLoss: {agg_metrics['mean_wQuantileLoss']:.4f}")
num_samples contrôle le nombre de chemins de Monte Carlo. Pour une utilisation en direct, 100 à 200 suffisent généralement ; pour une évaluation hors ligne, utilisez 500-1 000, car le CRPS estimé à partir d'un trop petit nombre d'échantillons est biaisé.
Autres voies vers une distribution prédictive
Trois alternatives valent la peine d'être connues mais n'ont pas besoin de leurs propres sections. MC Dropout (Gal et Ghahramani, 2016) maintient l'abandon actif lors de l'inférence et prend la moyenne et la variance sur l'ensemble passes avant; il s'agit d'une inférence variationnelle approximative déguisée, et c'est le moyen le plus rapide d'intégrer l'incertitude sur un modèle que vous avez déjà entraîné. Ensembles profonds (Lakshminarayanan et al., 2017) s'entraînent copies à partir de différentes graines et traitent la distribution prédictive comme un mélange - toujours solide dans les benchmarks, à le coût, ce qui l'exclut pour les horizons sensibles à la latence mais pas pour 4h ou quotidiennement. La normalisation des flux (Rasul et al., 2021) apprend une carte inversible d'une simple densité de base à une cible arbitraire, capturant la multimodalité et l'asymétrie sans choisir de famille paramétrique. Tous les trois produisent des échantillons, donc tout dans la section suivante s’applique à eux sans changement.
CRPS : la bonne métrique pour une distribution prédictive

MSE et MAE marquent des prévisions de points. Ils ne peuvent pas vous dire si une distribution était bonne, car ils n'en regardent qu'un seul résumé. Le remplacement standard est le score de probabilité classé continu, et c'est l'élément le plus utile de cet article.
Le CRPS est la distance carrée intégrée entre le CDF prédit et le CDF dégénéré qui met toute la masse sur ce qui s'est réellement passé :
Trois propriétés lui valent sa place :
- C'est une règle de notation appropriée (Gneiting et Raftery, 2007) : minimisée en attente uniquement lorsque la distribution prédite est égale à la vraie. Ni un excès de confiance délibéré ni une couverture avec une distribution artificiellement large n’améliorent votre score. MSE-on-the-median n'a pas une telle propriété, c'est pourquoi CRPS n'est pas facultatif.
- Il généralise le MAE. Pour une prévision ponctuelle dégénérée, elle se réduit à une erreur absolue, donc le CRPS vit dans les unités de la cible — en prévoyant les rendements horaires du journal, un CRPS de 0,004 est directement comparable à une erreur absolue moyenne de 40 bps, plutôt qu'à un nombre sans unité que vous ne pouvez pas vérifier.
- Il récompense la netteté sous réserve de calibrage. Entre deux prévisions également calibrées, les scores les plus étroits sont meilleurs. C’est aussi pourquoi le SDRC seul est insuffisant : un mauvais score ne vous indique pas laquelle des deux conditions a échoué, c’est à cela que sert la section suivante.
Le pont vers la perte quantile
C’est la connexion qui manque au reste du blog. Étant donné une grille quantile plutôt qu'un CDF complet, le CRPS est approximé par la perte du flipper :
Lisez cela littéralement : la "perte quantile" rapportée dans l'article du TFT et le CRPS discuté ici sont la même quantité jusqu'à un facteur 2, l'approximation se resserrant à mesure que la grille se densifie. Il ne s’agit pas de mesures concurrentes et il n’y a aucune raison de rapporter les deux.
Une mise en garde, car ça mord. GluonTS mean_wQuantileLoss est la même perte moyenne de flipper normalisée par la somme des valeurs cibles absolues, ce qui la rend sans dimension et comparable entre des actifs de différentes échelles. Le SDRC proprement dit n'est pas normalisé et reste dans les unités de retour. Ne pas imprimer mean_wQuantileLoss sous une étiquette qui dit « CRPS » — la version préliminaire de cet article a fait exactement cela, et c'est un moyen simple de comparer deux nombres qui ne sont pas sur la même échelle.
SDRC à partir d'échantillons
Avec des échantillons de Monte Carlo (DeepAR, ensembles, flux), utilisez la forme énergétique :
Le premier terme récompense la précision, le second pénalise la surdispersion. Écrit naïvement, le deuxième terme est et devient le goulot d'étranglement lorsque vous obtenez des milliers de prévisions. Le tri le réduit d'abord : pour les statistiques par ordre croissant , la double somme est égale , donc tout est dominé par le genre.
import numpy as np
def crps_empirical(samples: np.ndarray, observation: float) -> float:
"""CRPS from Monte Carlo samples, O(n log n) via order statistics."""
n = len(samples)
mae = np.mean(np.abs(samples - observation))
x = np.sort(samples)
k = np.arange(1, n + 1)
dispersion = np.sum((2 * k - n - 1) * x) / n**2
return mae - dispersion
def crps_quantile(quantile_predictions: np.ndarray,
quantile_levels: np.ndarray,
observation: float) -> float:
"""CRPS approximation from a quantile grid (2x mean pinball loss)."""
errors = observation - quantile_predictions
pinball = np.where(errors >= 0,
quantile_levels * errors,
(quantile_levels - 1) * errors)
return 2.0 * np.mean(pinball)
Les deux formes doivent s’accorder étroitement sur la même distribution prédictive ; si ce n’est pas le cas, soupçonnez un croisement de quantiles ou un nombre insuffisant d’échantillons. En production, properscoring.crps_ensemble(observation, samples) est une halte-accueil bien testée.
Calibrage : l'incertitude déclarée est-elle honnête ?

Un bon SDRC ne garantit pas que les intervalles signifient ce qu'ils prétendent. Un modèle dont l'intervalle nominal de 90 % capture 70 % des résultats est trop confiant, et dans une règle de dimensionnement qui lit la largeur de l'intervalle, il vous soutiendra précisément quand il ne le devrait pas. L'article de TFT énonce l'avertissement et prescrit une prédiction conforme comme correctif ; ce qui suit est la manière dont vous détectez et diagnostiquez réellement la panne.
L'histogramme PIT
Pour chaque observation , calculez son quantile sous le CDF prédit pour cette étape :
Si le modèle est calibré, est uniforme sur . La puissance du diagnostic réside dans le fait que la forme de l’écart désigne le mode de défaillance :
- En forme de U : trop confiant — trop de masse atterrit dans les queues, la distribution est trop étroite.
- En forme de bosse : peu confiant — les observations se regroupent près du centre, la distribution est trop large.
- inclinaison à gauche : le modèle surestime systématiquement.
- inclinaison à droite : le modèle sous-estime systématiquement.
Une note pour éviter toute confusion avec modèles de copules pour risque commun, qui utilise également la transformation intégrale de probabilité : il s'agit là d'une transformation marginale, une étape de prétraitement qui convertit les marginaux GARCH-EVT en observations pseudo-uniformes afin qu'une copule puisse y être ajustée. Ici, la transformation est appliquée aux prévisions hors échantillon et l'uniformité est le résultat testé, et non une entrée fabriquée. Mêmes mathématiques, sens d'inférence opposé.
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import kstest
def pit_calibration_check(forecasts, actuals, n_bins=20):
"""PIT histogram + KS test against uniform.
forecasts: list of SampleForecast objects (GluonTS)
"""
pit_values = []
for forecast, actual in zip(forecasts, actuals):
samples = forecast.samples # (n_samples, prediction_length)
h = forecast.prediction_length
for t in range(h):
obs = actual.values[-h + t]
pit_values.append(np.mean(samples[:, t] <= obs))
pit_values = np.array(pit_values)
ks_stat, p_value = kstest(pit_values, "uniform")
fig, ax = plt.subplots(figsize=(8, 4))
ax.hist(pit_values, bins=n_bins, density=True, alpha=0.7, edgecolor="black")
ax.axhline(y=1.0, color="red", linestyle="--", label="Perfect calibration")
ax.set_xlabel("PIT value")
ax.set_ylabel("Density")
ax.set_title(f"PIT Histogram (KS={ks_stat:.3f}, p={p_value:.3f})")
ax.legend()
plt.tight_layout()
return pit_values, ks_stat, p_value
Deux mises en garde sur le test KS. Il suppose des valeurs PIT indépendantes et les prévisions multihorizons qui se chevauchent sont fortement autocorrélées : traitez donc la valeur p comme un drapeau approximatif et la forme de l'histogramme comme la véritable preuve. Et avec suffisamment d'observations, le test rejette l'uniformité en raison d'un mauvais étalonnage trop faible pour avoir de l'importance ; c’est l’ampleur de l’effet qui devrait motiver une décision.
Vérification de la couverture
Un diagnostic plus grossier mais plus directement exploitable : faites le % d'intervalles contiennent % de résultats ?
import numpy as np
import pandas as pd
def coverage_table(forecasts, actuals, levels=(0.50, 0.80, 0.90, 0.95)):
"""Empirical coverage at multiple nominal levels."""
results = {}
for level in levels:
lower_q = (1 - level) / 2
upper_q = 1 - lower_q
covered = total = 0
for forecast, actual in zip(forecasts, actuals):
samples = forecast.samples
h = forecast.prediction_length
for t in range(h):
obs = actual.values[-h + t]
lo = np.quantile(samples[:, t], lower_q)
hi = np.quantile(samples[:, t], upper_q)
covered += int(lo <= obs <= hi)
total += 1
empirical = covered / total
results[f"{int(level*100)}% interval"] = {
"nominal": level, "empirical": empirical,
"gap": empirical - level,
}
return pd.DataFrame(results).T
La règle générale : les écarts inférieurs à 2 points de pourcentage sont du bruit pour des tailles d'échantillon typiques, et les écarts au-delà de 3 à 5 points sont un véritable problème d'étalonnage qui apparaîtra dans le dimensionnement des positions. Exécutez-le par horizon, et non en pool : la couverture se dégrade presque toujours à mesure que l'horizon s'étend et le pooling le cache.
Lorsque l'étalonnage échoue
Trois réparations standards, par ordre croissant de garantie. La mise à l'échelle de la température divise le paramètre d'échelle prédit par une valeur apprise. ajusté sur des données retenues - un paramètre, trivialement bon marché, corrige une sur/sous-confiance uniforme mais rien ne dépend de la forme. Le réétalonnage isotonique mappe les niveaux quantiles prédits aux fréquences observées de manière monotone, ce qui gère la distorsion de forme mais nécessite un ensemble d'étalonnage raisonnablement important. La prédiction conforme enveloppe n'importe quel modèle et offre des garanties de couverture sur échantillons finis ; l'algorithme de division conforme complet, le classement statistique d'ordre exact et les pièges d'interpolation et de serrage qu'un résumé d'une ligne invite se trouvent tous dans prédiction conforme pour le trading.
Considérations pratiques

Non-stationnarité. L'étalonnage dérive à mesure que les régimes de volatilité changent, de sorte qu'un ensemble d'étalonnage fixe se désintègre. Le mécanisme conçu exactement pour cela est l'inférence conforme adaptative, qui met à jour un niveau de couverture erronée en ligne et offre une garantie de couverture à long terme même dans des séquences contradictoires — voir la section ACI de prédiction conforme pour le trading.
Coût de calcul. DeepAR avec 500 exemples de chemins coûte environ une seule passe avant. Pour le travail intrajournalier préférez la régression quantile (tous les quantiles en un seul passage) ou une tête paramétrique (prédire une fois); réserver l'échantillonnage autorégressif pour les horizons 4h et quotidiens.
Changements de régime. Associez le prévisionniste à un détecteur de régime et conservez les paramètres d'étalonnage par régime — détection de régime avec HMM a la mise en œuvre et le backtest fonctionnels.
Prévisions multivariées. Les distributions marginales par actif ne suffisent pas pour le risque du portefeuille ; ce qui compte, c'est la queue commune, et les modèles de copule pour le risque commun quantifient à quel point les hypothèses d'indépendance la sous-estiment.
Consommateurs en aval. La VaR et le déficit attendu proviennent directement d'une prévision basée sur un échantillon en tant que quantile et moyenne conditionnelle - les définitions et la recette de Monte Carlo se trouvent dans modèles de copule pour le risque commun. Le dimensionnement de Kelly est la seule chose qui n'est pas gratuite : dériver une fraction de Kelly à partir d'un intervalle de prévision nécessite une hypothèse supplémentaire sur la distribution au sein de cet intervalle, et l'article conforme plaide explicitement contre l'ajout d'un rapport d'intervalle sur . Voir le critère de Kelly pour les stratégies pour savoir ce que la fraction nécessite réellement.
Conclusion

La pile d'évaluation pour une prévision probabiliste est courte et non négociable : CRPS pour le score, car il est propre et vit dans les unités de votre cible ; l'histogramme PIT pour le diagnostic, car il nomme le mode de défaillance plutôt que de simplement en signaler un ; couverture par horizon pour la décision, car c'est le nombre qui correspond à la taille de la position. Tout ce qui est signalé comme « perte quantile » correspond à un CRPS jusqu'à un facteur de 2, il y a donc une métrique ici, pas deux.
Ce qui est inconfortable, c’est que tout cela n’est qu’un mécanisme permettant de découvrir qu’un modèle est pire que ce que vous espériez. C'est là le point. Un modèle qui élargit honnêtement ses intervalles lorsqu’il ne sait pas est strictement plus utile qu’un modèle qui reste étroit avec assurance, et la seule façon de les distinguer est de les noter correctement.
Références
- Salinas, D., Flunkert, V., Gasthaus, J. et Januschowski, T. (2020). "DeepAR : prévision probabiliste avec réseaux récurrents autorégressifs." Journal international de prévision, 36(3), 1181-1191.
- Koenker, R. & Bassett, G. (1978). «Quantiles de régression». Économétrique, 46(1), 33-50.
- Gneiting, T. & Raftery, AE (2007). "Règles de notation, prédiction et estimation strictement appropriées." Journal de l'American Statistical Association, 102(477), 359-378.
- Gneiting, T., Balabdaoui, F. et Raftery, AE (2007). "Prévisions probabilistes, calibrage et netteté." Journal de la Royal Statistical Society : Série B, 69(2), 243-268.
- Gal, Y. et Ghahramani, Z. (2016). "L'abandon comme approximation bayésienne : représenter l'incertitude du modèle dans l'apprentissage profond." ICML.
- Lakshminarayanan, B., Pritzel, A. et Blundell, C. (2017). "Estimation prédictive simple et évolutive de l'incertitude à l'aide d'ensembles profonds." NeurIPS.
- Rasul, K., Sheikh, AS, Schuster, I., Bergmann, U. et Vollgraf, R. (2021). "Prévision de séries chronologiques probabilistes multivariées via des flux de normalisation conditionnés." ICLR.
- GluonTS : Modélisation probabiliste de séries temporelles en Python. https://ts.gluon.ai
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.