prophet.metrics

  1# Copyright (c) Facebook, Inc. and its affiliates.
  2
  3# This source code is licensed under the MIT license found in the
  4# LICENSE file in the root directory of this source tree.
  5
  6from __future__ import annotations
  7
  8import logging
  9from typing import TYPE_CHECKING, Any, Callable, Final, Sequence, cast
 10
 11import numpy as np
 12import pandas as pd
 13
 14if TYPE_CHECKING:
 15    from typing import TypeAlias, TypeVar
 16
 17    _PerformanceMetric: TypeAlias = Callable[[pd.DataFrame, int], pd.DataFrame]
 18    _PerformanceMetricT = TypeVar("_PerformanceMetricT", bound=_PerformanceMetric)
 19
 20
 21logger: logging.Logger = logging.getLogger('prophet')
 22
 23PERFORMANCE_METRICS: Final[dict[str, Callable[..., Any]]] = {}
 24
 25
 26def register_performance_metric(func: Callable[..., Any]) -> Callable[..., Any]:
 27    """Register custom performance metric
 28
 29    Parameters that your metric should contain
 30    ----------
 31    df: Cross-validation results dataframe.
 32    w: Aggregation window size.
 33
 34    Registered metric should return following
 35    -------
 36    Dataframe with columns horizon and metric.
 37    """
 38    PERFORMANCE_METRICS[func.__name__] = func
 39    return func
 40
 41
 42def performance_metrics(
 43    df: pd.DataFrame,
 44    metrics: Sequence[str] | None = None,
 45    rolling_window: float = 0.1,
 46    monthly: bool = False,
 47) -> pd.DataFrame | None:
 48    """Compute performance metrics from cross-validation results.
 49
 50    Computes a suite of performance metrics on the output of cross-validation.
 51    By default, the following metrics are included:
 52    'mse': mean squared error
 53    'rmse': root mean squared error
 54    'mae': mean absolute error
 55    'mape': mean absolute percent error
 56    'mdape': median absolute percent error
 57    'smape': symmetric mean absolute percentage error
 58    'coverage': coverage of the upper and lower intervals
 59
 60    A subset of these can be specified by passing a list of names as the
 61    `metrics` argument.
 62
 63    Metrics are calculated over a rolling window of cross validation
 64    predictions, after sorting by horizon. Averaging is first done within each
 65    value of horizon, and then across horizons as needed to reach the window
 66    size. The size of that window (number of simulated forecast points) is
 67    determined by the rolling_window argument, which specifies a proportion of
 68    simulated forecast points to include in each window. rolling_window=0 will
 69    compute it separately for each horizon. The default of rolling_window=0.1
 70    will use 10% of the rows in df in each window. rolling_window=1 will
 71    compute the metric across all simulated forecast points. The results are
 72    set to the right edge of the window.
 73
 74    If rolling_window < 0, then metrics are computed at each datapoint with no
 75    averaging (i.e., 'mse' will actually be squared error with no mean).
 76
 77    The output is a dataframe containing column 'horizon' along with columns
 78    for each of the metrics computed.
 79
 80    Parameters
 81    ----------
 82    df: The dataframe returned by cross_validation.
 83    metrics: Performance metrics to compute. A sequence of names; if not
 84        provided, uses ['mse', 'rmse', 'mae', 'mape', 'mdape', 'smape',
 85        'coverage']. The caller's sequence is not modified.
 86    rolling_window: Proportion of data to use in each rolling window for
 87        computing the metrics. Should be in [0, 1] to average.
 88    monthly: monthly=True will compute horizons as numbers of calendar months
 89        from the cutoff date, starting from 0 for the cutoff month.
 90
 91    Returns
 92    -------
 93    Dataframe with a column for each metric, and column 'horizon'
 94    """
 95    valid_metrics = ['mse', 'rmse', 'mae', 'mape', 'mdape', 'smape', 'coverage']
 96    if metrics is None:
 97        metrics = valid_metrics
 98    # Copy so that removing skipped metrics below does not modify the caller's list
 99    metrics = list(metrics)
