kdiagram.plot.uncertainty.plot_model_drift

kdiagram.plot.uncertainty.plot_model_drift(df, q_cols=None, q10_cols=None, q90_cols=None, horizons=None, color_metric_cols=None, acov='quarter_circle', value_label='Uncertainty Width (Q90 - Q10)', cmap='coolwarm', figsize=(8, 8), title='Model Forecast Drift Over Time', show_grid=True, annotate=True, grid_props=None, savefig=None, dpi=300, ax=None)[source]

Visualize forecast drift across prediction horizons.

Renders a polar bar chart depicting how average model uncertainty (or another metric) evolves as the forecast horizon increases. Each bar corresponds to a specific forecast horizon, arranged angularly.

This plot is crucial for diagnosing model degradation over longer lead times, often termed concept drift or model aging [1]. A distinct increase in bar height (radius) for later horizons signals inflating uncertainty or error, potentially indicating a need for model retraining or adjustments to account for changing dynamics. Use this visualization to assess if your model’s reliability holds as you forecast further into the future.

Parameters:
dfpandas.DataFrame

Input DataFrame containing the necessary quantile columns (or columns specified in color_metric_cols) for each forecast horizon.

q_colslist[tuple[str, str]], optional

A list where each element is a tuple containing the column names for the lower and upper quantiles for a specific horizon, e.g., [('q10_h1', 'q90_h1'), ('q10_h2', 'q90_h2')]. If None, q10_cols and q90_cols must be provided instead. Default is None.

q10_colslist[str], optional

List of column names representing the lower quantile (e.g., 10th percentile) for each successive horizon. Must be provided if q_cols is None. Must have the same length as q90_cols and horizons (if provided). Default is None.

q90_colslist[str], optional

List of column names representing the upper quantile (e.g., 90th percentile) for each successive horizon. Must be provided if q_cols is None. Must have the same length as q10_cols and horizons (if provided). Default is None.

horizonslist of str or int, optional

Labels corresponding to each forecast horizon (plotted on the angular axis). The order must match the order in q_cols or q10_cols/q90_cols. If None, generic labels like “Horizon 1”, “Horizon 2”, … are generated. Default is None.

color_metric_colslist of str, optional

If provided, the bars are colored based on the mean value of these columns for each horizon (e.g., provide RMSE columns like ['rmse_h1', 'rmse_h2', ...]). If None, bars are colored based on the calculated mean interval width (Q90-Q10). Default is None.

acov{‘default’, ‘half_circle’, ‘quarter_circle’,

‘eighth_circle’}, optional

Specifies the angular coverage (span) of the plot. Use narrower sectors for fewer horizons. Default is 'quarter_circle'.

  • 'default': Full circle (\(2\pi\), 360°).

  • 'half_circle': Half circle (\(\pi\), 180°).

  • 'quarter_circle': Quarter circle (\(\pi/2\), 90°).

  • 'eighth_circle': Eighth circle (\(\pi/4\), 45°).

value_labelstr, optional

Label displayed for the radial axis, describing the metric represented by the bar height. Default is 'Uncertainty Width (Q90 - Q10)'.

cmapstr, optional

Name of the Matplotlib colormap used to color the bars based on their radial value (or the color_metric_cols value). Default is 'coolwarm'.

figsizetuple of (float, float), optional

Figure dimensions (width, height) in inches. If None, uses the default (8, 8).

titlestr, optional

Headline text displayed above the plot. Default is 'Model Forecast Drift Over Time'.

show_gridbool, optional

If True, displays radial and angular grid lines to aid interpretation. Default is True.

annotatebool, optional

If True, displays the numeric value (mean width or mean color metric) on top of each bar. Default is True.

grid_propsdict, optional

Dictionary of keyword arguments to customize the appearance of the grid lines (passed to ax.grid()). E.g., {'linestyle': ':', 'linewidth': 0.5}. Default is None.

savefigstr, optional

Full path and filename to save the plot (e.g., ‘drift.pdf’). If None, the plot is displayed interactively. Default is None.

Returns:
matplotlib.axes.Axes

The polar axes object containing the bar chart, allowing for further customization if desired.

Raises:
ValueError

If required quantile columns (q_cols or both q10_cols and q90_cols) are missing from df, or if the lengths of provided column lists/horizons mismatch.

TypeError

If data in the specified columns cannot be processed numerically.

Parameters:

See also

kdiagram.plot.uncertainty.plot_uncertainty_drift

Visualize drift of the uncertainty pattern using rings.

kdiagram.utils.plot.set_axis_grid

Helper for grid styling (if used).

Notes

The primary radial value plotted for each horizon \(h\) is the mean interval width calculated across all \(N\) samples:

(1)\[\bar{w}_h = \frac{1}{N}\sum_{j=1}^{N} \left( Q_{up, j, h} - Q_{low, j, h} \right)\]

If color_metric_cols is provided, a similar average is calculated for those columns to determine bar color.

Radii may be scaled relative to the maximum radius if a restricted angular coverage (acov is not ‘default’) is used, to better fit the visual sector [2].

References

[1]

Kouadio, K. L., Liu, R., Loukou, K. G. H., Liu, J., & Liu, W. (2025). Analytics Framework for Interpreting Spatiotemporal Probabilistic Forecasts. International Journal of Forecasting. Manuscript submitted.

[2]

Gama, J., Žliobaitė, I., Bifet, A., Pechenizkiy, M., & Bouchachia, A. (2014). A survey on concept drift adaptation. ACM Computing Surveys (CSUR), 46(4), 1-37.

Examples

>>> import pandas as pd
>>> import numpy as np
>>> from kdiagram.plot.uncertainty import plot_model_drift
>>> # Example with synthetic data
>>> years = [2023, 2024, 2025, 2026]
>>> n_samples=50
>>> df_synth = pd.DataFrame()
>>> q10_cols, q90_cols = [], []
>>> for i, year in enumerate(years):
...     ql, qu = f'val_{year}_q10', f'val_{year}_q90'
...     q10_cols.append(ql); q90_cols.append(qu)
...     q10 = np.random.rand(n_samples)*5 + i*0.5
...     q90 = q10 + np.random.rand(n_samples)*2 + 1 + i*0.8
...     df_synth[ql]=q10; df_synth[qu]=q90
>>> ax = plot_model_drift(
...     df=df_synth,
...     q10_cols=q10_cols,
...     q90_cols=q90_cols,
...     horizons=years,
...     acov='quarter_circle', # Use 90 degree span
...     title='Synthetic Model Drift Example'
... )