100    if ('yhat_lower' not in df or 'yhat_upper' not in df) and ('coverage' in metrics):
101        metrics.remove('coverage')
102    if len(set(metrics)) != len(metrics):
103        raise ValueError('Input metrics must be a list of unique values')
104    if not set(metrics).issubset(set(PERFORMANCE_METRICS)):
105        raise ValueError(
106            'Valid values for metrics are: {}'.format(valid_metrics)
107        )
108    df_m = df.copy()
109    if monthly:
110        df_m['horizon'] = df_m['ds'].dt.to_period('M').astype(int) - df_m['cutoff'].dt.to_period('M').astype(int)
111    else:
112        df_m['horizon'] = df_m['ds'] - df_m['cutoff']
113    df_m.sort_values('horizon', inplace=True)
114    if 'mape' in metrics and df_m['y'].abs().min() < 1e-8:
115        logger.info('Skipping MAPE because y close to 0')
116        metrics.remove('mape')
117    if len(metrics) == 0:
118        return None
119    w = int(rolling_window * df_m.shape[0])
120    if w >= 0:
121        w = max(w, 1)
122        w = min(w, df_m.shape[0])
123    # Compute all metrics
124    dfs = {}
125    for metric in metrics:
126        dfs[metric] = PERFORMANCE_METRICS[metric](df_m, w)
127    res = dfs[metrics[0]]
128    for i in range(1, len(metrics)):
129        res_m = dfs[metrics[i]]
130        assert np.array_equal(res['horizon'].values, res_m['horizon'].values)
131        res[metrics[i]] = res_m[metrics[i]]
132    return res
133
134
135def rolling_mean_by_h(x: np.ndarray, h: np.ndarray, w: int, name: str) -> pd.DataFrame:
136    """Compute a rolling mean of x, after first aggregating by h.
137
138    Right-aligned. Computes a single mean for each unique value of h. Each
139    mean is over at least w samples.
140
141    Parameters
142    ----------
143    x: Array.
144    h: Array of horizon for each value in x.
145    w: Integer window size (number of elements).
146    name: Name for metric in result dataframe
147
148    Returns
149    -------
150    Dataframe with columns horizon and name, the rolling mean of x.
151    """
152    # Aggregate over h
153    df = pd.DataFrame({'x': x, 'h': h})
154    df2 = (
155        df.groupby('h').agg(['sum', 'count']).reset_index().sort_values('h')
156    )
157    xs = df2['x']['sum'].values
158    ns = df2['x']['count'].values
159    hs = df2.h.values
160
161    trailing_i = len(df2) - 1
162    x_sum = 0
163    n_sum = 0
164    # We don't know output size but it is bounded by len(df2)
165    res_x = np.empty(len(df2))
166
167    # Start from the right and work backwards
168    for i in range(len(df2) - 1, -1, -1):
169        x_sum += xs[i]
170        n_sum += ns[i]
171        while n_sum >= w:
172            # Include points from the previous horizon. All of them if still
173            # less than w, otherwise weight the mean by the difference
174            excess_n = n_sum - w
175            excess_x = excess_n * xs[i] / ns[i]
176            res_x[trailing_i] = (x_sum - excess_x)/ w
177            x_sum -= xs[trailing_i]
178            n_sum -= ns[trailing_i]
179            trailing_i -= 1
180
181    res_h = hs[(trailing_i + 1):]
182    res_x = res_x[(trailing_i + 1):]
183
184    return pd.DataFrame({'horizon': res_h, name: res_x})
185
186
187
188def rolling_median_by_h(x: np.ndarray, h: np.ndarray, w: int, name: str) -> pd.DataFrame:
189    """Compute a rolling median of x, after first aggregating by h.
190
191    Right-aligned. Computes a single median for each unique value of h. Each
192    median is over at least w samples.
193
194    For each h where there are fewer than w samples, we take samples from the previous h,
195    moving backwards. (In other words, we ~ assume that the x's are shuffled within each h.)
196
197    Parameters
198    ----------
199    x: Array.
200    h: Array of horizon for each value in x.
201    w: Integer window size (number of elements).
202    name: Name for metric in result dataframe
203
204    Returns
205    -------
206    Dataframe with columns horizon and name, the rolling median of x.
207    """
208    # Aggregate over h
209    df = pd.DataFrame({'x': x, 'h': h})
210    grouped = df.groupby('h')
211    df2 = grouped.size().reset_index().sort_values('h')
212    hs = df2['h']
213
214    res_h = []
215    res_x = []
216    # Start from the right and work backwards
217    i = len(hs) - 1
218    while i >= 0:
219        h_i = hs[i]
220        xs = grouped.get_group(h_i).x.tolist()
221
222        # wrap in array so this works if h is pandas Series with custom index or numpy array
223        next_idx_to_add = np.array(h == h_i).argmax() - 1
224        while (len(xs) < w) and (next_idx_to_add >= 0):
225            # Include points from the previous horizon. All of them if still
226            # less than w, otherwise just enough to get to w.
227            xs.append(x[next_idx_to_add])
228            next_idx_to_add -= 1
229        if len(xs) < w:
230            # Ran out of points before getting enough.
231            break
232        res_h.append(hs[i])
233        res_x.append(np.median(xs))
234        i -= 1
235    res_h.reverse()
236    res_x.reverse()
237    return pd.DataFrame({'horizon': res_h, name: res_x})
238
239
240# The functions below specify performance metrics for cross-validation results.
241# Each takes as input the output of cross_validation, and returns the statistic
242# as a dataframe, given a window size for rolling aggregation.
243
244
245@register_performance_metric
246def mse(df: pd.DataFrame, w: int) -> pd.DataFrame:
247    """Mean squared error
248
249    Parameters
250    ----------
251    df: Cross-validation results dataframe.
252    w: Aggregation window size.
253
254    Returns
255    -------
256    Dataframe with columns horizon and mse.
257    """
258    se = (df['y'] - df['yhat']) ** 2
259    if w < 0:
260        return pd.DataFrame({'horizon': df['horizon'], 'mse': se})
261    return rolling_mean_by_h(
262        x=cast(np.ndarray, se.values),
263        h=cast(np.ndarray, df['horizon'].values),
264        w=w,
265        name='mse',
266    )
267
268
269@register_performance_metric
270def rmse(df: pd.DataFrame, w: int) -> pd.DataFrame:
271    """Root mean squared error
272
273    Parameters
274    ----------
275    df: Cross-validation results dataframe.
276    w: Aggregation window size.
277
278    Returns
279    -------
280    Dataframe with columns horizon and rmse.
281    """
282    res = mse(df, w)
283    res['mse'] = np.sqrt(res['mse'])
284    res.rename({'mse': 'rmse'}, axis='columns', inplace=True)
285    return res
286
287
288@register_performance_metric
289def mae(df: pd.DataFrame, w: int) -> pd.DataFrame:
290    """Mean absolute error
291
292    Parameters
293    ----------
294    df: Cross-validation results dataframe.
295    w: Aggregation window size.
296
297    Returns
298    -------
299    Dataframe with columns horizon and mae.
300    """
301    ae = (df['y'] - df['yhat']).abs()
302    if w < 0:
303        return pd.DataFrame({'horizon': df['horizon'], 'mae': ae})
304    return rolling_mean_by_h(
305        x=cast(np.ndarray, ae.values),
306        h=cast(np.ndarray, df['horizon'].values),
307        w=w,
308        name='mae',
309    )
310
311
312@register_performance_metric
313def mape(df: pd.DataFrame, w: int) -> pd.DataFrame:
314    """Mean absolute percent error
315
316    Parameters
317    ----------
318    df: Cross-validation results dataframe.
319    w: Aggregation window size.
320
321    Returns
322    -------
323    Dataframe with columns horizon and mape.
324    """
325    ape = ((df['y'] - df['yhat']) / df['y']).abs()
326    if w < 0:
327        return pd.DataFrame({'horizon': df['horizon'], 'mape': ape})
328    return rolling_mean_by_h(
329        x=cast(np.ndarray, ape.values),
330        h=cast(np.ndarray, df['horizon'].values),
331        w=w,
332        name='mape',
333    )
334
335
336@register_performance_metric
337def mdape(df: pd.DataFrame, w: int) -> pd.DataFrame:
338    """Median absolute percent error
339
340    Parameters
341    ----------
342    df: Cross-validation results dataframe.
343    w: Aggregation window size.
344
345    Returns
346    -------
347    Dataframe with columns horizon and mdape.
348    """
349    ape = ((df['y'] - df['yhat']) / df['y']).abs()
350    if w < 0:
351        return pd.DataFrame({'horizon': df['horizon'], 'mdape': ape})
352    return rolling_median_by_h(
353        x=cast(np.ndarray, ape.values),
354        h=cast(np.ndarray, df['horizon'].values),
355        w=w,
356        name='mdape',
357    )
358
359
360@register_performance_metric
361def smape(df: pd.DataFrame, w: int) -> pd.DataFrame:
362    """Symmetric mean absolute percentage error
363    based on Chen and Yang (2004) formula
364
365    Parameters
366    ----------
367    df: Cross-validation results dataframe.
368    w: Aggregation window size.
369
370    Returns
371    -------
372    Dataframe with columns horizon and smape.
373    """
374    sape = (df['y'] - df['yhat']).abs() / ((np.abs(df['y']) + np.abs(df['yhat'])) / 2)
375    sape = sape.fillna(0)
376    if w < 0:
377        return pd.DataFrame({'horizon': df['horizon'], 'smape': sape})
378    return rolling_mean_by_h(
379        x=cast(np.ndarray, sape.values),
380        h=cast(np.ndarray, df['horizon'].values),
381        w=w,
382        name='smape',
383    )
384
385
386@register_performance_metric
387def coverage(df: pd.DataFrame, w: int) -> pd.DataFrame:
388    """Coverage
389
390    Parameters
391    ----------
392    df: Cross-validation results dataframe.
393    w: Aggregation window size.
394
395    Returns
396    -------
397    Dataframe with columns horizon and coverage.
398    """
399    is_covered = (df['y'] >= df['yhat_lower']) & (df['y'] <= df['yhat_upper'])
400    if w < 0:
401        return pd.DataFrame({'horizon': df['horizon'], 'coverage': is_covered})
402    return rolling_mean_by_h(
403        x=cast(np.ndarray, is_covered.values),
404        h=cast(np.ndarray, df['horizon'].values),
405        w=w,
406        name='coverage',
407    )
408
409
410__all__ = [
411    "PERFORMANCE_METRICS",
412    "register_performance_metric",
413    "performance_metrics",
414    "rolling_mean_by_h",
415    "rolling_median_by_h",
416    "mse",
417    "rmse",
418    "mae",
419    "mape",
420    "mdape",
421    "smape",
422    "coverage",
423]
PERFORMANCE_METRICS: Final[dict[str, Callable[..., Any]]] = {'mse': <function mse>, 'rmse': <function rmse>, 'mae': <function mae>, 'mape': <function mape>, 'mdape': <function mdape>, 'smape': <function smape>, 'coverage': <function coverage>}
def register_performance_metric(func: Callable[..., Any]) -> Callable[..., Any]:
27def register_performance_metric(func: Callable[..., Any]) -> Callable[..., Any]:
28    """Register custom performance metric
29
30    Parameters that your metric should contain
31    ----------
32    df: Cross-validation results dataframe.
33    w: Aggregation window size.
34
35    Registered metric should return following
36    -------
37    Dataframe with columns horizon and metric.
38    """
39    PERFORMANCE_METRICS[func.__name__] = func
40    return func

Register custom performance metric

Parameters that your metric should contain

df: Cross-validation results dataframe. w: Aggregation window size.

Registered metric should return following

Dataframe with columns horizon and metric.

def performance_metrics( df: pandas.DataFrame, metrics: Optional[Sequence[str]] = None, rolling_window: float = 0.1, monthly: bool = False) -> pandas.DataFrame | None:
 43def performance_metrics(
 44    df: pd.DataFrame,
 45    metrics: Sequence[str] | None = None,
 46    rolling_window: float = 0.1,
 47    monthly: bool = False,
 48) -> pd.DataFrame | None:
 49    """Compute performance metrics from cross-validation results.
 50
 51    Computes a suite of performance metrics on the output of cross-validation.
 52    By default, the following metrics are included:
 53    'mse': mean squared error
 54    'rmse': root mean squared error
 55    'mae': mean absolute error
 56    'mape': mean absolute percent error
 57    'mdape': median absolute percent error
 58    'smape': symmetric mean absolute percentage error
 59    'coverage': coverage of the upper and lower intervals
 60
 61    A subset of these can be specified by passing a list of names as the
 62    `metrics` argument.
 63
 64    Metrics are calculated over a rolling window of cross validation
 65    predictions, after sorting by horizon. Averaging is first done within each
 66    value of horizon, and then across horizons as needed to reach the window
 67    size. The size of that window (number of simulated forecast points) is
 68    determined by the rolling_window argument, which specifies a proportion of
 69    simulated forecast points to include in each window. rolling_window=0 will
 70    compute it separately for each horizon. The default of rolling_window=0.1
 71    will use 10% of the rows in df in each window. rolling_window=1 will
 72    compute the metric across all simulated forecast points. The results are
 73    set to the right edge of the window.
 74
 75    If rolling_window < 0, then metrics are computed at each datapoint with no
 76    averaging (i.e., 'mse' will actually be squared error with no mean).
 77
 78    The output is a dataframe containing column 'horizon' along with columns
 79    for each of the metrics computed.
 80
 81    Parameters
 82    ----------
 83    df: The dataframe returned by cross_validation.
 84    metrics: Performance metrics to compute. A sequence of names; if not
 85        provided, uses ['mse', 'rmse', 'mae', 'mape', 'mdape', 'smape',
 86        'coverage']. The caller's sequence is not modified.
 87    rolling_window: Proportion of data to use in each rolling window for
 88        computing the metrics. Should be in [0, 1] to average.
 89    monthly: monthly=True will compute horizons as numbers of calendar months
 90        from the cutoff date, starting from 0 for the cutoff month.
 91
 92    Returns
 93    -------
 94    Dataframe with a column for each metric, and column 'horizon'
 95    """
 96    valid_metrics = ['mse', 'rmse', 'mae', 'mape', 'mdape', 'smape', 'coverage']
 97    if metrics is None:
 98        metrics = valid_metrics
 99    # Copy so that removing skipped metrics below does not modify the caller's list
100    metrics = list(metrics)
101    if ('yhat_lower' not in df or 'yhat_upper' not in df) and ('coverage' in metrics):
102        metrics.remove('coverage')
103    if len(set(metrics)) != len(metrics):
104        raise ValueError('Input metrics must be a list of unique values')
105    if not set(metrics).issubset(set(PERFORMANCE_METRICS)):
106        raise ValueError(
107            'Valid values for metrics are: {}'.format(valid_metrics)
108        )
109    df_m = df.copy()
110    if monthly:
111        df_m['horizon'] = df_m['ds'].dt.to_period('M').astype(int) - df_m['cutoff'].dt.to_period('M').astype(int)
112    else:
113        df_m['horizon'] = df_m['ds'] - df_m['cutoff']
114    df_m.sort_values('horizon', inplace=True)
115    if 'mape' in metrics and df_m['y'].abs().min() < 1e-8:
116        logger.info('Skipping MAPE because y close to 0')
117        metrics.remove('mape')
118    if len(metrics) == 0:
119        return None
120    w = int(rolling_window * df_m.shape[0])
121    if w >= 0:
122        w = max(w, 1)
123        w = min(w, df_m.shape[0])
124    # Compute all metrics
125    dfs = {}
126    for metric in metrics:
127        dfs[metric] = PERFORMANCE_METRICS[metric](df_m, w)
128    res = dfs[metrics[0]]
129    for i in range(1, len(metrics)):
130        res_m = dfs[metrics[i]]
131        assert np.array_equal(res['horizon'].values, res_m['horizon'].values)
132        res[metrics[i]] = res_m[metrics[i]]
133    return res

Compute performance metrics from cross-validation results.

Computes a suite of performance metrics on the output of cross-validation. By default, the following metrics are included: 'mse': mean squared error 'rmse': root mean squared error 'mae': mean absolute error 'mape': mean absolute percent error 'mdape': median absolute percent error 'smape': symmetric mean absolute percentage error 'coverage': coverage of the upper and lower intervals

A subset of these can be specified by passing a list of names as the metrics argument.

Metrics are calculated over a rolling window of cross validation predictions, after sorting by horizon. Averaging is first done within each value of horizon, and then across horizons as needed to reach the window size. The size of that window (number of simulated forecast points) is determined by the rolling_window argument, which specifies a proportion of simulated forecast points to include in each window. rolling_window=0 will compute it separately for each horizon. The default of rolling_window=0.1 will use 10% of the rows in df in each window. rolling_window=1 will compute the metric across all simulated forecast points. The results are set to the right edge of the window.

If rolling_window < 0, then metrics are computed at each datapoint with no averaging (i.e., 'mse' will actually be squared error with no mean).

The output is a dataframe containing column 'horizon' along with columns for each of the metrics computed.

Parameters
  • df (The dataframe returned by cross_validation.):

  • metrics (Performance metrics to compute. A sequence of names; if not): provided, uses ['mse', 'rmse', 'mae', 'mape', 'mdape', 'smape', 'coverage']. The caller's sequence is not modified.

  • rolling_window (Proportion of data to use in each rolling window for): computing the metrics. Should be in [0, 1] to average.
  • monthly (monthly=True will compute horizons as numbers of calendar months): from the cutoff date, starting from 0 for the cutoff month.
Returns
  • Dataframe with a column for each metric, and column 'horizon'
def rolling_mean_by_h( x: numpy.ndarray, h: numpy.ndarray, w: int, name: str) -> pandas.DataFrame:
136def rolling_mean_by_h(x: np.ndarray, h: np.ndarray, w: int, name: str) -> pd.DataFrame:
137    """Compute a rolling mean of x, after first aggregating by h.
138
139    Right-aligned. Computes a single mean for each unique value of h. Each
140    mean is over at least w samples.
141
142    Parameters
143    ----------
144    x: Array.
145    h: Array of horizon for each value in x.
146    w: Integer window size (number of elements).
147    name: Name for metric in result dataframe
148
149    Returns
150    -------
151    Dataframe with columns horizon and name, the rolling mean of x.
152    """
153    # Aggregate over h
154    df = pd.DataFrame({'x': x, 'h': h})
155    df2 = (
156        df.groupby('h').agg(['sum', 'count']).reset_index().sort_values('h')
157    )
158    xs = df2['x']['sum'].values
159    ns = df2['x']['count'].values
160    hs = df2.h.values
161
162    trailing_i = len(df2) - 1
163    x_sum = 0
164    n_sum = 0
165    # We don't know output size but it is bounded by len(df2)
166    res_x = np.empty(len(df2))
167
168    # Start from the right and work backwards
169    for i in range(len(df2) - 1, -1, -1):
170        x_sum += xs[i]
171        n_sum += ns[i]
172        while n_sum >= w:
173            # Include points from the previous horizon. All of them if still
174            # less than w, otherwise weight the mean by the difference
175            excess_n = n_sum - w
176            excess_x = excess_n * xs[i] / ns[i]
177            res_x[trailing_i] = (x_sum - excess_x)/ w
178            x_sum -= xs[trailing_i]
179            n_sum -= ns[trailing_i]
180            trailing_i -= 1
181
182    res_h = hs[(trailing_i + 1):]
183    res_x = res_x[(trailing_i + 1):]
184
185    return pd.DataFrame({'horizon': res_h, name: res_x})

Compute a rolling mean of x, after first aggregating by h.

Right-aligned. Computes a single mean for each unique value of h. Each mean is over at least w samples.

Parameters
  • x (Array.):

  • h (Array of horizon for each value in x.):

  • w (Integer window size (number of elements).):

  • name (Name for metric in result dataframe):

Returns
  • Dataframe with columns horizon and name, the rolling mean of x.
def rolling_median_by_h( x: numpy.ndarray, h: numpy.ndarray, w: int, name: str) -> pandas.DataFrame:
189def rolling_median_by_h(x: np.ndarray, h: np.ndarray, w: int, name: str) -> pd.DataFrame:
190    """Compute a rolling median of x, after first aggregating by h.
191
192    Right-aligned. Computes a single median for each unique value of h. Each
193    median is over at least w samples.
194
195    For each h where there are fewer than w samples, we take samples from the previous h,
196    moving backwards. (In other words, we ~ assume that the x's are shuffled within each h.)
197
198    Parameters
199    ----------
200    x: Array.
201    h: Array of horizon for each value in x.
202    w: Integer window size (number of elements).
203    name: Name for metric in result dataframe
204
205    Returns
206    -------
207    Dataframe with columns horizon and name, the rolling median of x.
208    """
209    # Aggregate over h
210    df = pd.DataFrame({'x': x, 'h': h})
211    grouped = df.groupby('h')
212    df2 = grouped.size().reset_index().sort_values('h')
213    hs = df2['h']
214
215    res_h = []
216    res_x = []
217    # Start from the right and work backwards
218    i = len(hs) - 1
219    while i >= 0:
220        h_i = hs[i]
221        xs = grouped.get_group(h_i).x.tolist()
222
223        # wrap in array so this works if h is pandas Series with custom index or numpy array
224        next_idx_to_add = np.array(h == h_i).argmax() - 1
225        while (len(xs) < w) and (next_idx_to_add >= 0):
226            # Include points from the previous horizon. All of them if still
227            # less than w, otherwise just enough to get to w.
228            xs.append(x[next_idx_to_add])
229            next_idx_to_add -= 1
230        if len(xs) < w:
231            # Ran out of points before getting enough.
232            break
233        res_h.append(hs[i])
234        res_x.append(np.median(xs))
235        i -= 1
236    res_h.reverse()
237    res_x.reverse()
238    return pd.DataFrame({'horizon': res_h, name: res_x})

Compute a rolling median of x, after first aggregating by h.

Right-aligned. Computes a single median for each unique value of h. Each median is over at least w samples.

For each h where there are fewer than w samples, we take samples from the previous h, moving backwards. (In other words, we ~ assume that the x's are shuffled within each h.)

Parameters
  • x (Array.):

  • h (Array of horizon for each value in x.):

  • w (Integer window size (number of elements).):

  • name (Name for metric in result dataframe):

Returns
  • Dataframe with columns horizon and name, the rolling median of x.
@register_performance_metric
def mse(df: pandas.DataFrame, w: int) -> pandas.DataFrame:
246@register_performance_metric
247def mse(df: pd.DataFrame, w: int) -> pd.DataFrame:
248    """Mean squared error
249
250    Parameters
251    ----------
252    df: Cross-validation results dataframe.
253    w: Aggregation window size.
254
255    Returns
256    -------
257    Dataframe with columns horizon and mse.
258    """
259    se = (df['y'] - df['yhat']) ** 2
260    if w < 0:
261        return pd.DataFrame({'horizon': df['horizon'], 'mse': se})
262    return rolling_mean_by_h(
263        x=cast(np.ndarray, se.values),
264        h=cast(np.ndarray, df['horizon'].values),
265        w=w,
266        name='mse',
267    )

Mean squared error

Parameters
  • df (Cross-validation results dataframe.):

  • w (Aggregation window size.):

Returns
  • Dataframe with columns horizon and mse.
@register_performance_metric
def rmse(df: pandas.DataFrame, w: int) -> pandas.DataFrame:
270@register_performance_metric
271def rmse(df: pd.DataFrame, w: int) -> pd.DataFrame:
272    """Root mean squared error
273
274    Parameters
275    ----------
276    df: Cross-validation results dataframe.
277    w: Aggregation window size.
278
279    Returns
280    -------
281    Dataframe with columns horizon and rmse.
282    """
283    res = mse(df, w)
284    res['mse'] = np.sqrt(res['mse'])
285    res.rename({'mse': 'rmse'}, axis='columns', inplace=True)
286    return res

Root mean squared error

Parameters
  • df (Cross-validation results dataframe.):

  • w (Aggregation window size.):

Returns
  • Dataframe with columns horizon and rmse.
@register_performance_metric
def mae(df: pandas.DataFrame, w: int) -> pandas.DataFrame:
289@register_performance_metric
290def mae(df: pd.DataFrame, w: int) -> pd.DataFrame:
291    """Mean absolute error
292
293    Parameters
294    ----------
295    df: Cross-validation results dataframe.
296    w: Aggregation window size.
297
298    Returns
299    -------
300    Dataframe with columns horizon and mae.
301    """
302    ae = (df['y'] - df['yhat']).abs()
303    if w < 0:
304        return pd.DataFrame({'horizon': df['horizon'], 'mae': ae})
305    return rolling_mean_by_h(
306        x=cast(np.ndarray, ae.values),
307        h=cast(np.ndarray, df['horizon'].values),
308        w=w,
309        name='mae',
310    )

Mean absolute error

Parameters
  • df (Cross-validation results dataframe.):

  • w (Aggregation window size.):

Returns
  • Dataframe with columns horizon and mae.
@register_performance_metric
def mape(df: pandas.DataFrame, w: int) -> pandas.DataFrame:
313@register_performance_metric
314def mape(df: pd.DataFrame, w: int) -> pd.DataFrame:
315    """Mean absolute percent error
316
317    Parameters
318    ----------
319    df: Cross-validation results dataframe.
320    w: Aggregation window size.
321
322    Returns
323    -------
324    Dataframe with columns horizon and mape.
325    """
326    ape = ((df['y'] - df['yhat']) / df['y']).abs()
327    if w < 0:
328        return pd.DataFrame({'horizon': df['horizon'], 'mape': ape})
329    return rolling_mean_by_h(
330        x=cast(np.ndarray, ape.values),
331        h=cast(np.ndarray, df['horizon'].values),
332        w=w,
333        name='mape',
334    )

Mean absolute percent error

Parameters
  • df (Cross-validation results dataframe.):

  • w (Aggregation window size.):

Returns
  • Dataframe with columns horizon and mape.
@register_performance_metric
def mdape(df: pandas.DataFrame, w: int) -> pandas.DataFrame:
337@register_performance_metric
338def mdape(df: pd.DataFrame, w: int) -> pd.DataFrame:
339    """Median absolute percent error
340
341    Parameters
342    ----------
343    df: Cross-validation results dataframe.
344    w: Aggregation window size.
345
346    Returns
347    -------
348    Dataframe with columns horizon and mdape.
349    """
350    ape = ((df['y'] - df['yhat']) / df['y']).abs()
351    if w < 0:
352        return pd.DataFrame({'horizon': df['horizon'], 'mdape': ape})
353    return rolling_median_by_h(
354        x=cast(np.ndarray, ape.values),
355        h=cast(np.ndarray, df['horizon'].values),
356        w=w,
357        name='mdape',
358    )

Median absolute percent error

Parameters
  • df (Cross-validation results dataframe.):

  • w (Aggregation window size.):

Returns
  • Dataframe with columns horizon and mdape.
@register_performance_metric
def smape(df: pandas.DataFrame, w: int) -> pandas.DataFrame:
361@register_performance_metric
362def smape(df: pd.DataFrame, w: int) -> pd.DataFrame:
363    """Symmetric mean absolute percentage error
364    based on Chen and Yang (2004) formula
365
366    Parameters
367    ----------
368    df: Cross-validation results dataframe.
369    w: Aggregation window size.
370
371    Returns
372    -------
373    Dataframe with columns horizon and smape.
374    """
375    sape = (df['y'] - df['yhat']).abs() / ((np.abs(df['y']) + np.abs(df['yhat'])) / 2)
376    sape = sape.fillna(0)
377    if w < 0:
378        return pd.DataFrame({'horizon': df['horizon'], 'smape': sape})
379    return rolling_mean_by_h(
380        x=cast(np.ndarray, sape.values),
381        h=cast(np.ndarray, df['horizon'].values),
382        w=w,
383        name='smape',
384    )

Symmetric mean absolute percentage error based on Chen and Yang (2004) formula

Parameters
  • df (Cross-validation results dataframe.):

  • w (Aggregation window size.):

Returns
  • Dataframe with columns horizon and smape.
@register_performance_metric
def coverage(df: pandas.DataFrame, w: int) -> pandas.DataFrame:
387@register_performance_metric
388def coverage(df: pd.DataFrame, w: int) -> pd.DataFrame:
389    """Coverage
390
391    Parameters
392    ----------
393    df: Cross-validation results dataframe.
394    w: Aggregation window size.
395
396    Returns
397    -------
398    Dataframe with columns horizon and coverage.
399    """
400    is_covered = (df['y'] >= df['yhat_lower']) & (df['y'] <= df['yhat_upper'])
401    if w < 0:
402        return pd.DataFrame({'horizon': df['horizon'], 'coverage': is_covered})
403    return rolling_mean_by_h(
404        x=cast(np.ndarray, is_covered.values),
405        h=cast(np.ndarray, df['horizon'].values),
406        w=w,
407        name='coverage',
408    )

Coverage

Parameters
  • df (Cross-validation results dataframe.):

  • w (Aggregation window size.):

Returns
  • Dataframe with columns horizon and coverage.