# Differences Source: https://nixtlaverse.nixtla.io/coreforecast/differences Find the optimal number of differences ## ### `num_seas_diffs` ```python theme={null} num_seas_diffs(x, season_length, max_d=1) ``` Determine the optimal number of seasonal differences for stationarity. Uses a seasonal strength heuristic based on STL decomposition to determine if seasonal differencing is needed. The function applies seasonal differencing iteratively until the seasonal strength falls below the threshold or the maximum number of differences is reached. **Parameters:** | Name | Type | Description | Default | | --------------- | -------------------------------------- | --------------------------------------------------------- | -------------- | | `x` | [ndarray](#numpy.ndarray) | Array with the time series. | *required* | | `season_length` | [int](#int) | Length of the seasonal pattern. | *required* | | `max_d` | [int](#int) | Maximum number of differences to consider. Defaults to 1. | 1 | **Returns:** | Name | Type | Description | | ----- | ------------------------ | --------------------------------------- | | `int` | [int](#int) | Optimal number of seasonal differences. | ### `num_diffs` ```python theme={null} num_diffs(x, max_d=1) ``` Determine the optimal number of non-seasonal differences for stationarity. Uses the KPSS (Kwiatkowski-Phillips-Schmidt-Shin) test to determine how many times the series needs to be differenced to achieve stationarity. The function applies differencing iteratively until the KPSS statistic falls below the threshold or the maximum number of differences is reached. **Parameters:** | Name | Type | Description | Default | | ------- | -------------------------------------- | --------------------------------------------------------- | -------------- | | `x` | [ndarray](#numpy.ndarray) | Array with the time series. | *required* | | `max_d` | [int](#int) | Maximum number of differences to consider. Defaults to 1. | 1 | **Returns:** | Name | Type | Description | | ----- | ------------------------ | ------------------------------ | | `int` | [int](#int) | Optimal number of differences. | ### `diff` ```python theme={null} diff(x, d) ``` Subtract previous values of the series **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------- | --------------------------- | ---------- | | `x` | [ndarray](#numpy.ndarray) | Array with the time series. | *required* | | `d` | [int](#int) | Lag to subtract | *required* | **Returns:** | Type | Description | | -------------------------------------- | ------------------------------------ | | [ndarray](#numpy.ndarray) | np.ndarray: Differenced time series. | # Expanding Source: https://nixtlaverse.nixtla.io/coreforecast/expanding Compute expanding mean, std, min, max, and quantile ## ### `expanding_mean` ```python theme={null} expanding_mean(x, skipna=False) ``` Compute the expanding\_mean of the input array. **Parameters:** | Name | Type | Description | Default | | -------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `skipna` | bool | If True, exclude NaN values from calculations. When False (default), any NaN value causes the result to be NaN, maintaining backwards compatibility. When True, NaN values are ignored (matching pandas default behavior). | *required* | **Returns:** | Type | Description | | ---------------------------------------------- | ----------- | | np.ndarray: Array with the expanding statistic | | **Examples:** ```pycon theme={null} >>> import numpy as np >>> x = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) >>> # Default behavior: NaN propagates >>> expanding_mean(x) array([1., 1.5, nan, nan, nan]) >>> # With skipna=True: NaN values are excluded >>> expanding_mean(x, skipna=True) array([1., 1.5, 1.5, 2.33..., 3.0]) ``` ### `expanding_std` ```python theme={null} expanding_std(x, skipna=False) ``` Compute the expanding\_std of the input array. **Parameters:** | Name | Type | Description | Default | | -------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `skipna` | bool | If True, exclude NaN values from calculations. When False (default), any NaN value causes the result to be NaN, maintaining backwards compatibility. When True, NaN values are ignored (matching pandas default behavior). | *required* | **Returns:** | Type | Description | | ---------------------------------------------- | ----------- | | np.ndarray: Array with the expanding statistic | | **Examples:** ```pycon theme={null} >>> import numpy as np >>> x = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) >>> # Default behavior: NaN propagates >>> expanding_std(x) array([1., 1.5, nan, nan, nan]) >>> # With skipna=True: NaN values are excluded >>> expanding_std(x, skipna=True) array([1., 1.5, 1.5, 2.33..., 3.0]) ``` ### `expanding_min` ```python theme={null} expanding_min(x, skipna=False) ``` Compute the expanding\_min of the input array. **Parameters:** | Name | Type | Description | Default | | -------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `skipna` | bool | If True, exclude NaN values from calculations. When False (default), any NaN value causes the result to be NaN, maintaining backwards compatibility. When True, NaN values are ignored (matching pandas default behavior). | *required* | **Returns:** | Type | Description | | ---------------------------------------------- | ----------- | | np.ndarray: Array with the expanding statistic | | **Examples:** ```pycon theme={null} >>> import numpy as np >>> x = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) >>> # Default behavior: NaN propagates >>> expanding_min(x) array([1., 1.5, nan, nan, nan]) >>> # With skipna=True: NaN values are excluded >>> expanding_min(x, skipna=True) array([1., 1.5, 1.5, 2.33..., 3.0]) ``` ### `expanding_max` ```python theme={null} expanding_max(x, skipna=False) ``` Compute the expanding\_max of the input array. **Parameters:** | Name | Type | Description | Default | | -------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `skipna` | bool | If True, exclude NaN values from calculations. When False (default), any NaN value causes the result to be NaN, maintaining backwards compatibility. When True, NaN values are ignored (matching pandas default behavior). | *required* | **Returns:** | Type | Description | | ---------------------------------------------- | ----------- | | np.ndarray: Array with the expanding statistic | | **Examples:** ```pycon theme={null} >>> import numpy as np >>> x = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) >>> # Default behavior: NaN propagates >>> expanding_max(x) array([1., 1.5, nan, nan, nan]) >>> # With skipna=True: NaN values are excluded >>> expanding_max(x, skipna=True) array([1., 1.5, 1.5, 2.33..., 3.0]) ``` ### `expanding_quantile` ```python theme={null} expanding_quantile(x, p, skipna=False) ``` Compute the expanding\_quantile of the input array. **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `x` | [ndarray](#numpy.ndarray) | Input array. | *required* | | `p` | [float](#float) | Quantile to compute. | *required* | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), any NaN value causes the result to be NaN, maintaining backwards compatibility. When True, NaN values are ignored (matching pandas default behavior). | False | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the expanding statistic | **Examples:** ```pycon theme={null} >>> import numpy as np >>> x = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) >>> # Default behavior: NaN propagates >>> expanding_quantile(x, 0.5) array([1., 1.5, nan, nan, nan]) >>> # With skipna=True: NaN values are excluded >>> expanding_quantile(x, 0.5, skipna=True) array([1., 1.5, 1.5, 2., 2.5]) ``` # Exponentially weighted Source: https://nixtlaverse.nixtla.io/coreforecast/exponentially_weighted Compute exponentially weighted mean ## ### `exponentially_weighted_mean` ```python theme={null} exponentially_weighted_mean(x, alpha, skipna=False) ``` Compute the exponentially weighted mean of the input array. **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `x` | [ndarray](#numpy.ndarray) | Input array. | *required* | | `alpha` | [float](#float) | Weight parameter. | *required* | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations using forward-fill behavior. When False (default), any NaN value causes the result to be NaN, maintaining backwards compatibility. When True, the last valid value is forward-filled through NaN values (matching pandas default behavior). | False | **Returns:** | Type | Description | | -------------------------------------- | ------------------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the exponentially weighted mean. | **Examples:** ```pycon theme={null} >>> import numpy as np >>> x = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) >>> # Default behavior: NaN propagates >>> exponentially_weighted_mean(x, alpha=0.5) array([1., 1.5, nan, nan, nan]) >>> # With skipna=True: forward-fill through NaN >>> exponentially_weighted_mean(x, alpha=0.5, skipna=True) array([1., 1.5, 1.5, 2.75, 3.875]) ``` # Grouped Array Source: https://nixtlaverse.nixtla.io/coreforecast/grouped_array Group arrays by a categorical variable # coreforecast Source: https://nixtlaverse.nixtla.io/coreforecast/index Fast implementations of common forecasting routines ## Motivation At Nixtla we have implemented several libraries to deal with time series data. We often have to apply some transformation over all of the series, which can prove time consuming even for simple operations like performing some kind of scaling. We've used [numba](https://numba.pydata.org/) to speed up our expensive computations, however that comes with other issues such as cold starts and more dependencies (LLVM). That's why we developed this library, which implements several operators in C++ to transform time series data (or other kind of data that can be thought of as independent groups), with the possibility to use multithreading to get the best performance possible. You probably won't need to use this library directly but rather use one of our higher level libraries like [mlforecast](https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/lag_transforms_guide.html#built-in-transformations-experimental), which will use this library under the hood. If you're interested on using this library directly (only depends on numpy) you should continue reading. ## Installation ### PyPI ```python theme={null} pip install coreforecast ``` ### conda-forge ```python theme={null} conda install -c conda-forge coreforecast ``` ## Minimal example The base data structure is the "grouped array" which holds two numpy 1d arrays: * **data**: values of the series. * **indptr**: series boundaries such that `data[indptr[i] : indptr[i + 1]]` returns the `i-th` series. For example, if you have two series of sizes 5 and 10 the indptr would be \[0, 5, 15]. ```python theme={null} import numpy as np from coreforecast.grouped_array import GroupedArray data = np.arange(10) indptr = np.array([0, 3, 10]) ga = GroupedArray(data, indptr) ``` Once you have this structure you can run any of the provided transformations, for example: ```python theme={null} from coreforecast.lag_transforms import ExpandingMean from coreforecast.scalers import LocalStandardScaler exp_mean = ExpandingMean(lag=1).transform(ga) scaler = LocalStandardScaler().fit(ga) standardized = scaler.transform(ga) ``` ## Single-array functions We've also implemented some functions that work on single arrays, you can refer to the following pages: * [differences](https://nixtlaverse.nixtla.io/coreforecast/differences) * [scalers](https://nixtlaverse.nixtla.io/coreforecast/scalers) * [seasonal](https://nixtlaverse.nixtla.io/coreforecast/seasonal) * [rolling](https://nixtlaverse.nixtla.io/coreforecast/rolling) * [expanding](https://nixtlaverse.nixtla.io/coreforecast/expanding) * [exponentially weighted](https://nixtlaverse.nixtla.io/coreforecast/exponentially_weighted) # Lag transformations | CoreForecast Source: https://nixtlaverse.nixtla.io/coreforecast/lag_transforms Compute lag transforms ## Overview Lag transforms allow you to compute lagged features and rolling statistics over grouped time series data. All transforms work with the `GroupedArray` structure and provide both `transform()` and `update()` methods for batch processing and incremental updates. ## Basic Example ```python theme={null} import numpy as np from coreforecast.grouped_array import GroupedArray from coreforecast.lag_transforms import Lag, RollingMean # Create sample data: two time series data = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 10.0, 20.0, 30.0]) indptr = np.array([0, 5, 8]) # First series: 5 elements, second: 3 elements ga = GroupedArray(data, indptr) # Simple lag lag2 = Lag(lag=2) lagged = lag2.transform(ga) # Rolling mean with lag rolling_mean = RollingMean(lag=1, window_size=3) rolling = rolling_mean.transform(ga) ``` ## Rolling Window Examples Rolling window operations compute statistics over a sliding window of observations. ```python theme={null} import numpy as np from coreforecast.grouped_array import GroupedArray from coreforecast.lag_transforms import ( RollingMean, RollingStd, RollingMin, RollingMax, RollingQuantile ) # Sample time series data data = np.array([10.0, 12.0, 15.0, 14.0, 18.0, 20.0, 22.0, 19.0]) indptr = np.array([0, 8]) ga = GroupedArray(data, indptr) # Rolling mean with window size 3, lag 1 rolling_mean = RollingMean(lag=1, window_size=3) mean_result = rolling_mean.transform(ga) # Computes mean of last 3 values with lag 1 # Rolling standard deviation rolling_std = RollingStd(lag=1, window_size=3, min_samples=2) std_result = rolling_std.transform(ga) # Rolling minimum and maximum rolling_min = RollingMin(lag=1, window_size=3) rolling_max = RollingMax(lag=1, window_size=3) min_result = rolling_min.transform(ga) max_result = rolling_max.transform(ga) # Rolling median (50th percentile) rolling_median = RollingQuantile(lag=1, p=0.5, window_size=3) median_result = rolling_median.transform(ga) ``` ## Seasonal Rolling Examples Seasonal rolling operations compute statistics over windows that respect seasonality patterns. ```python theme={null} import numpy as np from coreforecast.grouped_array import GroupedArray from coreforecast.lag_transforms import ( SeasonalRollingMean, SeasonalRollingStd, SeasonalRollingMin, SeasonalRollingMax, SeasonalRollingQuantile ) # Daily data with weekly seasonality (14 days) data = np.array([10.0, 15.0, 12.0, 18.0, 20.0, 22.0, 25.0, 11.0, 16.0, 13.0, 19.0, 21.0, 23.0, 26.0]) indptr = np.array([0, 14]) ga = GroupedArray(data, indptr) # Seasonal rolling mean with weekly pattern seasonal_mean = SeasonalRollingMean( lag=1, season_length=7, # Weekly seasonality window_size=2 # Use last 2 seasonal observations ) seasonal_result = seasonal_mean.transform(ga) # Computes mean using observations from the same day of week # Seasonal rolling std seasonal_std = SeasonalRollingStd(lag=1, season_length=7, window_size=2) seasonal_std_result = seasonal_std.transform(ga) # Seasonal rolling min/max seasonal_min = SeasonalRollingMin(lag=1, season_length=7, window_size=2) seasonal_max = SeasonalRollingMax(lag=1, season_length=7, window_size=2) # Seasonal rolling quantile seasonal_q90 = SeasonalRollingQuantile( lag=1, p=0.9, season_length=7, window_size=2 ) ``` ## Expanding Window Examples Expanding windows compute cumulative statistics from the start of each series. ```python theme={null} import numpy as np from coreforecast.grouped_array import GroupedArray from coreforecast.lag_transforms import ( ExpandingMean, ExpandingStd, ExpandingMin, ExpandingMax, ExpandingQuantile ) # Sample data: two time series data = np.array([5.0, 10.0, 8.0, 12.0, 15.0, 20.0, 25.0, 30.0]) indptr = np.array([0, 5, 8]) ga = GroupedArray(data, indptr) # Expanding mean (cumulative average) exp_mean = ExpandingMean(lag=1) cumulative_avg = exp_mean.transform(ga) # Each value is the mean of all previous observations # Expanding standard deviation exp_std = ExpandingStd(lag=1) cumulative_std = exp_std.transform(ga) # Expanding min and max exp_min = ExpandingMin(lag=1) exp_max = ExpandingMax(lag=1) running_min = exp_min.transform(ga) running_max = exp_max.transform(ga) # Expanding quantile exp_median = ExpandingQuantile(lag=1, p=0.5) running_median = exp_median.transform(ga) ``` ## Exponentially Weighted Mean Example The exponentially weighted mean gives more weight to recent observations. ```python theme={null} import numpy as np from coreforecast.grouped_array import GroupedArray from coreforecast.lag_transforms import ExponentiallyWeightedMean # Sample data data = np.array([10.0, 12.0, 15.0, 14.0, 18.0, 20.0]) indptr = np.array([0, 6]) ga = GroupedArray(data, indptr) # Exponentially weighted mean with alpha=0.3 # Higher alpha = more weight to recent values ewm = ExponentiallyWeightedMean(lag=1, alpha=0.3) smoothed = ewm.transform(ga) ``` ## Update Method for Incremental Processing All transforms provide an `update()` method for efficient incremental computation when new data arrives. ```python theme={null} import numpy as np from coreforecast.grouped_array import GroupedArray from coreforecast.lag_transforms import ExpandingMean # Initial data data = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) indptr = np.array([0, 5]) ga = GroupedArray(data, indptr) # Transform to initialize statistics exp_mean = ExpandingMean(lag=1) result = exp_mean.transform(ga) # New observation arrives new_data = np.array([6.0]) new_indptr = np.array([0, 1]) new_ga = GroupedArray(new_data, new_indptr) # Update statistics incrementally (much faster than re-transforming) updated_value = exp_mean.update(new_ga) # Returns the updated expanding mean for the new observation ``` ## Available lag transformations ### `Lag` ```python theme={null} Lag(lag) ``` Bases: [\_BaseLagTransform](#coreforecast.lag_transforms._BaseLagTransform) Simple lag operator **Parameters:** | Name | Type | Description | Default | | ----- | ------------------------ | --------------------------- | ---------- | | `lag` | [int](#int) | Number of periods to offset | *required* | ### `RollingMean` Bases: [\_RollingBase](#coreforecast.lag_transforms._RollingBase) Rolling Mean **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation. | *required* | | `window_size` | [int](#int) | Length of the rolling window. | *required* | | `min_samples` | [int](#int) | Minimum number of samples required to compute the statistic. If None, defaults to window\_size. | None | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `RollingStd` Bases: [\_RollingBase](#coreforecast.lag_transforms._RollingBase) Rolling Standard Deviation **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation. | *required* | | `window_size` | [int](#int) | Length of the rolling window. | *required* | | `min_samples` | [int](#int) | Minimum number of samples required to compute the statistic. If None, defaults to window\_size. | None | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `RollingMin` Bases: [\_RollingBase](#coreforecast.lag_transforms._RollingBase) Rolling Minimum **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation. | *required* | | `window_size` | [int](#int) | Length of the rolling window. | *required* | | `min_samples` | [int](#int) | Minimum number of samples required to compute the statistic. If None, defaults to window\_size. | None | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `RollingMax` Bases: [\_RollingBase](#coreforecast.lag_transforms._RollingBase) Rolling Maximum **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation. | *required* | | `window_size` | [int](#int) | Length of the rolling window. | *required* | | `min_samples` | [int](#int) | Minimum number of samples required to compute the statistic. If None, defaults to window\_size. | None | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `RollingQuantile` ```python theme={null} RollingQuantile(lag, p, window_size, min_samples=None, skipna=False) ``` Bases: [\_RollingBase](#coreforecast.lag_transforms._RollingBase) Rolling quantile **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `p` | [float](#float) | Quantile to compute | *required* | | `window_size` | [int](#int) | Length of the rolling window | *required* | | `min_samples` | [int](#int) | Minimum number of samples required to compute the statistic. If None, defaults to window\_size. | None | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `SeasonalRollingMean` Bases: [\_SeasonalRollingBase](#coreforecast.lag_transforms._SeasonalRollingBase) Seasonal rolling Mean **Parameters:** | Name | Type | Description | Default | | --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `season_length` | [int](#int) | Length of the seasonal period, e.g. 7 for weekly data | *required* | | `window_size` | [int](#int) | Length of the rolling window | *required* | | `min_samples` | [int](#int) | Minimum number of samples required to compute the statistic. If None, defaults to window\_size. | None | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `SeasonalRollingStd` Bases: [\_SeasonalRollingBase](#coreforecast.lag_transforms._SeasonalRollingBase) Seasonal rolling Standard Deviation **Parameters:** | Name | Type | Description | Default | | --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `season_length` | [int](#int) | Length of the seasonal period, e.g. 7 for weekly data | *required* | | `window_size` | [int](#int) | Length of the rolling window | *required* | | `min_samples` | [int](#int) | Minimum number of samples required to compute the statistic. If None, defaults to window\_size. | None | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `SeasonalRollingMin` Bases: [\_SeasonalRollingBase](#coreforecast.lag_transforms._SeasonalRollingBase) Seasonal rolling Minimum **Parameters:** | Name | Type | Description | Default | | --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `season_length` | [int](#int) | Length of the seasonal period, e.g. 7 for weekly data | *required* | | `window_size` | [int](#int) | Length of the rolling window | *required* | | `min_samples` | [int](#int) | Minimum number of samples required to compute the statistic. If None, defaults to window\_size. | None | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `SeasonalRollingMax` Bases: [\_SeasonalRollingBase](#coreforecast.lag_transforms._SeasonalRollingBase) Seasonal rolling Maximum **Parameters:** | Name | Type | Description | Default | | --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `season_length` | [int](#int) | Length of the seasonal period, e.g. 7 for weekly data | *required* | | `window_size` | [int](#int) | Length of the rolling window | *required* | | `min_samples` | [int](#int) | Minimum number of samples required to compute the statistic. If None, defaults to window\_size. | None | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `SeasonalRollingQuantile` ```python theme={null} SeasonalRollingQuantile(lag, p, season_length, window_size, min_samples=None, skipna=False) ``` Bases: [\_SeasonalRollingBase](#coreforecast.lag_transforms._SeasonalRollingBase) Seasonal rolling statistic **Parameters:** | Name | Type | Description | Default | | --------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `p` | [float](#float) | Quantile to compute | *required* | | `season_length` | [int](#int) | Length of the seasonal period, e.g. 7 for weekly data | *required* | | `window_size` | [int](#int) | Length of the rolling window | *required* | | `min_samples` | [int](#int) | Minimum number of samples required to compute the statistic. If None, defaults to window\_size. | None | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `ExpandingMean` Bases: [\_ExpandingBase](#coreforecast.lag_transforms._ExpandingBase) Expanding Mean **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `ExpandingStd` Bases: [\_ExpandingBase](#coreforecast.lag_transforms._ExpandingBase) Expanding Standard Deviation **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `ExpandingMin` Bases: [\_ExpandingComp](#coreforecast.lag_transforms._ExpandingComp) Expanding Minimum **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `ExpandingMax` Bases: [\_ExpandingComp](#coreforecast.lag_transforms._ExpandingComp) Expanding Maximum **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `ExpandingQuantile` ```python theme={null} ExpandingQuantile(lag, p, skipna=False) ``` Bases: [\_BaseLagTransform](#coreforecast.lag_transforms._BaseLagTransform) Expanding quantile **Parameters:** | Name | Type | Description | Default | | -------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `p` | [float](#float) | Quantile to compute | *required* | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations. When False (default), NaN values propagate through the calculation. | False | ### `ExponentiallyWeightedMean` ```python theme={null} ExponentiallyWeightedMean(lag, alpha, skipna=False) ``` Bases: [\_BaseLagTransform](#coreforecast.lag_transforms._BaseLagTransform) Exponentially weighted mean **Parameters:** | Name | Type | Description | Default | | -------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `lag` | [int](#int) | Number of periods to offset by before applying the transformation | *required* | | `alpha` | [float](#float) | Smoothing factor | *required* | | `skipna` | [bool](#bool) | If True, exclude NaN values from calculations using forward-fill behavior. When False (default), NaN values propagate through the calculation. | False | # Rolling Source: https://nixtlaverse.nixtla.io/coreforecast/rolling Compute rolling mean, std, min, max, and quantile ## ### `rolling_mean` ```python theme={null} rolling_mean(x, window_size, min_samples=None, skipna=False) ``` Compute the rolling\_mean of the input array. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `window_size` | int | The size of the rolling window. | *required* | | `min_samples` | int | The minimum number of samples required to compute the statistic. If None, it is set to `window_size`. | *required* | | `skipna` | bool | Exclude NaN values from calculations. When False (default), any NaN value in the window causes the result to be NaN. When True, NaN values are ignored and statistics are computed on remaining valid values in the window. Defaults to False for backwards compatibility. | *required* | **Returns:** | Type | Description | | -------------------------------------------- | ----------- | | np.ndarray: Array with the rolling statistic | | ### `rolling_std` ```python theme={null} rolling_std(x, window_size, min_samples=None, skipna=False) ``` Compute the rolling\_std of the input array. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `window_size` | int | The size of the rolling window. | *required* | | `min_samples` | int | The minimum number of samples required to compute the statistic. If None, it is set to `window_size`. | *required* | | `skipna` | bool | Exclude NaN values from calculations. When False (default), any NaN value in the window causes the result to be NaN. When True, NaN values are ignored and statistics are computed on remaining valid values in the window. Defaults to False for backwards compatibility. | *required* | **Returns:** | Type | Description | | -------------------------------------------- | ----------- | | np.ndarray: Array with the rolling statistic | | ### `rolling_min` ```python theme={null} rolling_min(x, window_size, min_samples=None, skipna=False) ``` Compute the rolling\_min of the input array. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `window_size` | int | The size of the rolling window. | *required* | | `min_samples` | int | The minimum number of samples required to compute the statistic. If None, it is set to `window_size`. | *required* | | `skipna` | bool | Exclude NaN values from calculations. When False (default), any NaN value in the window causes the result to be NaN. When True, NaN values are ignored and statistics are computed on remaining valid values in the window. Defaults to False for backwards compatibility. | *required* | **Returns:** | Type | Description | | -------------------------------------------- | ----------- | | np.ndarray: Array with the rolling statistic | | ### `rolling_max` ```python theme={null} rolling_max(x, window_size, min_samples=None, skipna=False) ``` Compute the rolling\_max of the input array. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `window_size` | int | The size of the rolling window. | *required* | | `min_samples` | int | The minimum number of samples required to compute the statistic. If None, it is set to `window_size`. | *required* | | `skipna` | bool | Exclude NaN values from calculations. When False (default), any NaN value in the window causes the result to be NaN. When True, NaN values are ignored and statistics are computed on remaining valid values in the window. Defaults to False for backwards compatibility. | *required* | **Returns:** | Type | Description | | -------------------------------------------- | ----------- | | np.ndarray: Array with the rolling statistic | | ### `rolling_quantile` ```python theme={null} rolling_quantile(x, p, window_size, min_samples=None, skipna=False) ``` Compute the rolling\_quantile of the input array. **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `x` | [ndarray](#numpy.ndarray) | Input array. | *required* | | `p` | [float](#float) | Quantile to compute. | *required* | | `window_size` | [int](#int) | The size of the rolling window. | *required* | | `min_samples` | [int](#int) | The minimum number of samples required to compute the statistic. If None, it is set to `window_size`. | None | | `skipna` | [bool](#bool) | Exclude NaN values from calculations. When False (default), any NaN value in the window causes the result to be NaN. When True, NaN values are ignored and statistics are computed on remaining valid values in the window. Defaults to False for backwards compatibility. | False | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with rolling statistic | ### `seasonal_rolling_mean` ```python theme={null} seasonal_rolling_mean(x, season_length, window_size, min_samples=None, skipna=False) ``` Compute the seasonal\_rolling\_mean of the input array **Parameters:** | Name | Type | Description | Default | | --------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `season_length` | int | The length of the seasonal period. | *required* | | `window_size` | int | The size of the rolling window. | *required* | | `min_samples` | int | The minimum number of samples required to compute the statistic. If None, it is set to `window_size`. | *required* | | `skipna` | bool | Exclude NaN values from calculations. When False (default), any NaN value in the window causes the result to be NaN. When True, NaN values are ignored and statistics are computed on remaining valid values in the window. Defaults to False for backwards compatibility. | *required* | **Returns:** | Type | Description | | ----------------------------------------------------- | ----------- | | np.ndarray: Array with the seasonal rolling statistic | | ### `seasonal_rolling_std` ```python theme={null} seasonal_rolling_std(x, season_length, window_size, min_samples=None, skipna=False) ``` Compute the seasonal\_rolling\_std of the input array **Parameters:** | Name | Type | Description | Default | | --------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `season_length` | int | The length of the seasonal period. | *required* | | `window_size` | int | The size of the rolling window. | *required* | | `min_samples` | int | The minimum number of samples required to compute the statistic. If None, it is set to `window_size`. | *required* | | `skipna` | bool | Exclude NaN values from calculations. When False (default), any NaN value in the window causes the result to be NaN. When True, NaN values are ignored and statistics are computed on remaining valid values in the window. Defaults to False for backwards compatibility. | *required* | **Returns:** | Type | Description | | ----------------------------------------------------- | ----------- | | np.ndarray: Array with the seasonal rolling statistic | | ### `seasonal_rolling_min` ```python theme={null} seasonal_rolling_min(x, season_length, window_size, min_samples=None, skipna=False) ``` Compute the seasonal\_rolling\_min of the input array **Parameters:** | Name | Type | Description | Default | | --------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `season_length` | int | The length of the seasonal period. | *required* | | `window_size` | int | The size of the rolling window. | *required* | | `min_samples` | int | The minimum number of samples required to compute the statistic. If None, it is set to `window_size`. | *required* | | `skipna` | bool | Exclude NaN values from calculations. When False (default), any NaN value in the window causes the result to be NaN. When True, NaN values are ignored and statistics are computed on remaining valid values in the window. Defaults to False for backwards compatibility. | *required* | **Returns:** | Type | Description | | ----------------------------------------------------- | ----------- | | np.ndarray: Array with the seasonal rolling statistic | | ### `seasonal_rolling_max` ```python theme={null} seasonal_rolling_max(x, season_length, window_size, min_samples=None, skipna=False) ``` Compute the seasonal\_rolling\_max of the input array **Parameters:** | Name | Type | Description | Default | | --------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | np.ndarray | Input array. | *required* | | `season_length` | int | The length of the seasonal period. | *required* | | `window_size` | int | The size of the rolling window. | *required* | | `min_samples` | int | The minimum number of samples required to compute the statistic. If None, it is set to `window_size`. | *required* | | `skipna` | bool | Exclude NaN values from calculations. When False (default), any NaN value in the window causes the result to be NaN. When True, NaN values are ignored and statistics are computed on remaining valid values in the window. Defaults to False for backwards compatibility. | *required* | **Returns:** | Type | Description | | ----------------------------------------------------- | ----------- | | np.ndarray: Array with the seasonal rolling statistic | | ### `seasonal_rolling_quantile` ```python theme={null} seasonal_rolling_quantile(x, p, season_length, window_size, min_samples=None, skipna=False) ``` Compute the seasonal\_rolling\_quantile of the input array. **Parameters:** | Name | Type | Description | Default | | --------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `x` | [ndarray](#numpy.ndarray) | Input array. | *required* | | `p` | [float](#float) | Quantile to compute. | *required* | | `season_length` | [int](#int) | The length of the seasonal period. | *required* | | `window_size` | [int](#int) | The size of the rolling window. | *required* | | `min_samples` | [int](#int) | The minimum number of samples required to compute the statistic. If None, it is set to `window_size`. | None | | `skipna` | [bool](#bool) | Exclude NaN values from calculations. When False (default), any NaN value in the window causes the result to be NaN. When True, NaN values are ignored and statistics are computed on remaining valid values in the window. Defaults to False for backwards compatibility. | False | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with rolling statistic | # Scalers Source: https://nixtlaverse.nixtla.io/coreforecast/scalers Scale arrays ## ### `boxcox_lambda` ```python theme={null} boxcox_lambda(x, method, season_length=None, lower=-0.9, upper=2.0) ``` Find optimum lambda for the Box-Cox transformation **Parameters:** | Name | Type | Description | Default | | --------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `x` | [ndarray](#numpy.ndarray) | Array with data to transform. | *required* | | `method` | [str](#str) | Method to use. Valid options are 'guerrero' and 'loglik'. 'guerrero' minimizes the coefficient of variation for subseries of `x` and supports negative values. 'loglik' maximizes the log-likelihood function. | *required* | | `season_length` | [int](#int) | Length of the seasonal period. Only required if method='guerrero'. | None | | `lower` | [float](#float) | Lower bound for the lambda. | -0.9 | | `upper` | [float](#float) | Upper bound for the lambda. | 2.0 | **Returns:** | Name | Type | Description | | ------- | ---------------------------- | --------------- | | `float` | [float](#float) | Optimum lambda. | ### `boxcox` ```python theme={null} boxcox(x, lmbda) ``` Apply the Box-Cox transformation **Parameters:** | Name | Type | Description | Default | | ------- | -------------------------------------- | ----------------------------- | ---------- | | `x` | [ndarray](#numpy.ndarray) | Array with data to transform. | *required* | | `lmbda` | [float](#float) | Lambda value to use. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the transformed data. | ### `inv_boxcox` ```python theme={null} inv_boxcox(x, lmbda) ``` Invert the Box-Cox transformation **Parameters:** | Name | Type | Description | Default | | ------- | -------------------------------------- | ----------------------------- | ---------- | | `x` | [ndarray](#numpy.ndarray) | Array with data to transform. | *required* | | `lmbda` | [float](#float) | Lambda value to use. | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the inverted transformation. | ### `LocalMinMaxScaler` ```python theme={null} LocalMinMaxScaler(skipna=False) ``` Bases: [\_BaseLocalScaler](#coreforecast.scalers._BaseLocalScaler) Scale each group to the \[0, 1] interval **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `skipna` | [bool](#bool) | If True, exclude NaN values when computing statistics. When False (default), NaN values are included and may result in NaN statistics. | False | #### `LocalMinMaxScaler.fit` ```python theme={null} fit(ga) ``` Compute the statistics for each group. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------------------------------ | ------------------------- | | `self` | [\_BaseLocalScaler](#coreforecast.scalers._BaseLocalScaler) | The fitted scaler object. | #### `LocalMinMaxScaler.fit_transform` ```python theme={null} fit_transform(ga) ``` "Compute the statistics for each group and apply the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the transformed data. | #### `LocalMinMaxScaler.inverse_transform` ```python theme={null} inverse_transform(ga) ``` Use the computed statistics to invert the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the inverted transformation. | #### `LocalMinMaxScaler.stack` ```python theme={null} stack(scalers) ``` #### `LocalMinMaxScaler.take` ```python theme={null} take(idxs) ``` #### `LocalMinMaxScaler.transform` ```python theme={null} transform(ga) ``` Use the computed statistics to apply the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the transformed data. | ### `LocalStandardScaler` ```python theme={null} LocalStandardScaler(skipna=False) ``` Bases: [\_BaseLocalScaler](#coreforecast.scalers._BaseLocalScaler) Scale each group to have zero mean and unit variance **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `skipna` | [bool](#bool) | If True, exclude NaN values when computing statistics. When False (default), NaN values are included and may result in NaN statistics. | False | #### `LocalStandardScaler.fit` ```python theme={null} fit(ga) ``` Compute the statistics for each group. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------------------------------ | ------------------------- | | `self` | [\_BaseLocalScaler](#coreforecast.scalers._BaseLocalScaler) | The fitted scaler object. | #### `LocalStandardScaler.fit_transform` ```python theme={null} fit_transform(ga) ``` "Compute the statistics for each group and apply the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the transformed data. | #### `LocalStandardScaler.inverse_transform` ```python theme={null} inverse_transform(ga) ``` Use the computed statistics to invert the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the inverted transformation. | #### `LocalStandardScaler.stack` ```python theme={null} stack(scalers) ``` #### `LocalStandardScaler.take` ```python theme={null} take(idxs) ``` #### `LocalStandardScaler.transform` ```python theme={null} transform(ga) ``` Use the computed statistics to apply the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the transformed data. | ### `LocalRobustScaler` ```python theme={null} LocalRobustScaler(scale, skipna=False) ``` Bases: [\_BaseLocalScaler](#coreforecast.scalers._BaseLocalScaler) Scale each group using robust statistics **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | | `scale` | [str](#str) | Type of robust scaling to use. Valid options are 'iqr' and 'mad'. If 'iqr' will use the inter quartile range as the scale. If 'mad' will use median absolute deviation as the scale. | *required* | | `skipna` | [bool](#bool) | If True, exclude NaN values when computing statistics. When False (default), NaN values are included and may result in NaN statistics. | False | #### `LocalRobustScaler.fit` ```python theme={null} fit(ga) ``` Compute the statistics for each group. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------------------------------ | ------------------------- | | `self` | [\_BaseLocalScaler](#coreforecast.scalers._BaseLocalScaler) | The fitted scaler object. | #### `LocalRobustScaler.fit_transform` ```python theme={null} fit_transform(ga) ``` "Compute the statistics for each group and apply the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the transformed data. | #### `LocalRobustScaler.inverse_transform` ```python theme={null} inverse_transform(ga) ``` Use the computed statistics to invert the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the inverted transformation. | #### `LocalRobustScaler.stack` ```python theme={null} stack(scalers) ``` #### `LocalRobustScaler.take` ```python theme={null} take(idxs) ``` #### `LocalRobustScaler.transform` ```python theme={null} transform(ga) ``` Use the computed statistics to apply the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the transformed data. | ### `LocalBoxCoxScaler` ```python theme={null} LocalBoxCoxScaler(method, season_length=None, lower=-0.9, upper=2.0) ``` Bases: [\_BaseLocalScaler](#coreforecast.scalers._BaseLocalScaler) Find the optimum lambda for the Box-Cox transformation by group and apply it **Parameters:** | Name | Type | Description | Default | | --------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `season_length` | [int](#int) | Length of the seasonal period. Only required if method='guerrero'. | None | | `lower` | [float](#float) | Lower bound for the lambda. | -0.9 | | `upper` | [float](#float) | Upper bound for the lambda. | 2.0 | | `method` | [str](#str) | Method to use. Valid options are 'guerrero' and 'loglik'. 'guerrero' minimizes the coefficient of variation for subseries of `x` and supports negative values. 'loglik' maximizes the log-likelihood function. | *required* | #### `LocalBoxCoxScaler.fit` ```python theme={null} fit(ga) ``` Compute the statistics for each group. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------------------------------ | ------------------------- | | `self` | [\_BaseLocalScaler](#coreforecast.scalers._BaseLocalScaler) | The fitted scaler object. | #### `LocalBoxCoxScaler.fit_transform` ```python theme={null} fit_transform(ga) ``` "Compute the statistics for each group and apply the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the transformed data. | #### `LocalBoxCoxScaler.inverse_transform` ```python theme={null} inverse_transform(ga) ``` Use the computed lambdas to invert the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the inverted transformation. | #### `LocalBoxCoxScaler.stack` ```python theme={null} stack(scalers) ``` #### `LocalBoxCoxScaler.take` ```python theme={null} take(idxs) ``` #### `LocalBoxCoxScaler.transform` ```python theme={null} transform(ga) ``` Use the computed lambdas to apply the transformation. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the transformed data. | ### `Difference` ```python theme={null} Difference(d) ``` Subtract a lag to each group **Parameters:** | Name | Type | Description | Default | | ---- | ------------------------ | ---------------- | ---------- | | `d` | [int](#int) | Lag to subtract. | *required* | #### `Difference.fit_transform` ```python theme={null} fit_transform(ga) ``` Apply the transformation **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the transformed data. | #### `Difference.inverse_transform` ```python theme={null} inverse_transform(ga) ``` Invert the transformation **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------------------------------------- | ------------------------ | ---------- | | `ga` | [GroupedArray](#coreforecast._lib.grouped_array.GroupedArray) | Array with grouped data. | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------------------------------- | | [ndarray](#numpy.ndarray) | np.ndarray: Array with the inverted transformation. | #### `Difference.stack` ```python theme={null} stack(scalers) ``` #### `Difference.take` ```python theme={null} take(idxs) ``` # Seasonal Source: https://nixtlaverse.nixtla.io/coreforecast/seasonal Find the seasonal period ## ### `find_season_length` ```python theme={null} find_season_length(x, max_season_length) ``` Find the length of the seasonal period of the time series. Returns 0 if no seasonality is found. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------- | --------------------------- | ---------- | | `x` | [ndarray](#numpy.ndarray) | Array with the time series. | *required* | **Returns:** | Name | Type | Description | | ----- | ------------------------ | -------------- | | `int` | [int](#int) | Season period. | # Utils Source: https://nixtlaverse.nixtla.io/coreforecast/utils # module `coreforecast.utils` # Favorita Source: https://nixtlaverse.nixtla.io/datasetsforecast/favorita.html Favorita dataset ## ### `FavoritaData` Favorita Data. The processed Favorita dataset of grocery contains item sales daily history with additional information on promotions, items, stores, and holidays, containing 371,312 series from January 2013 to August 2017, with a geographic hierarchy of states, cities, and stores. This wrangling matches that of the DPMN paper. References: * [Kin G. Olivares, O. Nganba Meetei, Ruijun Ma, Rohan Reddy, Mengfei Cao, Lee Dicker (2022). "Probabilistic Hierarchical Forecasting with Deep Poisson Mixtures". International Journal Forecasting, special issue.](https://doi.org/10.1016/j.ijforecast.2023.04.007) #### `FavoritaData.load` ```python theme={null} load(directory, group, cache=True, verbose=False) ``` Load Favorita forecasting benchmark dataset. In contrast with other hierarchical datasets, this dataset contains a geographic hierarchy for each individual grocery item series, identified with 'item\_id' column. The geographic hierarchy is captured by the 'hier\_id' column. For this reason minor wrangling is needed to adapt it for use with HierarchicalForecast, and StatsForecast libraries. **Parameters:** | Name | Type | Description | Default | | ----------- | -------------------------- | ----------------------------------------------------------------------- | ------------------ | | `directory` | [str](#str) | Directory where data will be downloaded and saved. | *required* | | `group` | [str](#str) | Dataset group name in 'Favorita200', 'Favorita500', 'FavoritaComplete'. | *required* | | `cache` | [bool](#bool) | If True saves and loads. Defaults to True. | True | | `verbose` | [bool](#bool) | Whether or not print partial outputs. Defaults to False. | False | **Returns:** | Name | Type | Description | | ------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `tuple` | | A tuple containing: - Y\_df (pd.DataFrame): Target base time series with columns \['item\_id', 'hier\_id', 'ds', 'y']. - S\_df (pd.DataFrame): Hierarchical constraints dataframe of size (base, bottom). - tags (dict): Dictionary with hierarchical level information. | Example: ```python theme={null} # Qualitative evaluation of hierarchical data from datasetsforecast.favorita import FavoritaData from hierarchicalforecast.utils import HierarchicalPlot group = 'Favorita200' # 'Favorita500', 'FavoritaComplete' directory = './data/favorita' Y_df, S_df, tags = FavoritaData.load(directory=directory, group=group) Y_item_df = Y_df[Y_df.item_id==1916577] # 112830, 1501570, 1916577 Y_item_df = Y_item_df.rename(columns={'hier_id': 'unique_id'}) Y_item_df = Y_item_df.set_index('unique_id') del Y_item_df['item_id'] hplots = HierarchicalPlot(S=S_df, tags=tags) hplots.plot_hierarchically_linked_series( Y_df=Y_item_df, bottom_series='store_[40]', ) ``` #### `FavoritaData.load_preprocessed` ```python theme={null} load_preprocessed(directory, group, cache=True, verbose=False) ``` Load Favorita group datasets. For the exploration of more complex models, we make available the entire information including data at the bottom level of the items sold in Favorita stores, in addition to the aggregate/national level information for the items. **Parameters:** | Name | Type | Description | Default | | ----------- | -------------------------- | ----------------------------------------------------------------------- | ------------------ | | `directory` | [str](#str) | Directory where data will be downloaded and saved. | *required* | | `group` | [str](#str) | Dataset group name in 'Favorita200', 'Favorita500', 'FavoritaComplete'. | *required* | | `cache` | [bool](#bool) | If True saves and loads. Defaults to True. | True | | `verbose` | [bool](#bool) | Whether or not print partial outputs. Defaults to False. | False | **Returns:** | Name | Type | Description | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tuple` | [Tuple](#typing.Tuple)\[[DataFrame](#pandas.DataFrame), [DataFrame](#pandas.DataFrame), [DataFrame](#pandas.DataFrame), [DataFrame](#pandas.DataFrame)] | A tuple containing: - static\_bottom (pd.DataFrame): Static variables of bottom level series. - static\_agg (pd.DataFrame): Static variables of aggregate level series. - temporal\_bottom (pd.DataFrame): Temporal variables of bottom level series. - temporal\_agg (pd.DataFrame): Temporal variables of aggregate level series. | #### Example ```python theme={null} # Qualitative evaluation of hierarchical data from datasetsforecast.favorita import FavoritaData from hierarchicalforecast.utils import HierarchicalPlot group = 'Favorita200' # 'Favorita500', 'FavoritaComplete' directory = './data/favorita' Y_df, S_df, tags = FavoritaData.load(directory=directory, group=group) Y_item_df = Y_df[Y_df.item_id==1916577] # 112830, 1501570, 1916577 Y_item_df = Y_item_df.rename(columns={'hier_id': 'unique_id'}) Y_item_df = Y_item_df.set_index('unique_id') del Y_item_df['item_id'] hplots = HierarchicalPlot(S=S_df, tags=tags) hplots.plot_hierarchically_linked_series( Y_df=Y_item_df, bottom_series='store_[40]', ) ``` ## Auxiliary Functions This auxiliary functions are used to efficiently create and wrangle Favorita’s series. ## Numpy Wrangling ### `numpy_balance` ```python theme={null} numpy_balance(*arrs) ``` Fast NumPy implementation of 'balance' operation. Useful to create a balanced panel dataset, ie a dataset with all the interactions of 'unique\_id' and 'ds'. **Parameters:** | Name | Type | Description | Default | | ------- | ---- | ------------- | --------------- | | `*arrs` | | NumPy arrays. | () | **Returns:** | Type | Description | | -------------------------------------- | --------------------------------------- | | [ndarray](#numpy.ndarray) | NumPy array with balanced combinations. | ### `numpy_ffill` ```python theme={null} numpy_ffill(arr) ``` Fast NumPy implementation of `ffill` that fills missing values. Fills missing values in an array by propagating the last non-missing value forward. For example, if the array has the following values: ``` 0 1 2 3 1 2 NaN 4 ``` The `ffill` method would fill the missing values as follows: ``` 0 1 2 3 1 2 2 4 ``` **Parameters:** | Name | Type | Description | Default | | ----- | -------------------------------------- | ------------ | ---------- | | `arr` | [ndarray](#numpy.ndarray) | NumPy array. | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------------------- | | [ndarray](#numpy.ndarray) | NumPy array with forward-filled values. | ### `numpy_bfill` ```python theme={null} numpy_bfill(arr) ``` Fast NumPy implementation of `bfill` that fills missing values. Fills missing values in an array by propagating the last non-missing value backwards. For example, if the array has the following values: ``` 0 1 2 3 1 2 NaN 4 ``` The `bfill` method would fill the missing values as follows: ``` 0 1 2 3 1 2 4 4 ``` **Parameters:** | Name | Type | Description | Default | | ----- | -------------------------------------- | ------------ | ---------- | | `arr` | [ndarray](#numpy.ndarray) | NumPy array. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------------------- | | [ndarray](#numpy.ndarray) | NumPy array with backward-filled values. | ### `one_hot_encoding` ```python theme={null} one_hot_encoding(df, index_col) ``` Encodes dataFrame's categorical variables skipping index column. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------------------------- | ----------------------------------- | ---------- | | `df` | [DataFrame](#pandas.DataFrame) | DataFrame with categorical columns. | *required* | | `index_col` | [str](#str) | The index column to avoid encoding. | *required* | **Returns:** | Type | Description | | ------------------------------------------- | --------------------------------------------------- | | [DataFrame](#pandas.DataFrame) | DataFrame with one hot encoded categorical columns. | ### `nested_one_hot_encoding` ```python theme={null} nested_one_hot_encoding(df, index_col) ``` Encodes dataFrame's hierarchically-nested categorical variables. Skips the index column. Nested categorical variables (example geographic levels country>state), require the dummy features to preserve encoding order, to reflect the hierarchy of the categorical variables. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------------------------- | --------------------------------------------------------- | ---------- | | `df` | [DataFrame](#pandas.DataFrame) | DataFrame with hierarchically-nested categorical columns. | *required* | | `index_col` | [str](#str) | The index column to avoid encoding. | *required* | **Returns:** | Type | Description | | ------------------------------------------- | ------------------------------------------------------------------------- | | [DataFrame](#pandas.DataFrame) | DataFrame with one hot encoded hierarchically-nested categorical columns. | ### `get_levels_from_S_df` ```python theme={null} get_levels_from_S_df(S_df) ``` Get hierarchical index levels implied by aggregation constraints dataframe. Create levels from summation matrix (base, bottom). Goes through the rows until all the bottom level series are 'covered' by the aggregation constraints to discover blocks/hierarchy levels. **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------------- | ------------------------------------------------------------ | ---------- | | `S_df` | [DataFrame](#pandas.DataFrame) | Summing matrix of size (base, bottom), see aggregate method. | *required* | **Returns:** | Name | Type | Description | | -------- | -------------------------- | -------------------------------------------------------------- | | `levels` | [list](#list) | Hierarchical aggregation indexes, where each entry is a level. | ### `distance_to_holiday` ```python theme={null} distance_to_holiday(holiday_dates, dates) ``` ### `make_holidays_distance_df` ```python theme={null} make_holidays_distance_df(holidays_df, dates) ``` ### `CodeTimer` ```python theme={null} CodeTimer(name=None, verbose=True) ``` ### `Favorita200` ```python theme={null} Favorita200(freq='D', horizon=34, seasonality=7, test_size=34, tags_names=('Country', 'Country/State', 'Country/State/City', 'Country/State/City/Store')) ``` ### `Favorita500` ```python theme={null} Favorita500(freq='D', horizon=34, seasonality=7, test_size=34, tags_names=('Country', 'Country/State', 'Country/State/City', 'Country/State/City/Store')) ``` ### `FavoritaComplete` ### `FavoritaRawData` Favorita Raw Data. Raw subset datasets from the Favorita 2018 Kaggle competition. This class contains utilities to download, load and filter portions of the dataset. If you prefer, you can also download original dataset available from Kaggle directly: ``` pip install kaggle --upgrade kaggle competitions download -c favorita-grocery-sales-forecasting ``` #### `FavoritaRawData.download` ```python theme={null} download(directory) ``` Downloads Favorita Competition Dataset. The dataset weights 980MB, its download is not currently robust to brief interruptions of the process. It is recommended execute with good connection. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | ---------------------------------------- | ---------- | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | Examples: ```python theme={null} from datasetsforecast.favorita import FavoritaRawData verbose = True group = 'Favorita200' # 'Favorita500', 'FavoritaComplete' directory = './data/favorita' # directory = f's3://favorita' filter_items, filter_stores, filter_dates, raw_group_data = FavoritaRawData._load_raw_group_data(directory=directory, group=group, verbose=verbose) n_items = len(filter_items) n_stores = len(filter_stores) n_dates = len(filter_dates) print('\n') print('n_stores: \t', n_stores) print('n_items: \t', n_items) print('n_dates: \t', n_dates) print('n_items * n_dates: \t\t', n_items * n_dates) print('n_items * n_stores: \t\t', n_items * n_stores) print('n_items * n_dates * n_stores: \t', n_items * n_dates * n_stores) ``` # Hierarchical Source: https://nixtlaverse.nixtla.io/datasetsforecast/hierarchical.html Hierarchical dataset Here we host a collection of datasets used in previous hierarchical research by Rangapuram et al. \[2021], Olivares et al. \[2023], and Kamarthi et al. \[2022]. The benchmark datasets utilized include 1. Australian Monthly Labour: [Labour](#labour), 2. SF Bay Area daily Traffic: [Traffic](#traffic), [OldTraffic](#oldtraffic), 3. Quarterly Australian Tourism Visits: ([TourismSmall](#tourismsmall)), 4. Monthly Australian Tourism visits: [TourismLarge](#tourismlarge), [OldTourismLarge](#oldtourismlarge), 5. daily Wikipedia article views: [Wiki2](#wiki2). Old datasets favor the original datasets with minimal target variable preprocessing (Rangapuram et al. \[2021], Olivares et al. \[2023]), while the remaining datasets follow PROFHIT experimental settings. ## References * [Syama Sundar Rangapuram, Lucien D Werner, Konstantinos Benidis, Pedro Mercado, Jan Gasthaus, Tim Januschowski. (2021). “End-to-End Learning of Coherent Probabilistic Forecasts for Hierarchical Time Series”. Proceedings of the 38th International Conference on Machine Learning (ICML).](https://proceedings.mlr.press/v139/rangapuram21a.html) * [Kin G. Olivares, O. Nganba Meetei, Ruijun Ma, Rohan Reddy, Mengfei Cao, Lee Dicker (2022).”Probabilistic Hierarchical Forecasting with Deep Poisson Mixtures”. International Journal Forecasting, special issue.](https://doi.org/10.1016/j.ijforecast.2023.04.007) * [Harshavardhan Kamarthi, Lingkai Kong, Alexander Rodriguez, Chao Zhang, and B. Prakash. PROFHIT: Probabilistic robust forecasting for hierarchical time-series. Computing Research Repository.URL https://arxiv.org/abs/2206.07940.](https://arxiv.org/abs/2206.07940) ## ### `Labour` ```python theme={null} Labour(freq='MS', horizon=8, papers_horizon=12, seasonality=12, test_size=125, tags_names=('Country', 'Country/Region', 'Country/Gender/Region', 'Country/Employment/Gender/Region')) ``` ### `TourismLarge` ```python theme={null} TourismLarge(freq='MS', horizon=12, papers_horizon=12, seasonality=12, test_size=57, tags_names=('Country', 'Country/State', 'Country/State/Zone', 'Country/State/Zone/Region', 'Country/Purpose', 'Country/State/Purpose', 'Country/State/Zone/Purpose', 'Country/State/Zone/Region/Purpose')) ``` ### `TourismSmall` ```python theme={null} TourismSmall(freq='Q', horizon=4, papers_horizon=4, seasonality=4, test_size=9, tags_names=('Country', 'Country/Purpose', 'Country/Purpose/State', 'Country/Purpose/State/CityNonCity')) ``` ### `Traffic` ```python theme={null} Traffic(freq='D', horizon=14, papers_horizon=7, seasonality=7, test_size=91, tags_names=('Level1', 'Level2', 'Level3', 'Level4')) ``` ### `Wiki2` ```python theme={null} Wiki2(freq='D', horizon=14, papers_horizon=7, seasonality=7, test_size=91, tags_names=('Views', 'Views/Country', 'Views/Country/Access', 'Views/Country/Access/Agent', 'Views/Country/Access/Agent/Topic')) ``` ### `OldTraffic` ```python theme={null} OldTraffic(freq='D', horizon=1, papers_horizon=1, seasonality=7, test_size=91, tags_names=('Level1', 'Level2', 'Level3', 'Level4')) ``` ### `HierarchicalData` #### `HierarchicalData.download` ```python theme={null} download(directory) ``` Download Hierarchical Datasets. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | ----------------------------------- | ---------- | | `directory` | [str](#str) | Directory path to download dataset. | *required* | #### `HierarchicalData.load` ```python theme={null} load(directory, group, cache=True) ``` Downloads hierarchical forecasting benchmark datasets. **Parameters:** | Name | Type | Description | Default | | ----------- | -------------------------- | ---------------------------------------- | ----------------- | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `group` | [str](#str) | Group name. | *required* | | `cache` | [bool](#bool) | If `True` saves and loads | True | **Returns:** | Type | Description | | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[DataFrame](#pandas.DataFrame), [DataFrame](#pandas.DataFrame)] | Tuple\[pd.DataFrame, pd.DataFrame]: Target time series with columns \['unique\_id', 'ds', 'y']. Containes the base time series, Summing matrix of size (hierarchies, bottom). | # datasetsforecast Source: https://nixtlaverse.nixtla.io/datasetsforecast/index.html Datasets for time series forecasting ## Install ```sh theme={null} pip install datasetsforecast ``` ## Datasets * [Favorita](./favorita.html) * [Hierarchical](./hierarchical.html) * [Longhorizon](./long_horizon.html) * [M3](./m3.html) * [M4](./m4.html) * [M5](./m5.html) * [PHM2008](./phm2008.html) ## How to use All the modules have a `load` method which you can use to load the dataset for a specific group. If you don’t have the data locally it will be downloaded for you. ```python theme={null} from datasetsforecast.phm2008 import PHM2008 ``` ```python theme={null} train_df, test_df = PHM2008.load(directory='data', group='FD001') train_df.shape, test_df.shape ``` ((20631, 17), (13096, 17)) # Long Horizon Source: https://nixtlaverse.nixtla.io/datasetsforecast/long_horizon.html Download and wrangling utility for long-horizon datasets. ## ### `ETTm2` ```python theme={null} ETTm2(freq='15T', name='ETTm2', n_ts=7, test_size=11520, val_size=11520, horizons=(96, 192, 336, 720)) ``` The ETTm2 dataset monitors an electricity transformer from a region of a province of China including oil temperature and variants of load (such as high useful load and high useless load) from July 2016 to July 2018 at a fifteen minute frequency. Reference: * [Zhou, et al. Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting. AAAI 2021.](https://arxiv.org/abs/2012.07436) ### `ETTm1` ```python theme={null} ETTm1(freq='15T', name='ETTm1', n_ts=7, test_size=11520, val_size=11520, horizons=(96, 192, 336, 720)) ``` The ETTm1 dataset monitors an electricity transformer from a region of a province of China including oil temperature and variants of load (such as high useful load and high useless load) from July 2016 to July 2018 at a fifteen minute frequency. ### `ETTh2` ```python theme={null} ETTh2(freq='H', name='ETTh2', n_ts=1, test_size=11520, val_size=11520, horizons=(96, 192, 336, 720)) ``` The ETTh2 dataset monitors an electricity transformer from a region of a province of China including oil temperature and variants of load (such as high useful load and high useless load) from July 2016 to July 2018 at an hourly frequency. ### `ETTh1` ```python theme={null} ETTh1(freq='H', name='ETTh1', n_ts=1, test_size=11520, val_size=11520, horizons=(96, 192, 336, 720)) ``` The ETTh1 dataset monitors an electricity transformer from a region of a province of China including oil temperature and variants of load (such as high useful load and high useless load) from July 2016 to July 2018 at an hourly frequency. ### `ECL` ```python theme={null} ECL(freq='15T', name='ECL', n_ts=321, test_size=5260, val_size=2632, horizons=(96, 192, 336, 720)) ``` The Electricity dataset reports the fifteen minute electricity consumption (KWh) of 321 customers from 2012 to 2014. For comparability, we aggregate it hourly. Reference: * [Li, S et al. Enhancing the locality and breaking the memory bottleneck of Transformer on time series forecasting. NeurIPS 2019.](http://arxiv.org/abs/1907.00235) ### `Exchange` ```python theme={null} Exchange(freq='D', name='Exchange', n_ts=8, test_size=1517, val_size=760, horizons=(96, 192, 336, 720)) ``` The Exchange dataset is a collection of daily exchange rates of eight countries relative to the US dollar. The countries include Australia, UK, Canada, Switzerland, China, Japan, New Zealand and Singapore from 1990 to 2016. Reference: * [Lai, G., Chang, W., Yang, Y., and Liu, H. Modeling Long and Short-Term Temporal Patterns with Deep Neural Networks. SIGIR 2018.](http://arxiv.org/abs/1703.07015) ### `TrafficL` ```python theme={null} TrafficL(freq='H', name='traffic', n_ts=862, test_size=3508, val_size=1756, horizons=(96, 192, 336, 720)) ``` This large Traffic dataset was collected by the California Department of Transportation, it reports road hourly occupancy rates of 862 sensors, from January 2015 to December 2016. Reference: * [Lai, G., Chang, W., Yang, Y., and Liu, H. Modeling Long and Short-Term Temporal Patterns with Deep Neural Networks. SIGIR 2018.](http://arxiv.org/abs/1703.07015) * [Wu, H., Xu, J., Wang, J., and Long, M. Autoformer: Decomposition Transformers with auto-correlation for long-term series forecasting. NeurIPS 2021.](https://arxiv.org/abs/2106.13008). ### `ILI` ```python theme={null} ILI(freq='W', name='ili', n_ts=7, test_size=193, val_size=97, horizons=(24, 36, 48, 60)) ``` This dataset reports weekly recorded influenza-like illness (ILI) patients from Centers for Disease Control and Prevention of the United States from 2002 to 2021. It is measured as a ratio of ILI patients versus the total patients in the week. Reference: * [Wu, H., Xu, J., Wang, J., and Long, M. Autoformer: Decomposition Transformers with auto-correlation for long-term series forecasting. NeurIPS 2021.](https://arxiv.org/abs/2106.13008). ### `Weather` ```python theme={null} Weather(freq='10M', name='weather', n_ts=21, test_size=10539, val_size=5270, horizons=(96, 192, 336, 720)) ``` This Weather dataset contains the 2020 year of 21 meteorological measurements recorded every 10 minutes from the Weather Station of the Max Planck Biogeochemistry Institute in Jena, Germany. Reference: * [Wu, H., Xu, J., Wang, J., and Long, M. Autoformer: Decomposition Transformers with auto-correlation for long-term series forecasting. NeurIPS 2021.](https://arxiv.org/abs/2106.13008). ### `LongHorizon` ```python theme={null} LongHorizon(source_url='https://nhits-experiments.s3.amazonaws.com/datasets.zip') ``` This Long-Horizon datasets wrapper class, provides with utility to download and wrangle the following datasets: ETT, ECL, Exchange, Traffic, ILI and Weather. * Each set is normalized with the train data mean and standard deviation. * Datasets are partitioned into train, validation and test splits. * For all datasets: 70%, 10%, and 20% of observations are train, validation, test, except ETT that uses 20% validation. #### `LongHorizon.download` ```python theme={null} download(directory) ``` Download ETT Dataset. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | ----------------------------------- | ---------- | | `directory` | [str](#str) | Directory path to download dataset. | *required* | #### `LongHorizon.load` ```python theme={null} load(directory, group, cache=True) ``` Downloads and long-horizon forecasting benchmark datasets. **Parameters:** | Name | Type | Description | Default | | ----------- | -------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------- | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `group` | [str](#str) | Group name. Allowed groups: 'ETTh1', 'ETTh2', 'ETTm1', 'ETTm2', 'ECL', 'Exchange', 'Traffic', 'Weather', 'ILI'. | *required* | | `cache` | [bool](#bool) | If `True` saves and loads | True | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[DataFrame](#pandas.DataFrame), [Optional](#typing.Optional)\[[DataFrame](#pandas.DataFrame)], [Optional](#typing.Optional)\[[DataFrame](#pandas.DataFrame)]] | Tuple\[pd.DataFrame, Optional\[pd.DataFrame], Optional\[pd.DataFrame]]: Target time series with columns \['unique\_id', 'ds', 'y'], Exogenous time series with columns \['unique\_id', 'ds', 'y'], Static exogenous variables with columns \['unique\_id', 'ds'] and static variables. | # Long-Horizon Original Datasets Source: https://nixtlaverse.nixtla.io/datasetsforecast/long_horizon2.html Download and wrangling utility for long-horizon datasets. These datasets have been used by `NHITS, AutoFormer, Informer, PatchTST, TiDE` among many other neural forecasting methods. The datasets include the original [ETTh1, ETTh2, ETTm1, ETTm2, Weather, ILI, TrafficL](https://github.com/zhouhaoyi/ETDataset) benchmark datasets. ## ### `Weather` ```python theme={null} Weather(freq='10M', name='weather', n_ts=21, test_size=10539, val_size=5270, horizons=(96, 192, 336, 720)) ``` This Weather dataset contains the 2020 year of 21 meteorological measurements recorded every 10 minutes from the Weather Station of the Max Planck Biogeochemistry Institute in Jena, Germany. Reference: * [Wu, H., Xu, J., Wang, J., and Long, M. Autoformer: Decomposition Transformers with auto-correlation for long-term series forecasting. NeurIPS 2021.](https://arxiv.org/abs/2106.13008.) ### `TrafficL` ```python theme={null} TrafficL(freq='H', name='traffic', n_ts=862, test_size=3508, val_size=1756, horizons=(96, 192, 336, 720)) ``` This large Traffic dataset was collected by the California Department of Transportation, it reports road hourly occupancy rates of 862 sensors, from January 2015 to December 2016. Reference: * [Lai, G., Chang, W., Yang, Y., and Liu, H. Modeling Long and Short-Term Temporal Patterns with Deep Neural Networks. SIGIR 2018.](http://arxiv.org/abs/1703.07015) * [Wu, H., Xu, J., Wang, J., and Long, M. Autoformer: Decomposition Transformers with auto-correlation for long-term series forecasting. NeurIPS 2021.](https://arxiv.org/abs/2106.13008) ### `ECL` ```python theme={null} ECL(freq='15T', name='ECL', n_ts=321, n_time=26304, test_size=5260, val_size=2632, horizons=(96, 192, 336, 720)) ``` The Electricity dataset reports the fifteen minute electricity consumption (KWh) of 321 customers from 2012 to 2014. For comparability, we aggregate it hourly. Reference: * [Li, S et al. Enhancing the locality and breaking the memory bottleneck of Transformer on time series forecasting. NeurIPS 2019.](https://arxiv.org/abs/1907.00235) ### `ETTm2` ```python theme={null} ETTm2(freq='15T', name='ETTm2', n_ts=7, n_time=57600, test_size=11520, val_size=11520, horizons=(96, 192, 336, 720)) ``` The ETTm2 dataset monitors an electricity transformer from a region of a province of China including oil temperature and variants of load (such as high useful load and high useless load) from July 2016 to July 2018 at a fifteen minute frequency. Reference: * [Zhou, et al. Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting. AAAI 2021.](https://arxiv.org/abs/2012.07436) ### `ETTm1` ```python theme={null} ETTm1(freq='15T', name='ETTm1', n_ts=7, n_time=57600, test_size=11520, val_size=11520, horizons=(96, 192, 336, 720)) ``` The ETTm1 dataset monitors an electricity transformer from a region of a province of China including oil temperature and variants of load (such as high useful load and high useless load) from July 2016 to July 2018 at a fifteen minute frequency. ### `ETTh2` ```python theme={null} ETTh2(freq='H', name='ETTh2', n_ts=7, n_time=14400, test_size=2880, val_size=2880, horizons=(96, 192, 336, 720)) ``` The ETTh2 dataset monitors an electricity transformer from a region of a province of China including oil temperature and variants of load (such as high useful load and high useless load) from July 2016 to July 2018 at an hourly frequency. ### `ETTh1` ```python theme={null} ETTh1(freq='H', name='ETTh1', n_ts=7, n_time=14400, test_size=2880, val_size=2880, horizons=(96, 192, 336, 720)) ``` The ETTh1 dataset monitors an electricity transformer from a region of a province of China including oil temperature and variants of load (such as high useful load and high useless load) from July 2016 to July 2018 at an hourly frequency. ### `LongHorizon2` ```python theme={null} LongHorizon2(source_url='https://www.dropbox.com/s/rlc1qmprpvuqrsv/all_six_datasets.zip?dl=1') ``` This Long-Horizon datasets wrapper class, provides with utility to download and wrangle the following datasets: ETT, ECL, Exchange, Traffic, ILI and Weather. * Each set is normalized with the train data mean and standard deviation. * Datasets are partitioned into train, validation and test splits. * For all datasets: 70%, 10%, and 20% of observations are train, validation, test, except ETT that uses 20% validation. #### `LongHorizon2.download` ```python theme={null} download(directory) ``` Download Long Horizon 2 Datasets. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | ----------------------------------- | ---------- | | `directory` | [str](#str) | Directory path to download dataset. | *required* | #### `LongHorizon2.load` ```python theme={null} load(directory, group, normalize=True) ``` Downloads and long-horizon forecasting benchmark datasets. **Parameters:** | Name | Type | Description | Default | | ----------- | -------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------- | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `group` | [str](#str) | Group name. Allowed groups: 'ETTh1', 'ETTh2', 'ETTm1', 'ETTm2', 'ECL', 'Exchange', 'Traffic', 'Weather', 'ILI'. | *required* | | `normalize` | [bool](#bool) | If `True` std. normalize data or not | True | **Returns:** | Type | Description | | ------------------------------------------- | ------------------------------------------------------------------------- | | [DataFrame](#pandas.DataFrame) | pd.DataFrame: Target time series with columns \['unique\_id', 'ds', 'y']. | # M3 Source: https://nixtlaverse.nixtla.io/datasetsforecast/m3.html M3 dataset ## ### `Other` ```python theme={null} Other(seasonality=1, horizon=8, freq='D', name='Other', n_ts=174, source_url='https://zenodo.org/api/records/4656335/files/m3_other_dataset.zip/content', file_name='m3_other_dataset') ``` ### `Monthly` ```python theme={null} Monthly(seasonality=12, horizon=18, freq='ME', name='Monthly', n_ts=1428, source_url='https://zenodo.org/api/records/4656298/files/m3_monthly_dataset.zip/content', file_name='m3_monthly_dataset') ``` ### `Quarterly` ```python theme={null} Quarterly(seasonality=4, horizon=8, freq='QE', name='Quarterly', n_ts=756, source_url='https://zenodo.org/api/records/4656262/files/m3_quarterly_dataset.zip/content', file_name='m3_quarterly_dataset') ``` ### `Yearly` ```python theme={null} Yearly(seasonality=1, horizon=6, freq='YE', name='Yearly', n_ts=645, source_url='https://zenodo.org/api/records/4656222/files/m3_yearly_dataset.zip/content', file_name='m3_yearly_dataset') ``` ### `M3` ```python theme={null} M3() ``` #### `M3.download` ```python theme={null} download(directory, class_group) ``` Download M3 Dataset. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------ | ---------- | | `directory` | [str](#str) | Directory path to download dataset. | *required* | | `class_group` | | Dataclass with source\_url and file\_name. | *required* | #### `M3.load` ```python theme={null} load(directory, group) ``` Downloads and loads M3 data. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | ---------------------------------------------------------------------- | ---------- | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `group` | [str](#str) | Group name. Allowed groups: 'Yearly', 'Quarterly', 'Monthly', 'Other'. | *required* | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[DataFrame](#pandas.DataFrame), [Optional](#typing.Optional)\[[DataFrame](#pandas.DataFrame)], [Optional](#typing.Optional)\[[DataFrame](#pandas.DataFrame)]] | pd.DataFrame: Target time series with columns \['unique\_id', 'ds', 'y']. | # M4 Source: https://nixtlaverse.nixtla.io/datasetsforecast/m4.html M4 dataset ## ### `Other` ```python theme={null} Other(seasonality=1, horizon=8, freq='D', name='Other', n_ts=5000, included_groups=('Weekly', 'Daily', 'Hourly')) ``` ### `Hourly` ```python theme={null} Hourly(seasonality=24, horizon=48, freq='H', name='Hourly', n_ts=414) ``` ### `Daily` ```python theme={null} Daily(seasonality=1, horizon=14, freq='D', name='Daily', n_ts=4227) ``` ### `Weekly` ```python theme={null} Weekly(seasonality=1, horizon=13, freq='W', name='Weekly', n_ts=359) ``` ### `Monthly` ```python theme={null} Monthly(seasonality=12, horizon=18, freq='M', name='Monthly', n_ts=48000) ``` ### `Quarterly` ```python theme={null} Quarterly(seasonality=4, horizon=8, freq='Q', name='Quarterly', n_ts=24000) ``` ### `Yearly` ```python theme={null} Yearly(seasonality=1, horizon=6, freq='Y', name='Yearly', n_ts=23000) ``` ## Download data class ### `M4` ```python theme={null} M4(source_url='https://raw.githubusercontent.com/Mcompetitions/M4-methods/master/Dataset/', naive2_forecast_url='https://github.com/Nixtla/m4-forecasts/raw/master/forecasts/submission-Naive2.zip') ``` #### `M4.async_download` ```python theme={null} async_download(directory, group=None) ``` Download M4 Dataset. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | ----------------------------------- | ---------- | | `directory` | [str](#str) | Directory path to download dataset. | *required* | Example: ```python theme={null} group = 'Hourly' await M4.async_download('data', group=group) df, *_ = M4.load(directory='data', group=group) n_series = len(np.unique(df.unique_id.values)) display_str = f'Group: {group} ' display_str += f'n_series: {n_series}' print(display_str) ``` #### `M4.download` ```python theme={null} download(directory, group=None) ``` Download M4 Dataset. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | ------------------------------------------------------------------------ | ----------------- | | `directory` | [str](#str) | Directory path to download dataset. | *required* | | `group` | [str](#str) | Name of the group to download. If None, downloads all. Defaults to None. | None | #### `M4.load` ```python theme={null} load(directory, group, cache=True) ``` Downloads and loads M4 data. **Parameters:** | Name | Type | Description | Default | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------ | ----------------- | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `group` | [str](#str) | Group name. Allowed groups: 'Yearly', 'Quarterly', 'Monthly', 'Weekly', 'Daily', 'Hourly'. | *required* | | `cache` | [bool](#bool) | If `True` saves and loads | True | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [Tuple](#typing.Tuple)\[[DataFrame](#pandas.DataFrame), [Optional](#typing.Optional)\[[DataFrame](#pandas.DataFrame)], [Optional](#typing.Optional)\[[DataFrame](#pandas.DataFrame)]] | Tuple\[pd.DataFrame, Optional\[pd.DataFrame], Optional\[pd.DataFrame]]: Target time series with columns \['unique\_id', 'ds', 'y'], Static exogenous variables with columns \['unique\_id', 'ds'], and static variables. | ## Evaluation class ### `M4Evaluation` #### `M4Evaluation.evaluate` ```python theme={null} evaluate(directory, group, y_hat) ``` Evaluates y\_hat according to M4 methodology. **Parameters:** | Name | Type | Description | Default | | ----------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `group` | [str](#str) | Group name. Allowed groups: 'Yearly', 'Quarterly', 'Monthly', 'Weekly', 'Daily', 'Hourly'. | *required* | | `y_hat` | [Union](#typing.Union)\[[ndarray](#numpy.ndarray), [str](#str)] | Group forecasts as numpy array or benchmark url from [https://github.com/Nixtla/m4-forecasts/tree/master/forecasts](https://github.com/Nixtla/m4-forecasts/tree/master/forecasts). | *required* | **Returns:** | Type | Description | | ------------------------------------------- | ------------------------------------------------------------------------- | | [DataFrame](#pandas.DataFrame) | pd.DataFrame: DataFrame with columns OWA, SMAPE, MASE and group as index. | Examples: ```python theme={null} esrnn_url = 'https://github.com/Nixtla/m4-forecasts/raw/master/forecasts/submission-118.zip' esrnn_evaluation = M4Evaluation.evaluate('data', 'Hourly', esrnn_url) fforma_url = 'https://github.com/Nixtla/m4-forecasts/raw/master/forecasts/submission-245.zip' fforma_forecasts = M4Evaluation.load_benchmark('data', 'Hourly', fforma_url) fforma_evaluation = M4Evaluation.evaluate('data', 'Hourly', fforma_forecasts) ``` #### `M4Evaluation.load_benchmark` ```python theme={null} load_benchmark(directory, group, source_url=None) ``` Downloads and loads a bechmark forecasts. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `group` | [str](#str) | Group name. Allowed groups: 'Yearly', 'Quarterly', 'Monthly', 'Weekly', 'Daily', 'Hourly'. | *required* | | `source_url` | [str](#str) | Optional benchmark url obtained from [https://github.com/Nixtla/m4-forecasts/tree/master/forecasts](https://github.com/Nixtla/m4-forecasts/tree/master/forecasts). If `None` returns Naive2. | None | **Returns:** | Type | Description | | -------------------------------------- | ------------------------------------------------------ | | [ndarray](#numpy.ndarray) | np.ndarray: Numpy array of shape (n\_series, horizon). | ### URL-based evaluation The method `evaluate` from the class [`M4Evaluation`](https://Nixtla.github.io/datasetsforecast/m4.html#m4evaluation) can receive a url of a [benchmark uploaded to the M4 competiton](https://github.com/Mcompetitions/M4-methods/tree/master/Point%20Forecasts). The results compared to the on-the-fly evaluation were obtained from the [official evaluation](https://github.com/Mcompetitions/M4-methods/blob/master/Evaluation%20and%20Ranks.xlsx). ```python theme={null} import numpy as np esrnn_url = 'https://github.com/Nixtla/m4-forecasts/raw/master/forecasts/submission-118.zip' esrnn_evaluation = M4Evaluation.evaluate('data', 'Hourly', esrnn_url) # Test of the same evaluation as the original one assert np.isclose(esrnn_evaluation['SMAPE'].item(), 9.328, atol=1e-3) assert np.isclose(esrnn_evaluation['MASE'].item(), 0.893, atol=1e-3) assert np.isclose(esrnn_evaluation['OWA'].item(), 0.440, atol=1e-3) esrnn_evaluation ``` ### Numpy-based evaluation Also the method `evaluate` can recevie a numpy array of forecasts. ```python theme={null} import numpy as np fforma_url = 'https://github.com/Nixtla/m4-forecasts/raw/master/forecasts/submission-245.zip' fforma_forecasts = M4Evaluation.load_benchmark('data', 'Hourly', fforma_url) fforma_evaluation = M4Evaluation.evaluate('data', 'Hourly', fforma_forecasts) # Test of the same evaluation as the original one assert np.isclose(fforma_evaluation['SMAPE'].item(), 11.506, atol=1e-3) assert np.isclose(fforma_evaluation['MASE'].item(), 0.819, atol=1e-3) assert np.isclose(fforma_evaluation['OWA'].item(), 0.484, atol=1e-3) fforma_evaluation ``` # M5 Source: https://nixtlaverse.nixtla.io/datasetsforecast/m5.html M5 dataset ## ### `M5` ```python theme={null} M5(source_url='https://github.com/Nixtla/m5-forecasts/raw/main/datasets/m5.zip') ``` #### `M5.download` ```python theme={null} download(directory) ``` Downloads M5 Competition Dataset. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | ----------------------------------- | ---------- | | `directory` | [str](#str) | Directory path to download dataset. | *required* | #### `M5.load` ```python theme={null} load(directory, cache=True) ``` Downloads and loads M5 data. **Parameters:** | Name | Type | Description | Default | | ----------- | -------------------------- | ---------------------------------------- | ----------------- | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `cache` | [bool](#bool) | If `True` saves and loads. | True | **Returns:** | Type | Description | | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[DataFrame](#pandas.DataFrame), [DataFrame](#pandas.DataFrame), [DataFrame](#pandas.DataFrame)] | Tuple\[pd.DataFrame, pd.DataFrame, pd.DataFrame]: Target time series with columns \['unique\_id', 'ds', 'y'], Exogenous time series with columns \['unique\_id', 'ds', 'y'], Static exogenous variables with columns \['unique\_id', 'ds'] and static variables. | #### `M5.source_url` ```python theme={null} source_url: str = 'https://github.com/Nixtla/m5-forecasts/raw/main/datasets/m5.zip' ``` ## Evaluation class ### `M5Evaluation` #### `M5Evaluation.aggregate_levels` ```python theme={null} aggregate_levels(y_hat, categories=None) ``` Aggregates the 30\_480 series to get 42\_840. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------- | ---------------------------------------------------------------- | ----------------- | | `y_hat` | [DataFrame](#pandas.DataFrame) | Forecasts as wide pandas dataframe with columns \['unique\_id']. | *required* | | `categories` | [DataFrame](#pandas.DataFrame) | Categories of M5 dataset (not used). Defaults to None. | None | **Returns:** | Type | Description | | ------------------------------------------- | ----------------------------------------------------------------------------------------- | | [DataFrame](#pandas.DataFrame) | pd.DataFrame: Aggregated forecasts as wide pandas dataframe with columns \['unique\_id']. | #### `M5Evaluation.evaluate` ```python theme={null} evaluate(directory, y_hat, validation=False) ``` Evaluates y\_hat according to M4 methodology. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `validation` | [bool](#bool) | Wheter perform validation evaluation. Default False, return test evaluation. | False | | `y_hat` | [Union](#typing.Union)\[[DataFrame](#pandas.DataFrame), [str](#str)] | Forecasts as wide pandas dataframe with columns \['unique\_id'] and forecasts or benchmark url from [https://github.com/Nixtla/m5-forecasts/tree/main/forecasts](https://github.com/Nixtla/m5-forecasts/tree/main/forecasts). | *required* | **Returns:** | Type | Description | | ------------------------------------------- | ------------------------------------------------------------------------- | | [DataFrame](#pandas.DataFrame) | pd.DataFrame: DataFrame with columns OWA, SMAPE, MASE and group as index. | Examples: ```python theme={null} m5_winner_url = 'https://github.com/Nixtla/m5-forecasts/raw/main/forecasts/0001 YJ_STU.zip' winner_evaluation = M5Evaluation.evaluate('data', m5_winner_url) m5_second_place_url = 'https://github.com/Nixtla/m5-forecasts/raw/main/forecasts/0002 Matthias.zip' m5_second_place_forecasts = M5Evaluation.load_benchmark('data', m5_second_place_url) second_place_evaluation = M5Evaluation.evaluate('data', m5_second_place_forecasts) ``` #### `M5Evaluation.levels` ```python theme={null} levels: dict = dict(Level1=['total'], Level2=['state_id'], Level3=['store_id'], Level4=['cat_id'], Level5=['dept_id'], Level6=['state_id', 'cat_id'], Level7=['state_id', 'dept_id'], Level8=['store_id', 'cat_id'], Level9=['store_id', 'dept_id'], Level10=['item_id'], Level11=['state_id', 'item_id'], Level12=['item_id', 'store_id']) ``` #### `M5Evaluation.load_benchmark` ```python theme={null} load_benchmark(directory, source_url=None, validation=False) ``` Downloads and loads a bechmark forecasts. **Parameters:** | Name | Type | Description | Default | | ------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `source_url` | [str](#str) | Optional benchmark url obtained from [https://github.com/Nixtla/m5-forecasts/tree/master/forecasts](https://github.com/Nixtla/m5-forecasts/tree/master/forecasts). If `None` returns the M5 winner. | None | | `validation` | [bool](#bool) | Wheter return validation forecasts. Default False, return test forecasts. | False | **Returns:** | Type | Description | | -------------------------------------- | ------------------------------------------------------ | | [ndarray](#numpy.ndarray) | np.ndarray: Numpy array of shape (n\_series, horizon). | Example: ```python theme={null} winner_benchmark = M5Evaluation.load_benchmark('data') winner_evaluation = M5Evaluation.evaluate('data', winner_benchmark) ``` ### URL-based evaluation The method `evaluate` from the class [`M5Evaluation`](https://Nixtla.github.io/datasetsforecast/m5.html#m5evaluation) can receive a url of a [submission to the M5 competiton](https://github.com/Nixtla/m5-forecasts/tree/main/forecasts). The results compared to the on-the-fly evaluation were obtained from the [official evaluation](https://github.com/Mcompetitions/M5-methods/blob/master/Scores%20and%20Ranks.xlsx). ```python theme={null} m5_winner_url = 'https://github.com/Nixtla/m5-forecasts/raw/main/forecasts/0001 YJ_STU.zip' winner_evaluation = M5Evaluation.evaluate('data', m5_winner_url) # Test of the same evaluation as the original one test_close(winner_evaluation.loc['Total'].item(), 0.520, eps=1e-3) winner_evaluation ``` ### Pandas-based evaluation Also the method `evaluate` can recevie a pandas DataFrame of forecasts. ```python theme={null} m5_second_place_url = 'https://github.com/Nixtla/m5-forecasts/raw/main/forecasts/0002 Matthias.zip' m5_second_place_forecasts = M5Evaluation.load_benchmark('data', m5_second_place_url) second_place_evaluation = M5Evaluation.evaluate('data', m5_second_place_forecasts) # Test of the same evaluation as the original one test_close(second_place_evaluation.loc['Total'].item(), 0.528, eps=1e-3) second_place_evaluation ``` By default you can load the winner benchmark using the following. ```python theme={null} winner_benchmark = M5Evaluation.load_benchmark('data') winner_evaluation = M5Evaluation.evaluate('data', winner_benchmark) # Test of the same evaluation as the original one test_close(winner_evaluation.loc['Total'].item(), 0.520, eps=1e-3) winner_evaluation ``` ### Validation evaluation You can also evaluate the official validation set. ```python theme={null} winner_benchmark_val = M5Evaluation.load_benchmark('data', validation=True) winner_evaluation_val = M5Evaluation.evaluate('data', winner_benchmark_val, validation=True) winner_evaluation_val ``` ## Kaggle-Competition-M5 References The evaluation metric of the Favorita Kaggle competition was the normalized weighted root mean squared logarithmic error (NWRMSLE). Perishable items have a score weight of 1.25; otherwise, the weight is 1.0. $ NWRMSLE = \sqrt{\frac{\sum^{n}_{i=1} w_{i}\left(log(\hat{y}_{i}+1) - log(y_{i}+1)\right)^{2}}{\sum^{n}_{i=1} w_{i}}}$ | Kaggle Competition Forecasting Methods | 16D ahead NWRMSLE | | :--------------------------------------------------------------------------------------------------: | :---------------: | | [LGBM](https://www.kaggle.com/shixw125/1st-place-lgb-model-public-0-506-private-0-511/comments) \[1] | 0.5091 | | [Seq2Seq WaveNet](https://arxiv.org/abs/1803.04037) \[2] | 0.5129 | 1. [Corporación Favorita. Corporación favorita grocery sales forecasting. Kaggle Competition Leaderboard, 2018.](https://www.kaggle.com/c/favorita-grocery-sales-forecasting/leaderboard) 2. [Glib Kechyn, Lucius Yu, Yangguang Zang, and Svyatoslav Kechyn. Sales forecasting using wavenet within the framework of the Favorita Kaggle competition. Computing Research Repository, abs/1803.04037, 2018](https://arxiv.org/abs/1803.04037). # PHM2008 Source: https://nixtlaverse.nixtla.io/datasetsforecast/phm2008.html PHM2008 dataset ## ### `FD004` ```python theme={null} FD004(seasonality=1, horizon=8, freq='None', train_file='train_FD004.txt', test_file='test_FD004.txt', rul_file='RUL_FD004.txt', n_ts=249, n_test=248) ``` ### `FD003` ```python theme={null} FD003(seasonality=1, horizon=1, freq='None', train_file='train_FD003.txt', test_file='test_FD003.txt', rul_file='RUL_FD003.txt', n_ts=100, n_test=100) ``` ### `FD002` ```python theme={null} FD002(seasonality=1, horizon=1, freq='None', train_file='train_FD002.txt', test_file='test_FD002.txt', rul_file='RUL_FD002.txt', n_ts=260, n_test=259) ``` ### `FD001` ```python theme={null} FD001(seasonality=1, horizon=1, freq='None', train_file='train_FD001.txt', test_file='test_FD001.txt', rul_file='RUL_FD001.txt', n_ts=100, n_test=100) ``` ### `PHM2008` ```python theme={null} PHM2008() ``` #### `PHM2008.download` ```python theme={null} download(directory) ``` Download PHM2008 Dataset. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | ----------------------------------- | ---------- | | `directory` | [str](#str) | Directory path to download dataset. | *required* | #### `PHM2008.load` ```python theme={null} load(directory, group, clip_rul=True) ``` Downloads and loads M3 data. **Parameters:** | Name | Type | Description | Default | | ----------- | -------------------------- | --------------------------------------------------------------- | ----------------- | | `directory` | [str](#str) | Directory where data will be downloaded. | *required* | | `group` | [str](#str) | Group name. Allowed groups: 'FD001', 'FD002', 'FD003', 'FD004'. | *required* | | `clip_rul` | [bool](#bool) | Wether or not upper bound the remaining useful life to 125. | True | **Returns:** | Type | Description | | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | [Tuple](#typing.Tuple)\[[DataFrame](#pandas.DataFrame), [DataFrame](#pandas.DataFrame)] | Tuple\[pd.DataFrame, pd.DataFrame]: Target time series with columns \['unique\_id', 'ds', 'y', 'exogenous']. | # Utils | DatasetsForecast Source: https://nixtlaverse.nixtla.io/datasetsforecast/utils.html Utility functions for datasetsforecast ## ### `download_file` ```python theme={null} download_file(directory, source_url, decompress=False, filename=None, max_retries=3) ``` Download data from source\_url inside directory. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------ | | `directory` | ([str](#str), [Path](#pathlib.Path)) | Custom directory where data will be downloaded. | *required* | | `source_url` | [str](#str) | URL where data is hosted. | *required* | | `decompress` | [bool](#bool) | Whether to decompress downloaded file. Default False. | False | | `filename` | [str](#str) | Override filename for the downloaded file. If None, the filename is derived from the URL. | None | | `max_retries` | [int](#int) | Maximum number of retry attempts on transient errors. | 3 | ### `extract_file` ```python theme={null} extract_file(filepath, directory) ``` ### `async_download_files` ```python theme={null} async_download_files(path, urls) ``` Asynchronously download files from urls inside path. **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------------------------- | ----------------------------------------- | ---------- | | `path` | ([str](#str), [Path](#pathlib.Path)) | Directory where files will be downloaded. | *required* | | `urls` | [Iterable](#typing.Iterable)\[[str](#str)] | Iterable of URLs to download. | *required* | Example: ```python theme={null} import os import tempfile import requests gh_url = 'https://api.github.com/repos/Nixtla/datasetsforecast/contents/' base_url = 'https://raw.githubusercontent.com/Nixtla/datasetsforecast/main' headers = {} gh_token = os.getenv('GITHUB_TOKEN') if gh_token is not None: headers = {'Authorization': f'Bearer: {gh_token}'} resp = requests.get(gh_url, headers=headers) if resp.status_code != 200: raise Exception(resp.text) urls = [f'{base_url}/{e["path"]}' for e in resp.json() if e['type'] == 'file'] with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) await async_download_files(tmp, urls) files = list(tmp.iterdir()) assert len(files) == len(urls) ``` ### `download_files` ```python theme={null} download_files(directory, urls) ``` Download files from urls inside directory. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------------------------------------- | ----------------------------------------- | ---------- | | `directory` | ([str](#str), [Path](#pathlib.Path)) | Directory where files will be downloaded. | *required* | | `urls` | [Iterable](#typing.Iterable)\[[str](#str)] | Iterable of URLs to download. | *required* | Example: ```python theme={null} with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) fname = tmp / 'script.py' fname.write_text(f''' from datasetsforecast.utils import download_files download_files('{tmp.as_posix()}', {urls}) ''') !python {fname} fname.unlink() files = list(tmp.iterdir()) assert len(files) == len(urls) ``` # Core | HierarchicalForecast Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/core.html Core ## HierarchicalForecast contains pure Python implementations of hierarchical reconciliation methods as well as a `core.HierarchicalReconciliation` wrapper class that enables easy interaction with these methods through pandas DataFrames containing the hierarchical time series and the base predictions. The `core.HierarchicalReconciliation` reconciliation class operates with the hierarchical time series pd.DataFrame `Y_df`, the base predictions pd.DataFrame `Y_hat_df`, the aggregation constraints matrix `S_df`. For more information on the creation of aggregation constraints matrix see the utils [aggregation method](https://nixtlaverse.nixtla.io/hierarchicalforecast/utils.html#aggregate) ### `HierarchicalReconciliation` ```python theme={null} HierarchicalReconciliation(reconcilers) ``` Hierarchical Reconciliation Class. The `core.HierarchicalReconciliation` class allows you to efficiently fit multiple HierarchicaForecast methods for a collection of time series and base predictions stored in pandas DataFrames. The `Y_df` dataframe identifies series and datestamps with the unique\_id and ds columns while the y column denotes the target time series variable. The `Y_h` dataframe stores the base predictions, example ([AutoARIMA](../statsforecast/src/core/models.html#autoarima), [ETS](../statsforecast/src/core/models.html#autoets), etc.). **Parameters:** | Name | Type | Description | Default | | --------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ---------- | | `- reconcilers` | [list](#list)\[[HReconciler](#hierarchicalforecast.methods.HReconciler)] | A list of instantiated classes of the [reconciliation methods](./methods.html) module. | *required* |
References * [Rob J. Hyndman and George Athanasopoulos (2018). "Forecasting principles and practice, Hierarchical and Grouped Series"](https://otexts.com/fpp3/hierarchical.html).
#### `HierarchicalReconciliation.reconcile` ```python theme={null} reconcile(Y_hat_df, tags, S_df=None, Y_df=None, level=None, intervals_method='normality', num_samples=-1, seed=0, is_balanced=False, id_col='unique_id', time_col='ds', target_col='y', id_time_col='temporal_id', temporal=False, diagnostics=False, diagnostics_atol=1e-06) ``` Hierarchical Reconciliation Method. The `reconcile` method is analogous to SKLearn `fit_predict` method, it applies different reconciliation techniques instantiated in the `reconcilers` list. Most reconciliation methods can be described by the following convenient linear algebra notation: ```math theme={null} \tilde{\mathbf{y}}_{[a,b],\\tau} = \mathbf{S}_{[a,b][b]} \mathbf{P}_{[b][a,b]} \hat{\mathbf{y}}_{[a,b],\\tau} ``` where $a, b$ represent the aggregate and bottom levels, $\mathbf{S}_{[a,b][b]}$ contains the hierarchical aggregation constraints, and $\mathbf{P}_{[b][a,b]}$ varies across reconciliation methods. The reconciled predictions are ```math theme={null} \tilde{\mathbf{y}}_{[a,b],\tau} ``` and the base predictions ```math theme={null} \hat{\mathbf{y}}_{[a,b],\tau} ``` **Parameters:** | Name | Type | Description | Default | | ------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- | | `Y_hat_df` | [Frame](#narwhals.typing.Frame) | DataFrame, base forecasts with columns \['unique\_id', 'ds'] and models to reconcile. | *required* | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | Each key is a level and its value contains tags associated to that level. | *required* | | `S_df` | [Frame](#narwhals.typing.Frame) \| [SMatrix](#hierarchicalforecast.utils.SMatrix) | DataFrame or :class:`~hierarchicalforecast.utils.SMatrix` with summing matrix of size `(base, bottom)`, see [aggregate method](./utils.html#aggregate). Passing an `SMatrix` (from `aggregate(..., sparse_s=True)`) avoids dense materialization. Default is None. | None | | `Y_df` | [Optional](#Optional)\[[Frame](#narwhals.typing.Frame)] | DataFrame, training set of base time series with columns `['unique_id', 'ds', 'y']`. If a class of `self.reconciles` receives `y_hat_insample`, `Y_df` must include them as columns. Default is None. | None | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | positive float list \[0,100), confidence levels for prediction intervals. Default is None. | None | | `intervals_method` | [str](#str) | method used to calculate prediction intervals, one of `normality`, `bootstrap`, `permbu`. Default is "normality". | 'normality' | | `num_samples` | [int](#int) | if positive return that many probabilistic coherent samples. Default is -1. | -1 | | `seed` | [int](#int) | random seed for numpy generator's replicability. Default is 0. | 0 | | `is_balanced` | [bool](#bool) | wether `Y_df` is balanced, set it to True to speed things up if `Y_df` is balanced. Default is False. | False | | `id_col` | [str](#str) | column that identifies each serie. Default is "unique\_id". | 'unique\_id' | | `time_col` | [str](#str) | column that identifies each timestep, its values can be timestamps or integers. Default is "ds". | 'ds' | | `target_col` | [str](#str) | column that contains the target. Default is "y". | 'y' | | `id_time_col` | [str](#str) | column that identifies each temporal aggregation level (required when `temporal=True`). Default is "temporal\_id". | 'temporal\_id' | | `temporal` | [bool](#bool) | if True, perform temporal reconciliation. Default is False. | False | | `diagnostics` | [bool](#bool) | if True, compute coherence diagnostics and store in `self.diagnostics`. Default is False. | False | | `diagnostics_atol` | [float](#float) | absolute tolerance for numerical coherence check. Default is 1e-6. | 1e-06 | **Returns:** | Type | Description | | ---------------------------------------------- | --------------------------------------- | | [FrameT](#narwhals.typing.FrameT) | DataFrame, with reconciled predictions. |
Note When `diagnostics=True`, after reconciliation completes, `self.diagnostics` will contain a DataFrame with coherence metrics per hierarchical level, including: * `coherence_residual_mae_before/after`: Mean absolute coherence residual before/after reconciliation * `adjustment_mae/rmse/max/mean`: Statistics on the adjustments made by reconciliation * `negative_count_before/after`: Count of negative values before/after reconciliation * `is_coherent`: Whether reconciled forecasts satisfy aggregation constraints (Overall level only) * `coherence_max_violation`: Maximum coherence violation (Overall level only)
#### `HierarchicalReconciliation.bootstrap_reconcile` ```python theme={null} bootstrap_reconcile(Y_hat_df, S_df, tags, Y_df=None, level=None, intervals_method='normality', num_samples=-1, num_seeds=1, id_col='unique_id', time_col='ds', target_col='y') ``` Bootstraped Hierarchical Reconciliation Method. Applies N times, based on different random seeds, the `reconcile` method for the different reconciliation techniques instantiated in the `reconcilers` list. **Parameters:** | Name | Type | Description | Default | | ------------------ | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `Y_hat_df` | [Frame](#narwhals.typing.Frame) | DataFrame, base forecasts with columns \['unique\_id', 'ds'] and models to reconcile. | *required* | | `S_df` | [Frame](#narwhals.typing.Frame) | DataFrame with summing matrix of size `(base, bottom)`, see [aggregate method](./utils.html#aggregate). | *required* | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | Each key is a level and its value contains tags associated to that level. | *required* | | `Y_df` | [Optional](#Optional)\[[Frame](#narwhals.typing.Frame)] | DataFrame, training set of base time series with columns `['unique_id', 'ds', 'y']`. If a class of `self.reconciles` receives `y_hat_insample`, `Y_df` must include them as columns. Default is None. | None | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | positive float list \[0,100), confidence levels for prediction intervals. Default is None. | None | | `intervals_method` | [str](#str) | method used to calculate prediction intervals, one of `normality`, `bootstrap`, `permbu`. Default is "normality". | 'normality' | | `num_samples` | [int](#int) | if positive return that many probabilistic coherent samples. Default is -1. | -1 | | `num_seeds` | [int](#int) | random seed for numpy generator's replicability. Default is 1. | 1 | | `id_col` | [str](#str) | column that identifies each serie. Default is "unique\_id". | 'unique\_id' | | `time_col` | [str](#str) | column that identifies each timestep, its values can be timestamps or integers. Default is "ds". | 'ds' | | `target_col` | [str](#str) | column that contains the target. Default is "y". | 'y' | **Returns:** | Type | Description | | ---------------------------------------------- | --------------------------------------------------- | | [FrameT](#narwhals.typing.FrameT) | DataFrame, with bootstraped reconciled predictions. | ### Example ```python theme={null} import pandas as pd from hierarchicalforecast.core import HierarchicalReconciliation from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.utils import aggregate from hierarchicalforecast.evaluation import evaluate from statsforecast.core import StatsForecast from statsforecast.models import AutoETS from utilsforecast.losses import mase, rmse from functools import partial # Load TourismSmall dataset df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') df = df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) df.insert(0, 'Country', 'Australia') qs = df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True) df['ds'] = pd.PeriodIndex(qs, freq='Q').to_timestamp() # Create hierarchical seires based on geographic levels and purpose # And Convert quarterly ds string to pd.datetime format hierarchy_levels = [['Country'], ['Country', 'State'], ['Country', 'Purpose'], ['Country', 'State', 'Region'], ['Country', 'State', 'Purpose'], ['Country', 'State', 'Region', 'Purpose']] Y_df, S_df, tags = aggregate(df=df, spec=hierarchy_levels) # Split train/test sets Y_test_df = Y_df.groupby('unique_id').tail(8) Y_train_df = Y_df.drop(Y_test_df.index) # Compute base auto-ETS predictions # Careful identifying correct data freq, this data quarterly 'Q' fcst = StatsForecast(models=[AutoETS(season_length=4, model='ZZA')], freq='QS', n_jobs=-1) Y_hat_df = fcst.forecast(df=Y_train_df, h=8, fitted=True) Y_fitted_df = fcst.forecast_fitted_values() reconcilers = [ BottomUp(), MinTrace(method='ols'), MinTrace(method='mint_shrink'), ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags) # Evaluate eval_tags = {} eval_tags['Total'] = tags['Country'] eval_tags['Purpose'] = tags['Country/Purpose'] eval_tags['State'] = tags['Country/State'] eval_tags['Regions'] = tags['Country/State/Region'] eval_tags['Bottom'] = tags['Country/State/Region/Purpose'] Y_rec_df_with_y = Y_rec_df.merge(Y_test_df, on=['unique_id', 'ds'], how='left') mase_p = partial(mase, seasonality=4) evaluation = evaluate(Y_rec_df_with_y, metrics=[mase_p, rmse], tags=eval_tags, train_df=Y_train_df) numeric_cols = evaluation.select_dtypes(include="number").columns evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format) ``` # Hierarchical Evaluation Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/evaluation.html To assist the evaluation of hierarchical forecasting systems, we make available an [`evaluate`](https://Nixtla.github.io/hierarchicalforecast/src/evaluation.html#evaluate) function that can be used in combination with loss functions from `utilsforecast.losses`. *** ### `evaluate` ```python theme={null} evaluate(df, metrics, tags, models=None, train_df=None, level=None, id_col='unique_id', time_col='ds', target_col='y', agg_fn='mean', benchmark=None) ``` Evaluate hierarchical forecast using different metrics. **Parameters:** | Name | Type | Description | Default | | ------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas, polars, dask or spark DataFrame | Forecasts to evaluate. Must have `id_col`, `time_col`, `target_col` and models' predictions. | *required* | | `metrics` | list of callable | Functions with arguments `df`, `models`, `id_col`, `target_col` and optionally `train_df`. | *required* | | `tags` | [dict](#dict) | Each key is a level in the hierarchy and its value contains tags associated to that level. Each key is a level in the hierarchy and its value contains tags associated to that level. | *required* | | `models` | list of str | Names of the models to evaluate. If `None` will use every column in the dataframe after removing id, time and target. | None | | `train_df` | pandas, polars, dask or spark DataFrame | Training set. Used to evaluate metrics such as `mase`. | None | | `level` | list of int | Prediction interval levels. Used to compute losses that rely on quantiles. | None | | `id_col` | [str](#str) | Column that identifies each serie. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. | 'y' | | `agg_fn` | [str](#str) | Statistic to compute on the scores by id to reduce them to a single number. | 'mean' | | `benchmark` | [str](#str) | If passed, evaluators are scaled by the error of this benchmark model. | None | **Returns:** | Type | Description | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [FrameT](#narwhals.typing.FrameT) | pandas, polars DataFrame: Metrics with one row per (id, metric) combination and one column per model. If `agg_fn` is not `None`, there is only one row per metric. | ### Example ```python theme={null} import pandas as pd from hierarchicalforecast.core import HierarchicalReconciliation from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.utils import aggregate from hierarchicalforecast.evaluation import evaluate from statsforecast.core import StatsForecast from statsforecast.models import AutoETS from utilsforecast.losses import mase, rmse from functools import partial # Load TourismSmall dataset df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') df = df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) df.insert(0, 'Country', 'Australia') qs = df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True) df['ds'] = pd.PeriodIndex(qs, freq='Q').to_timestamp() # Create hierarchical seires based on geographic levels and purpose # And Convert quarterly ds string to pd.datetime format hierarchy_levels = [['Country'], ['Country', 'State'], ['Country', 'Purpose'], ['Country', 'State', 'Region'], ['Country', 'State', 'Purpose'], ['Country', 'State', 'Region', 'Purpose']] Y_df, S_df, tags = aggregate(df=df, spec=hierarchy_levels) # Split train/test sets Y_test_df = Y_df.groupby('unique_id').tail(8) Y_train_df = Y_df.drop(Y_test_df.index) # Compute base auto-ETS predictions # Careful identifying correct data freq, this data quarterly 'Q' fcst = StatsForecast(models=[AutoETS(season_length=4, model='ZZA')], freq='QS', n_jobs=-1) Y_hat_df = fcst.forecast(df=Y_train_df, h=8, fitted=True) Y_fitted_df = fcst.forecast_fitted_values() reconcilers = [ BottomUp(), MinTrace(method='ols'), MinTrace(method='mint_shrink'), ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags) # Evaluate eval_tags = {} eval_tags['Total'] = tags['Country'] eval_tags['Purpose'] = tags['Country/Purpose'] eval_tags['State'] = tags['Country/State'] eval_tags['Regions'] = tags['Country/State/Region'] eval_tags['Bottom'] = tags['Country/State/Region/Purpose'] Y_rec_df_with_y = Y_rec_df.merge(Y_test_df, on=['unique_id', 'ds'], how='left') mase_p = partial(mase, seasonality=4) evaluation = evaluate(Y_rec_df_with_y, metrics=[mase_p, rmse], tags=eval_tags, train_df=Y_train_df) numeric_cols = evaluation.select_dtypes(include="number").columns evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format) ``` ### References * [Gneiting, Tilmann, and Adrian E. Raftery. (2007). "Strictly proper scoring rules, prediction and estimation". Journal of the American Statistical Association.](https://sites.stat.washington.edu/raftery/Research/PDF/Gneiting2007jasa.pdf) * [Gneiting, Tilmann. (2011). "Quantiles as optimal point forecasts". International Journal of Forecasting.](https://www.sciencedirect.com/science/article/pii/S0169207010000063) * [Spyros Makridakis, Evangelos Spiliotis, Vassilios Assimakopoulos, Zhi Chen, Anil Gaba, Ilia Tsetlin, Robert L. Winkler. (2022). "The M5 uncertainty competition: Results, findings and conclusions". International Journal of Forecasting.](https://www.sciencedirect.com/science/article/pii/S0169207021001722) * [Anastasios Panagiotelis, Puwasala Gamakumara, George Athanasopoulos, Rob J. Hyndman. (2022). "Probabilistic forecast reconciliation: Properties, evaluation and score optimisation". European Journal of Operational Research.](https://www.sciencedirect.com/science/article/pii/S0377221722006087) * [Syama Sundar Rangapuram, Lucien D Werner, Konstantinos Benidis, Pedro Mercado, Jan Gasthaus, Tim Januschowski. (2021). "End-to-End Learning of Coherent Probabilistic Forecasts for Hierarchical Time Series". Proceedings of the 38th International Conference on Machine Learning (ICML).](https://proceedings.mlr.press/v139/rangapuram21a.html) * [Kin G. Olivares, O. Nganba Meetei, Ruijun Ma, Rohan Reddy, Mengfei Cao, Lee Dicker (2022). “Probabilistic Hierarchical Forecasting with Deep Poisson Mixtures”. Submitted to the International Journal Forecasting, Working paper available at arxiv.](https://arxiv.org/pdf/2110.13179.pdf) * [Makridakis, S., Spiliotis E., and Assimakopoulos V. (2022). “M5 Accuracy Competition: Results, Findings, and Conclusions.”, International Journal of Forecasting, Volume 38, Issue 4.](https://www.sciencedirect.com/science/article/pii/S0169207021001874) # Bootstrap Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/australiandomestictourism-bootstraped-intervals.html Open In Colab In many cases, only the time series at the lowest level of the hierarchies (bottom time series) are available. `HierarchicalForecast` has tools to create time series for all hierarchies and also allows you to calculate prediction intervals for all hierarchies. In this notebook we will see how to do it. ```python theme={null} !pip install hierarchicalforecast statsforecast ``` ```python theme={null} import pandas as pd # compute base forecast no coherent from statsforecast.models import AutoETS from statsforecast.core import StatsForecast #obtain hierarchical reconciliation methods and evaluation from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.utils import aggregate, HierarchicalPlot from hierarchicalforecast.core import HierarchicalReconciliation ``` ## Aggregate bottom time series In this example we will use the [Tourism](https://otexts.com/fpp3/tourism.html) dataset from the [Forecasting: Principles and Practice](https://otexts.com/fpp3/) book. The dataset only contains the time series at the lowest level, so we need to create the time series for all hierarchies. ```python theme={null} Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') Y_df = Y_df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) Y_df.insert(0, 'Country', 'Australia') Y_df = Y_df[['Country', 'Region', 'State', 'Purpose', 'ds', 'y']] Y_df['ds'] = Y_df['ds'].str.replace(r'(\d+) (Q\d)', r'\1\2', regex=True) Y_df['ds'] = pd.PeriodIndex(Y_df["ds"], freq='Q').to_timestamp() Y_df.head() ``` | | Country | Region | State | Purpose | ds | y | | - | --------- | -------- | --------------- | -------- | ---------- | ---------- | | 0 | Australia | Adelaide | South Australia | Business | 1998-01-01 | 135.077690 | | 1 | Australia | Adelaide | South Australia | Business | 1998-04-01 | 109.987316 | | 2 | Australia | Adelaide | South Australia | Business | 1998-07-01 | 166.034687 | | 3 | Australia | Adelaide | South Australia | Business | 1998-10-01 | 127.160464 | | 4 | Australia | Adelaide | South Australia | Business | 1999-01-01 | 137.448533 | The dataset can be grouped in the following non-strictly hierarchical structure. ```python theme={null} spec = [ ['Country'], ['Country', 'State'], ['Country', 'Purpose'], ['Country', 'State', 'Region'], ['Country', 'State', 'Purpose'], ['Country', 'State', 'Region', 'Purpose'] ] ``` Using the `aggregate` function from `HierarchicalForecast` we can generate: 1. `Y_df`: the hierarchical structured series $\mathbf{y}_{[a,b]\tau}$ 2. `S_df`: the aggregation constraings dataframe with $S_{[a,b]}$ 3. `tags`: a list with the ‘unique\_ids’ conforming each aggregation level. ```python theme={null} Y_df, S_df, tags = aggregate(df=Y_df, spec=spec) ``` ```python theme={null} Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ------------ | | 0 | Australia | 1998-01-01 | 23182.197269 | | 1 | Australia | 1998-04-01 | 20323.380067 | | 2 | Australia | 1998-07-01 | 19826.640511 | | 3 | Australia | 1998-10-01 | 20830.129891 | | 4 | Australia | 1999-01-01 | 22087.353380 | ```python theme={null} S_df.iloc[:5, :5] ``` | | unique\_id | Australia/ACT/Canberra/Business | Australia/ACT/Canberra/Holiday | Australia/ACT/Canberra/Other | Australia/ACT/Canberra/Visiting | | - | ---------------------------- | ------------------------------- | ------------------------------ | ---------------------------- | ------------------------------- | | 0 | Australia | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | Australia/ACT | 1.0 | 1.0 | 1.0 | 1.0 | | 2 | Australia/New South Wales | 0.0 | 0.0 | 0.0 | 0.0 | | 3 | Australia/Northern Territory | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | Australia/Queensland | 0.0 | 0.0 | 0.0 | 0.0 | ```python theme={null} tags['Country/Purpose'] ``` ```text theme={null} array(['Australia/Business', 'Australia/Holiday', 'Australia/Other', 'Australia/Visiting'], dtype=object) ``` We can visualize the `S_df` dataframe and `Y_df` using the `HierarchicalPlot` class as follows. ```python theme={null} hplot = HierarchicalPlot(S=S_df, tags=tags) ``` ```python theme={null} hplot.plot_summing_matrix() ``` ```python theme={null} hplot.plot_hierarchically_linked_series( bottom_series='Australia/ACT/Canberra/Holiday', Y_df=Y_df ) ``` ### Split Train/Test sets We use the final two years (8 quarters) as test set. ```python theme={null} Y_test_df = Y_df.groupby('unique_id', as_index=False).tail(8) Y_train_df = Y_df.drop(Y_test_df.index) ``` ```python theme={null} Y_train_df.groupby('unique_id').size() ``` ```text theme={null} unique_id Australia 72 Australia/ACT 72 Australia/ACT/Business 72 Australia/ACT/Canberra 72 Australia/ACT/Canberra/Business 72 .. Australia/Western Australia/Experience Perth/Other 72 Australia/Western Australia/Experience Perth/Visiting 72 Australia/Western Australia/Holiday 72 Australia/Western Australia/Other 72 Australia/Western Australia/Visiting 72 Length: 425, dtype: int64 ``` ## Computing Base Forecasts The following cell computes the **base forecasts** for each time series in `Y_df` using the `AutoETS` and model. Observe that `Y_hat_df` contains the forecasts but they are not coherent. Since we are computing prediction intervals using bootstrapping, we only need the fitted values of the models. ```python theme={null} fcst = StatsForecast(models=[AutoETS(season_length=4, model='ZAA')], freq='QS', n_jobs=-1) Y_hat_df = fcst.forecast(df=Y_train_df, h=8, fitted=True) Y_fitted_df = fcst.forecast_fitted_values() ``` ## Reconcile Base Forecasts The following cell makes the previous forecasts coherent using the `HierarchicalReconciliation` class. Since the hierarchy structure is not strict, we can’t use methods such as `TopDown` or `MiddleOut`. In this example we use `BottomUp` and `MinTrace`. If you want to calculate prediction intervals, you have to use the `level` argument as follows and set `intervals_method='bootstrap'`. ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method='mint_shrink'), MinTrace(method='ols') ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags, level=[80, 90], intervals_method='bootstrap') ``` The dataframe `Y_rec_df` contains the reconciled forecasts. ```python theme={null} Y_rec_df.head() ``` | | unique\_id | ds | AutoETS | AutoETS/BottomUp | AutoETS/BottomUp-lo-90 | AutoETS/BottomUp-lo-80 | AutoETS/BottomUp-hi-80 | AutoETS/BottomUp-hi-90 | AutoETS/MinTrace\_method-mint\_shrink | AutoETS/MinTrace\_method-mint\_shrink-lo-90 | AutoETS/MinTrace\_method-mint\_shrink-lo-80 | AutoETS/MinTrace\_method-mint\_shrink-hi-80 | AutoETS/MinTrace\_method-mint\_shrink-hi-90 | AutoETS/MinTrace\_method-ols | AutoETS/MinTrace\_method-ols-lo-90 | AutoETS/MinTrace\_method-ols-lo-80 | AutoETS/MinTrace\_method-ols-hi-80 | AutoETS/MinTrace\_method-ols-hi-90 | | - | ---------- | ---------- | ------------ | ---------------- | ---------------------- | ---------------------- | ---------------------- | ---------------------- | ------------------------------------- | ------------------------------------------- | ------------------------------------------- | ------------------------------------------- | ------------------------------------------- | ---------------------------- | ---------------------------------- | ---------------------------------- | ---------------------------------- | ---------------------------------- | | 0 | Australia | 2016-01-01 | 26080.878488 | 24487.152503 | 23242.757311 | 23332.592968 | 25379.829486 | 25424.139137 | 25521.551706 | 24407.442712 | 24698.931479 | 26357.024354 | 26466.740682 | 26034.132091 | 24914.199038 | 25100.470502 | 27102.746065 | 27176.467048 | | 1 | Australia | 2016-04-01 | 24587.012115 | 23068.314292 | 21823.919100 | 21910.615057 | 23945.982949 | 24278.683243 | 24106.522479 | 23185.403634 | 23283.902251 | 25098.332342 | 25473.239949 | 24567.457913 | 23483.983814 | 23640.627126 | 25709.792870 | 25809.220444 | | 2 | Australia | 2016-07-01 | 24147.307744 | 22686.983933 | 21293.529449 | 21526.525610 | 23697.859931 | 24150.879789 | 23717.610501 | 22603.501507 | 22802.771308 | 24802.973260 | 25228.795629 | 24150.111246 | 23030.178193 | 23154.972436 | 25359.917993 | 25404.792198 | | 3 | Australia | 2016-10-01 | 24794.040779 | 23428.037637 | 22034.583153 | 22273.826957 | 24241.840440 | 24438.913635 | 24472.939115 | 23361.285512 | 23584.825871 | 25338.713995 | 25469.426623 | 24831.540721 | 23725.927463 | 23836.401911 | 25900.154695 | 25977.249268 | | 4 | Australia | 2017-01-01 | 26283.998654 | 24939.637616 | 23695.217554 | 23903.395713 | 25815.638682 | 25973.164607 | 26029.322724 | 24948.339795 | 25144.179030 | 26900.068461 | 27119.073160 | 26348.229758 | 25254.682234 | 25487.518098 | 27410.894158 | 27477.330557 | ## Plot Predictions Then we can plot the probabilist forecasts using the following function. ```python theme={null} plot_df = Y_df.merge(Y_rec_df, on=['unique_id', 'ds'], how="outer") ``` ### Plot single time series ```python theme={null} hplot.plot_series( series='Australia', Y_df=plot_df, models=['y', 'AutoETS', 'AutoETS/MinTrace_method-ols', 'AutoETS/MinTrace_method-mint_shrink'], level=[80] ) ``` ```python theme={null} # Since we are plotting a bottom time series # the probabilistic and mean forecasts # differ due to bootstrapping hplot.plot_series( series='Australia/Western Australia/Experience Perth/Visiting', Y_df=plot_df, models=['y', 'AutoETS', 'AutoETS/BottomUp'], level=[80] ) ``` ### Plot hierarchichally linked time series ```python theme={null} hplot.plot_hierarchically_linked_series( bottom_series='Australia/Western Australia/Experience Perth/Visiting', Y_df=plot_df, models=['y', 'AutoETS', 'AutoETS/MinTrace_method-ols', 'AutoETS/BottomUp'], level=[80] ) ``` ```python theme={null} # ACT only has Canberra hplot.plot_hierarchically_linked_series( bottom_series='Australia/ACT/Canberra/Other', Y_df=plot_df, models=['y', 'AutoETS/MinTrace_method-mint_shrink'], level=[80, 90] ) ``` ### References * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) * [Shanika L. Wickramasuriya, George Athanasopoulos, and Rob J. Hyndman. Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization.Journal of the American Statistical Association, 114(526):804–819, 2019. doi: 10.1080/01621459.2018.1448825. URL https://robjhyndman.com/publications/mint/.](https://robjhyndman.com/publications/mint/) * [Puwasala Gamakumara Ph. D. dissertation. Monash University, Econometrics and Business Statistics (2020). “Probabilistic Forecast Reconciliation”](https://bridges.monash.edu/articles/thesis/Probabilistic_Forecast_Reconciliation_Theory_and_Applications/11869533) # Normality Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/australiandomestictourism-intervals.html Open In Colab In many cases, only the time series at the lowest level of the hierarchies (bottom time series) are available. `HierarchicalForecast` has tools to create time series for all hierarchies and also allows you to calculate prediction intervals for all hierarchies. In this notebook we will see how to do it. ```python theme={null} !pip install hierarchicalforecast statsforecast ``` ```python theme={null} import pandas as pd # compute base forecast no coherent from statsforecast.models import AutoARIMA from statsforecast.core import StatsForecast #obtain hierarchical reconciliation methods and evaluation from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.utils import aggregate, HierarchicalPlot from hierarchicalforecast.core import HierarchicalReconciliation ``` ## Aggregate bottom time series In this example we will use the [Tourism](https://otexts.com/fpp3/tourism.html) dataset from the [Forecasting: Principles and Practice](https://otexts.com/fpp3/) book. The dataset only contains the time series at the lowest level, so we need to create the time series for all hierarchies. ```python theme={null} Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') Y_df = Y_df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) Y_df.insert(0, 'Country', 'Australia') Y_df = Y_df[['Country', 'Region', 'State', 'Purpose', 'ds', 'y']] Y_df['ds'] = Y_df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True) Y_df['ds'] = pd.PeriodIndex(Y_df["ds"], freq='Q').to_timestamp() Y_df.head() ``` | | Country | Region | State | Purpose | ds | y | | - | --------- | -------- | --------------- | -------- | ---------- | ---------- | | 0 | Australia | Adelaide | South Australia | Business | 1998-01-01 | 135.077690 | | 1 | Australia | Adelaide | South Australia | Business | 1998-04-01 | 109.987316 | | 2 | Australia | Adelaide | South Australia | Business | 1998-07-01 | 166.034687 | | 3 | Australia | Adelaide | South Australia | Business | 1998-10-01 | 127.160464 | | 4 | Australia | Adelaide | South Australia | Business | 1999-01-01 | 137.448533 | The dataset can be grouped in the following non-strictly hierarchical structure. ```python theme={null} spec = [ ['Country'], ['Country', 'State'], ['Country', 'Purpose'], ['Country', 'State', 'Region'], ['Country', 'State', 'Purpose'], ['Country', 'State', 'Region', 'Purpose'] ] ``` Using the `aggregate` function from `HierarchicalForecast` we can generate: 1. `Y_df`: the hierarchical structured series $\mathbf{y}_{[a,b]\tau}$ 2. `S_df`: the aggregation constraings dataframe with $S_{[a,b]}$ 3. `tags`: a list with the ‘unique\_ids’ conforming each aggregation level. ```python theme={null} Y_df, S_df, tags = aggregate(df=Y_df, spec=spec) ``` ```python theme={null} Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ------------ | | 0 | Australia | 1998-01-01 | 23182.197269 | | 1 | Australia | 1998-04-01 | 20323.380067 | | 2 | Australia | 1998-07-01 | 19826.640511 | | 3 | Australia | 1998-10-01 | 20830.129891 | | 4 | Australia | 1999-01-01 | 22087.353380 | ```python theme={null} S_df.iloc[:5, :5] ``` | | unique\_id | Australia/ACT/Canberra/Business | Australia/ACT/Canberra/Holiday | Australia/ACT/Canberra/Other | Australia/ACT/Canberra/Visiting | | - | ---------------------------- | ------------------------------- | ------------------------------ | ---------------------------- | ------------------------------- | | 0 | Australia | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | Australia/ACT | 1.0 | 1.0 | 1.0 | 1.0 | | 2 | Australia/New South Wales | 0.0 | 0.0 | 0.0 | 0.0 | | 3 | Australia/Northern Territory | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | Australia/Queensland | 0.0 | 0.0 | 0.0 | 0.0 | ```python theme={null} tags['Country/Purpose'] ``` ```text theme={null} array(['Australia/Business', 'Australia/Holiday', 'Australia/Other', 'Australia/Visiting'], dtype=object) ``` We can visualize the `S` matrix and the data using the `HierarchicalPlot` class as follows. ```python theme={null} hplot = HierarchicalPlot(S=S_df, tags=tags) ``` ```python theme={null} hplot.plot_summing_matrix() ``` ```python theme={null} hplot.plot_hierarchically_linked_series( bottom_series='Australia/ACT/Canberra/Holiday', Y_df=Y_df ) ``` ### Split Train/Test sets We use the final two years (8 quarters) as test set. ```python theme={null} Y_test_df = Y_df.groupby('unique_id', as_index=False).tail(8) Y_train_df = Y_df.drop(Y_test_df.index) ``` ```python theme={null} Y_train_df.groupby('unique_id').size() ``` ```text theme={null} unique_id Australia 72 Australia/ACT 72 Australia/ACT/Business 72 Australia/ACT/Canberra 72 Australia/ACT/Canberra/Business 72 .. Australia/Western Australia/Experience Perth/Other 72 Australia/Western Australia/Experience Perth/Visiting 72 Australia/Western Australia/Holiday 72 Australia/Western Australia/Other 72 Australia/Western Australia/Visiting 72 Length: 425, dtype: int64 ``` ## Computing base forecasts The following cell computes the **base forecasts** for each time series in `Y_df` using the `AutoARIMA` and model. Observe that `Y_hat_df` contains the forecasts but they are not coherent. To reconcile the prediction intervals we need to calculate the uncoherent intervals using the `level` argument of `StatsForecast`. ```python theme={null} fcst = StatsForecast(models=[AutoARIMA(season_length=4)], freq='QS', n_jobs=-1) Y_hat_df = fcst.forecast(df=Y_train_df, h=8, fitted=True, level=[80, 90]) Y_fitted_df = fcst.forecast_fitted_values() ``` ## Reconcile forecasts The following cell makes the previous forecasts coherent using the `HierarchicalReconciliation` class. Since the hierarchy structure is not strict, we can’t use methods such as `TopDown` or `MiddleOut`. In this example we use `BottomUp` and `MinTrace`. If you want to calculate prediction intervals, you have to use the `level` argument as follows. ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method='mint_shrink'), MinTrace(method='ols') ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags, level=[80, 90]) ``` The dataframe `Y_rec_df` contains the reconciled forecasts. ```python theme={null} Y_rec_df.head() ``` | | unique\_id | ds | AutoARIMA | AutoARIMA-lo-90 | AutoARIMA-lo-80 | AutoARIMA-hi-80 | AutoARIMA-hi-90 | AutoARIMA/BottomUp | AutoARIMA/BottomUp-lo-90 | AutoARIMA/BottomUp-lo-80 | ... | AutoARIMA/MinTrace\_method-mint\_shrink | AutoARIMA/MinTrace\_method-mint\_shrink-lo-90 | AutoARIMA/MinTrace\_method-mint\_shrink-lo-80 | AutoARIMA/MinTrace\_method-mint\_shrink-hi-80 | AutoARIMA/MinTrace\_method-mint\_shrink-hi-90 | AutoARIMA/MinTrace\_method-ols | AutoARIMA/MinTrace\_method-ols-lo-90 | AutoARIMA/MinTrace\_method-ols-lo-80 | AutoARIMA/MinTrace\_method-ols-hi-80 | AutoARIMA/MinTrace\_method-ols-hi-90 | | - | ---------- | ---------- | ------------ | --------------- | --------------- | --------------- | --------------- | ------------------ | ------------------------ | ------------------------ | --- | --------------------------------------- | --------------------------------------------- | --------------------------------------------- | --------------------------------------------- | --------------------------------------------- | ------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ | | 0 | Australia | 2016-01-01 | 26212.553553 | 24705.948180 | 25038.715077 | 27386.392029 | 27719.158927 | 24646.517084 | 23983.656843 | 24130.064091 | ... | 25267.797338 | 24491.630618 | 24663.064091 | 25872.530586 | 26043.964058 | 26082.753488 | 25010.876141 | 25247.623803 | 26917.883174 | 27154.630835 | | 1 | Australia | 2016-04-01 | 25033.667125 | 23337.267588 | 23711.954696 | 26355.379554 | 26730.066662 | 22942.957703 | 22229.916838 | 22387.407579 | ... | 23836.804444 | 23002.620214 | 23186.868128 | 24486.740760 | 24670.988674 | 24822.102094 | 23616.734393 | 23882.966332 | 25761.237857 | 26027.469796 | | 2 | Australia | 2016-07-01 | 24507.027198 | 22640.028798 | 23052.396413 | 25961.657983 | 26374.025599 | 22568.286488 | 21805.892199 | 21974.283728 | ... | 23294.240908 | 22410.719833 | 22605.864873 | 23982.616942 | 24177.761983 | 24269.578724 | 22944.380043 | 23237.079287 | 25302.078162 | 25594.777406 | | 3 | Australia | 2016-10-01 | 25598.928613 | 23575.665243 | 24022.547410 | 27175.309816 | 27622.191983 | 23113.075726 | 22308.671860 | 22486.342127 | ... | 24154.484487 | 23221.706185 | 23427.730766 | 24881.238208 | 25087.262790 | 25340.549923 | 23905.434070 | 24222.410936 | 26458.688911 | 26775.665777 | | 4 | Australia | 2017-01-01 | 26982.576796 | 24669.535238 | 25180.421285 | 28784.732308 | 29295.618354 | 23779.264921 | 22874.194227 | 23074.098975 | ... | 25155.001372 | 24125.268915 | 24352.707952 | 25957.294793 | 26184.733830 | 26690.200927 | 25051.352698 | 25413.328335 | 27967.073518 | 28329.049155 | ## Plot forecasts Then we can plot the probabilistic forecasts using the following function. ```python theme={null} plot_df = Y_df.merge(Y_rec_df, on=['unique_id', 'ds'], how="outer") ``` ### Plot single time series ```python theme={null} hplot.plot_series( series='Australia', Y_df=plot_df, models=['y', 'AutoARIMA', 'AutoARIMA/MinTrace_method-ols'], level=[80] ) ``` ```python theme={null} # Since we are plotting a bottom time series # the probabilistic and mean forecasts # are the same hplot.plot_series( series='Australia/Western Australia/Experience Perth/Visiting', Y_df=plot_df, models=['y', 'AutoARIMA', 'AutoARIMA/BottomUp'], level=[80] ) ``` ### Plot hierarchichally linked time series ```python theme={null} hplot.plot_hierarchically_linked_series( bottom_series='Australia/Western Australia/Experience Perth/Visiting', Y_df=plot_df, models=['y', 'AutoARIMA', 'AutoARIMA/MinTrace_method-ols', 'AutoARIMA/BottomUp'], level=[80] ) ``` ```python theme={null} # ACT only has Canberra hplot.plot_hierarchically_linked_series( bottom_series='Australia/ACT/Canberra/Other', Y_df=plot_df, models=['y', 'AutoARIMA/MinTrace_method-mint_shrink'], level=[80, 90] ) ``` ### References * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) * [Shanika L. Wickramasuriya, George Athanasopoulos, and Rob J. Hyndman. Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization.Journal of the American Statistical Association, 114(526):804–819, 2019. doi: 10.1080/01621459.2018.1448825. URL https://robjhyndman.com/publications/mint/.](https://robjhyndman.com/publications/mint/) # Multi-model Aggregation Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/australiandomestictourism-multimodel.html > Geographical Hierarchical Forecasting on Australian Tourism Data using > multiple models for each level in the hierarchy. This notebook extends the classic Australian Domestic Tourism (`Tourism`) geographical aggregation example to showcase how `HierarchicalForecast` can be used to produce coherent forecasts when **different forecasting models are applied at each level of the hierarchy**. We will use the `Tourism` dataset, which contains monthly time series of the number of visitors to each state of Australia. Specifically, we will demonstrate fitting a diverse set of models across the hierarchical levels. This includes statistical models like `AutoETS` from `StatsForecast`, machine learning models such as `HistGradientBoostingRegressor` using `MLForecast`, and neural network models like `NBEATS` from `NeuralForecast`. After generating these base forecasts, we will reconcile them using `BottomUp`, `MinTrace(mint_shrink)`, `TopDown(forecast_proportions)` reconciliators from `HierarchicalForecast`. You can run these experiments using CPU or GPU with Google Colab. Open In Colab ```python theme={null} !pip install hierarchicalforecast statsforecast mlforecast datasetsforecast neuralforecast ``` ## 1. Load and Process Data In this example we will use the [Tourism](https://otexts.com/fpp3/tourism.html) dataset from the [Forecasting: Principles and Practice](https://otexts.com/fpp3/) book. The dataset only contains the time series at the lowest level, so we need to create the time series for all hierarchies. ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') Y_df = Y_df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) Y_df.insert(0, 'Country', 'Australia') Y_df = Y_df[['Country', 'Region', 'State', 'ds', 'y']] Y_df['ds'] = Y_df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True) Y_df['ds'] = pd.PeriodIndex(Y_df['ds'], freq='Q').to_timestamp() Y_df_first = Y_df.groupby(['Country', 'Region', 'State', 'ds'], as_index=False).agg({'y':'sum'}) Y_df_first.head() ``` | | Country | Region | State | ds | y | | - | --------- | -------- | --------------- | ---------- | ---------- | | 0 | Australia | Adelaide | South Australia | 1998-01-01 | 658.553895 | | 1 | Australia | Adelaide | South Australia | 1998-04-01 | 449.853935 | | 2 | Australia | Adelaide | South Australia | 1998-07-01 | 592.904597 | | 3 | Australia | Adelaide | South Australia | 1998-10-01 | 524.242760 | | 4 | Australia | Adelaide | South Australia | 1999-01-01 | 548.394105 | The dataset can be grouped in the following hierarchical structure. ```python theme={null} spec = [ ['Country'], ['Country', 'State'], ['Country', 'State', 'Region'] ] ``` Using the `aggregate` function from `HierarchicalForecast` we can get the full set of time series. ```python theme={null} from hierarchicalforecast.utils import aggregate ``` ```python theme={null} Y_df, S_df, tags = aggregate(Y_df_first, spec) ``` ### Split Train/Test sets We use the final two years (8 quarters) as test set. ```python theme={null} Y_test_df = Y_df.groupby('unique_id', as_index=False).tail(8) Y_train_df = Y_df.drop(Y_test_df.index) ``` ## 2. Computing different models for different hierarchies In this section, we illustrate how to fit a different type of model for each level of the hierarchy. In particular, for each level, we will fit the following models: * **Country**: `AutoETS` model from `StatsForecast`. * **Country/State**: `HistGradientBoostingRegressor` model from `scikit-learn` through the `MLForecast` API. * **Country/State/Region**: `NBEATS` model from `NeuralForecast`. ```python theme={null} from statsforecast.core import StatsForecast from statsforecast.models import AutoETS from mlforecast import MLForecast from sklearn.ensemble import HistGradientBoostingRegressor from neuralforecast import NeuralForecast from neuralforecast.models import NBEATS ``` This `fit_predict_any_models` function is a helper function for training and forecasting with models from `StatsForecast`, `MLForecast`, and `NeuralForecast`. ```python theme={null} def fit_predict_any_models(models, df, h): if isinstance(models, StatsForecast): yhat = models.forecast(df=df, h=h, fitted=True) yfitted = models.forecast_fitted_values() elif isinstance(models, MLForecast): models.fit(df, fitted=True) yhat = models.predict(new_df=df, h=h) yfitted = models.forecast_fitted_values() elif isinstance(models, NeuralForecast): models.fit(df=df, val_size=h) yhat = models.predict() yfitted = models.predict_insample(step_size=h) yfitted = yfitted.drop(columns=['cutoff']) else: raise ValueError("Model is not a StatsForecast, MLForecast or NeuralForecast object.") return yhat, yfitted ``` We now define the models that we want to use. ```python theme={null} h = 8 stat_models = StatsForecast(models=[AutoETS(season_length=4, model='ZZA')], freq='QS', n_jobs=-1) ml_models = MLForecast(models = [HistGradientBoostingRegressor()], freq='QS', lags=[1, 4]) neural_models = NeuralForecast(models=[NBEATS(h=h, input_size=16)],freq='QS') ``` We have defined a hierarchy consisting of three levels. We will use the different model types for each of the levels in the hierarchy. ```python theme={null} models = { 'Country': stat_models, 'Country/State': ml_models, 'Country/State/Region': neural_models } ``` To fit each model and create forecasts with it, we loop over the timeseries that are present in each level of the hierarchy, using the `tags` we created earlier using the `aggregate` function. ```python theme={null} Y_hat = [] Y_fitted = [] # We loop through the tags to fit and predict for each level of the hierarchy. for key, value in tags.items(): # We filter the training dataframe for the current level of the hierarchy. df_level = Y_train_df.query('unique_id.isin(@value)') # We fit and predict using the corresponding model for the current level. yhat_level, yfitted_level = fit_predict_any_models(models[key], df_level, h=h) # We add the predictions for this level Y_hat.append(yhat_level) Y_fitted.append(yfitted_level) # Concatenate the predictions for all levels into a single DataFrame Y_hat_df = pd.concat(Y_hat, ignore_index=True) Y_fitted_df = pd.concat(Y_fitted, ignore_index=True) ``` We have now created forecasts for different levels of the hierarchy, using different model types. Let’s look at the forecasts. ```python theme={null} Y_hat_df.head(10) ``` | | unique\_id | ds | AutoETS | HistGradientBoostingRegressor | NBEATS | | - | ------------- | ---------- | ------------ | ----------------------------- | ------ | | 0 | Australia | 2016-01-01 | 25990.068004 | NaN | NaN | | 1 | Australia | 2016-04-01 | 24458.490282 | NaN | NaN | | 2 | Australia | 2016-07-01 | 23974.055984 | NaN | NaN | | 3 | Australia | 2016-10-01 | 24563.454495 | NaN | NaN | | 4 | Australia | 2017-01-01 | 25990.068004 | NaN | NaN | | 5 | Australia | 2017-04-01 | 24458.490282 | NaN | NaN | | 6 | Australia | 2017-07-01 | 23974.055984 | NaN | NaN | | 7 | Australia | 2017-10-01 | 24563.454495 | NaN | NaN | | 8 | Australia/ACT | 2016-01-01 | NaN | 571.433902 | NaN | | 9 | Australia/ACT | 2016-04-01 | NaN | 548.060532 | NaN | As you can see, `AutoETS` only has entries for the `unique_id=Australia`, which is because we only created forecasts for the level `Country` using `AutoETS`. Secondly, we also only have forecasts using `HistGradientBoostingRegressor` for timeseries in the level `Country/State`, again as we only created forecasts for the level `Country/State` using `HistGradientBoostingRegressor`. Finally, `NBEATS` shows no forecasts at all in this view, but when we look at the tail of the predictions we see that `NBEATS` only has forecasts for the level `Country/State/Region`, which was also what we intended to create. ```python theme={null} Y_hat_df.tail(10) ``` | | unique\_id | ds | AutoETS | HistGradientBoostingRegressor | NBEATS | | --- | ------------------------------------------------- | ---------- | ------- | ----------------------------- | ----------- | | 670 | Australia/Western Australia/Australia's South ... | 2017-07-01 | NaN | NaN | 416.720154 | | 671 | Australia/Western Australia/Australia's South ... | 2017-10-01 | NaN | NaN | 605.681030 | | 672 | Australia/Western Australia/Experience Perth | 2016-01-01 | NaN | NaN | 1139.827393 | | 673 | Australia/Western Australia/Experience Perth | 2016-04-01 | NaN | NaN | 1017.152527 | | 674 | Australia/Western Australia/Experience Perth | 2016-07-01 | NaN | NaN | 917.289673 | | 675 | Australia/Western Australia/Experience Perth | 2016-10-01 | NaN | NaN | 1141.263062 | | 676 | Australia/Western Australia/Experience Perth | 2017-01-01 | NaN | NaN | 1134.063477 | | 677 | Australia/Western Australia/Experience Perth | 2017-04-01 | NaN | NaN | 1021.346558 | | 678 | Australia/Western Australia/Experience Perth | 2017-07-01 | NaN | NaN | 839.628418 | | 679 | Australia/Western Australia/Experience Perth | 2017-10-01 | NaN | NaN | 972.161499 | ## 3. Reconcile forecasts First, we need to make sure we have one forecast column containing all the forecasts across all the levels, as we want to reconcile the forecasts across the levels. We do so by taking the mean across the forecast columns. In this case, because there’s only a single entry for each unique\_id, it would be equivalent to just combine or sum the forecast columns. However, you might want to use more than one model *per level* in the hierarchy. In that case, you’d need to think about how to ensemble the multiple forecasts - a simple mean ensemble generally works well in those cases, so you can directly use the below code also for the more complex case where you have multiple models for each level. ```python theme={null} forecast_cols = [col for col in Y_hat_df.columns if col not in ['unique_id', 'ds', 'y']] Y_hat_df["all_forecasts"] = Y_hat_df[forecast_cols].mean(axis=1) Y_fitted_df["all_forecasts"] = Y_fitted_df[forecast_cols].mean(axis=1) ``` As we can see, we now have a single column `all_forecasts` that includes the forecasts across all the levels: ```python theme={null} Y_hat_df.head(10) ``` | | unique\_id | ds | AutoETS | HistGradientBoostingRegressor | NBEATS | all\_forecasts | | - | ------------- | ---------- | ------------ | ----------------------------- | ------ | -------------- | | 0 | Australia | 2016-01-01 | 25990.068004 | NaN | NaN | 25990.068004 | | 1 | Australia | 2016-04-01 | 24458.490282 | NaN | NaN | 24458.490282 | | 2 | Australia | 2016-07-01 | 23974.055984 | NaN | NaN | 23974.055984 | | 3 | Australia | 2016-10-01 | 24563.454495 | NaN | NaN | 24563.454495 | | 4 | Australia | 2017-01-01 | 25990.068004 | NaN | NaN | 25990.068004 | | 5 | Australia | 2017-04-01 | 24458.490282 | NaN | NaN | 24458.490282 | | 6 | Australia | 2017-07-01 | 23974.055984 | NaN | NaN | 23974.055984 | | 7 | Australia | 2017-10-01 | 24563.454495 | NaN | NaN | 24563.454495 | | 8 | Australia/ACT | 2016-01-01 | NaN | 571.433902 | NaN | 571.433902 | | 9 | Australia/ACT | 2016-04-01 | NaN | 548.060532 | NaN | 548.060532 | We are now ready to make the forecasts coherent using the `HierarchicalReconciliation` class. In this example we use `BottomUp`, `MinTrace(mint_shrink)`, `TopDown(forecast_proportions)` reconcilers. ```python theme={null} from hierarchicalforecast.methods import BottomUp, MinTrace, TopDown from hierarchicalforecast.core import HierarchicalReconciliation ``` ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method='mint_shrink'), TopDown(method='forecast_proportions') ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df[["unique_id", "ds", "all_forecasts"]], Y_df=Y_fitted_df[["unique_id", "ds", "y", "all_forecasts"]], S_df=S_df, tags=tags) ``` The dataframe `Y_rec_df` contains the reconciled forecasts. ```python theme={null} Y_rec_df.head() ``` | | unique\_id | ds | all\_forecasts | all\_forecasts/BottomUp | all\_forecasts/MinTrace\_method-mint\_shrink | all\_forecasts/TopDown\_method-forecast\_proportions | | - | ---------- | ---------- | -------------- | ----------------------- | -------------------------------------------- | ---------------------------------------------------- | | 0 | Australia | 2016-01-01 | 25990.068004 | 24916.914513 | 25959.517939 | 25990.068004 | | 1 | Australia | 2016-04-01 | 24458.490282 | 22867.133526 | 24656.012177 | 24458.490282 | | 2 | Australia | 2016-07-01 | 23974.055984 | 22845.050221 | 24933.182437 | 23974.055984 | | 3 | Australia | 2016-10-01 | 24563.454495 | 23901.916314 | 26382.869677 | 24563.454495 | | 4 | Australia | 2017-01-01 | 25990.068004 | 25246.089151 | 26923.282464 | 25990.068004 | ## 4. Evaluation The `HierarchicalForecast` package includes an `evaluate` function to evaluate the different hierarchies. To evaluate models we use `mase` metric and compare it to base predictions. ```python theme={null} from hierarchicalforecast.evaluation import evaluate from utilsforecast.losses import mase from functools import partial ``` ```python theme={null} eval_tags = {} eval_tags['Total'] = tags['Country'] eval_tags['State'] = tags['Country/State'] eval_tags['Regions'] = tags['Country/State/Region'] df = Y_rec_df.merge(Y_test_df, on=['unique_id', 'ds']) evaluation = evaluate(df = df, tags = eval_tags, train_df = Y_train_df, metrics = [partial(mase, seasonality=4)]) ``` ```python theme={null} evaluation ``` | | level | metric | all\_forecasts | all\_forecasts/BottomUp | all\_forecasts/MinTrace\_method-mint\_shrink | all\_forecasts/TopDown\_method-forecast\_proportions | | - | ------- | ------ | -------------- | ----------------------- | -------------------------------------------- | ---------------------------------------------------- | | 0 | Total | mase | 1.589074 | 3.002085 | 0.440261 | 1.589074 | | 1 | State | mase | 2.166374 | 1.905035 | 1.882345 | 2.361169 | | 2 | Regions | mase | 1.342429 | 1.342429 | 1.423867 | 1.458773 | | 3 | Overall | mase | 1.422878 | 1.414905 | 1.455446 | 1.545237 | We find that: * **No Single Best Method**: The results indicate that there is no universally superior reconciliation method. The optimal choice depends on which level of the hierarchy is most important. * **MinTrace for Country and Country/State**: The `MinTrace(mint_shrink)` reconciler shows best performance for the upper levels of the hierarchy, reducing the MASE from 1.59 (base forecast) to just 0.44. * **BottomUp for Country/State/Region and Overall**: The `BottomUp` method preserves only the NBEATS forecast of the most granular **Country/State/Regions** level, and aggregates those forecasts for the upper levels. It yields the **best Overall MASE score**. ## 6. Recap This notebook demonstrated the power and flexibility of HierarchicalForecast in a multi-model forecasting scenario. In this example we fitted: * `StatsForecast` with `AutoETS` model for the **Country** level. * `MLForecast` with `HistGradientBoostingRegressor` model for the **Country/State** level. * `NeuralForecast` with `NBEATS` model for the **Country/State/Region** level. We then combined the results into a single prediction. For the reconciliation of the forecasts, we used `HierarchicalReconciliation` with three different methods: * `BottomUp` * `MinTrace(method='mint_shrink')` * `TopDown(method='forecast_proportions')` Finally, we evaluated the performance of these reconciliation methods. # PERMBU Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/australiandomestictourism-permbu-intervals.html Open In Colab In many cases, only the time series at the lowest level of the hierarchies (bottom time series) are available. `HierarchicalForecast` has tools to create time series for all hierarchies and also allows you to calculate prediction intervals for all hierarchies. In this notebook we will see how to do it. ```python theme={null} !pip install hierarchicalforecast statsforecast ``` ```python theme={null} import pandas as pd # compute base forecast no coherent from statsforecast.models import AutoARIMA from statsforecast.core import StatsForecast #obtain hierarchical reconciliation methods and evaluation from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.utils import aggregate, HierarchicalPlot from hierarchicalforecast.core import HierarchicalReconciliation ``` ## Aggregate bottom time series In this example we will use the [Tourism](https://otexts.com/fpp3/tourism.html) dataset from the [Forecasting: Principles and Practice](https://otexts.com/fpp3/) book. The dataset only contains the time series at the lowest level, so we need to create the time series for all hierarchies. ```python theme={null} Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') Y_df = Y_df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) Y_df.insert(0, 'Country', 'Australia') Y_df = Y_df[['Country', 'Region', 'State', 'Purpose', 'ds', 'y']] Y_df['ds'] = Y_df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True) Y_df['ds'] = pd.PeriodIndex(Y_df["ds"], freq='Q').to_timestamp() Y_df.head() ``` | | Country | Region | State | Purpose | ds | y | | - | --------- | -------- | --------------- | -------- | ---------- | ---------- | | 0 | Australia | Adelaide | South Australia | Business | 1998-01-01 | 135.077690 | | 1 | Australia | Adelaide | South Australia | Business | 1998-04-01 | 109.987316 | | 2 | Australia | Adelaide | South Australia | Business | 1998-07-01 | 166.034687 | | 3 | Australia | Adelaide | South Australia | Business | 1998-10-01 | 127.160464 | | 4 | Australia | Adelaide | South Australia | Business | 1999-01-01 | 137.448533 | The dataset can be grouped in the following strictly hierarchical structure. ```python theme={null} spec = [ ['Country'], ['Country', 'State'], ['Country', 'State', 'Region'] ] ``` Using the `aggregate` function from `HierarchicalForecast` we can get the full set of time series. ```python theme={null} Y_df, S_df, tags = aggregate(df=Y_df, spec=spec) ``` ```python theme={null} Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ------------ | | 0 | Australia | 1998-01-01 | 23182.197269 | | 1 | Australia | 1998-04-01 | 20323.380067 | | 2 | Australia | 1998-07-01 | 19826.640511 | | 3 | Australia | 1998-10-01 | 20830.129891 | | 4 | Australia | 1999-01-01 | 22087.353380 | ```python theme={null} S_df.iloc[:5, :5] ``` | | unique\_id | Australia/ACT/Canberra | Australia/New South Wales/Blue Mountains | Australia/New South Wales/Capital Country | Australia/New South Wales/Central Coast | | - | ---------------------------- | ---------------------- | ---------------------------------------- | ----------------------------------------- | --------------------------------------- | | 0 | Australia | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | Australia/ACT | 1.0 | 0.0 | 0.0 | 0.0 | | 2 | Australia/New South Wales | 0.0 | 1.0 | 1.0 | 1.0 | | 3 | Australia/Northern Territory | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | Australia/Queensland | 0.0 | 0.0 | 0.0 | 0.0 | ```python theme={null} tags['Country/State'] ``` ```text theme={null} array(['Australia/ACT', 'Australia/New South Wales', 'Australia/Northern Territory', 'Australia/Queensland', 'Australia/South Australia', 'Australia/Tasmania', 'Australia/Victoria', 'Australia/Western Australia'], dtype=object) ``` We can visualize the `S` matrix and the data using the `HierarchicalPlot` class as follows. ```python theme={null} hplot = HierarchicalPlot(S=S_df, tags=tags) ``` ```python theme={null} hplot.plot_summing_matrix() ``` ```python theme={null} hplot.plot_hierarchically_linked_series( bottom_series='Australia/ACT/Canberra', Y_df=Y_df ) ``` ### Split Train/Test sets We use the final two years (8 quarters) as test set. ```python theme={null} Y_test_df = Y_df.groupby('unique_id', as_index=False).tail(8) Y_train_df = Y_df.drop(Y_test_df.index) ``` ```python theme={null} Y_train_df.groupby('unique_id').size() ``` ```text theme={null} unique_id Australia 72 Australia/ACT 72 Australia/ACT/Canberra 72 Australia/New South Wales 72 Australia/New South Wales/Blue Mountains 72 .. Australia/Western Australia/Australia's Coral Coast 72 Australia/Western Australia/Australia's Golden Outback 72 Australia/Western Australia/Australia's North West 72 Australia/Western Australia/Australia's South West 72 Australia/Western Australia/Experience Perth 72 Length: 85, dtype: int64 ``` ## Computing base forecasts The following cell computes the **base forecasts** for each time series in `Y_df` using the `AutoARIMA` and model. Observe that `Y_hat_df` contains the forecasts but they are not coherent. To reconcile the prediction intervals we need to calculate the uncoherent intervals using the `level` argument of `StatsForecast`. ```python theme={null} fcst = StatsForecast(models=[AutoARIMA(season_length=4)], freq='QS', n_jobs=-1) Y_hat_df = fcst.forecast(df=Y_train_df, h=8, fitted=True, level=[80, 90]) Y_fitted_df = fcst.forecast_fitted_values() ``` ## Reconcile forecasts and compute prediction intervals using PERMBU The following cell makes the previous forecasts coherent using the `HierarchicalReconciliation` class. In this example we use `BottomUp` and `MinTrace`. If you want to calculate prediction intervals, you have to use the `level` argument as follows and also `intervals_method='permbu'`. ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method='mint_shrink'), MinTrace(method='ols') ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags, level=[80, 90], intervals_method='permbu') ``` The dataframe `Y_rec_df` contains the reconciled forecasts. ```python theme={null} Y_rec_df.head() ``` | | unique\_id | ds | AutoARIMA | AutoARIMA-lo-90 | AutoARIMA-lo-80 | AutoARIMA-hi-80 | AutoARIMA-hi-90 | AutoARIMA/BottomUp | AutoARIMA/BottomUp-lo-90 | AutoARIMA/BottomUp-lo-80 | ... | AutoARIMA/MinTrace\_method-mint\_shrink | AutoARIMA/MinTrace\_method-mint\_shrink-lo-90 | AutoARIMA/MinTrace\_method-mint\_shrink-lo-80 | AutoARIMA/MinTrace\_method-mint\_shrink-hi-80 | AutoARIMA/MinTrace\_method-mint\_shrink-hi-90 | AutoARIMA/MinTrace\_method-ols | AutoARIMA/MinTrace\_method-ols-lo-90 | AutoARIMA/MinTrace\_method-ols-lo-80 | AutoARIMA/MinTrace\_method-ols-hi-80 | AutoARIMA/MinTrace\_method-ols-hi-90 | | - | ---------- | ---------- | ------------ | --------------- | --------------- | --------------- | --------------- | ------------------ | ------------------------ | ------------------------ | --- | --------------------------------------- | --------------------------------------------- | --------------------------------------------- | --------------------------------------------- | --------------------------------------------- | ------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ | | 0 | Australia | 2016-01-01 | 26212.553553 | 24705.948180 | 25038.715077 | 27386.392029 | 27719.158927 | 24955.501571 | 24143.056131 | 24387.230200 | ... | 25413.657606 | 24705.682710 | 24905.677772 | 25928.334367 | 26050.232961 | 26142.818016 | 25525.081721 | 25656.537995 | 26606.345032 | 26832.423921 | | 1 | Australia | 2016-04-01 | 25033.667125 | 23337.267588 | 23711.954696 | 26355.379554 | 26730.066662 | 23421.312868 | 22762.045247 | 22904.087197 | ... | 24058.906411 | 23486.828548 | 23627.152623 | 24659.405484 | 24847.778503 | 24946.338649 | 24297.061230 | 24434.805048 | 25535.549040 | 25640.659918 | | 2 | Australia | 2016-07-01 | 24507.027198 | 22640.028798 | 23052.396413 | 25961.657983 | 26374.025599 | 22807.706826 | 22065.402373 | 22223.120404 | ... | 23438.863893 | 22672.658701 | 22888.299153 | 23971.724733 | 24179.548677 | 24407.245003 | 23712.841797 | 23834.054327 | 25027.073615 | 25189.869286 | | 3 | Australia | 2016-10-01 | 25598.928613 | 23575.665243 | 24022.547410 | 27175.309816 | 27622.191983 | 23471.845870 | 22677.593575 | 22892.328939 | ... | 24322.049398 | 23619.419712 | 23682.803746 | 24847.299228 | 25028.345572 | 25496.855604 | 24740.210465 | 24923.560783 | 26094.250414 | 26273.617732 | | 4 | Australia | 2017-01-01 | 26982.576796 | 24669.535238 | 25180.421285 | 28784.732308 | 29295.618354 | 24668.735931 | 23760.842072 | 23964.283124 | ... | 25520.163549 | 24720.304392 | 24910.106650 | 26170.552678 | 26347.181903 | 26853.231907 | 26045.213677 | 26149.753374 | 27502.499674 | 27733.985566 | ## Plot forecasts Then we can plot the probabilist forecasts using the following function. ```python theme={null} plot_df = Y_df.merge(Y_rec_df, on=['unique_id', 'ds'], how="outer") ``` ### Plot single time series ```python theme={null} hplot.plot_series( series='Australia', Y_df=plot_df, models=['y', 'AutoARIMA', 'AutoARIMA/MinTrace_method-ols', 'AutoARIMA/BottomUp' ], level=[80] ) ``` ### Plot hierarchichally linked time series ```python theme={null} hplot.plot_hierarchically_linked_series( bottom_series='Australia/Western Australia/Experience Perth', Y_df=plot_df, models=['y', 'AutoARIMA', 'AutoARIMA/MinTrace_method-ols', 'AutoARIMA/BottomUp'], level=[80] ) ``` ```python theme={null} # ACT only has Canberra hplot.plot_hierarchically_linked_series( bottom_series='Australia/ACT/Canberra', Y_df=plot_df, models=['y', 'AutoARIMA/MinTrace_method-mint_shrink'], level=[80, 90] ) ``` ### References * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) * [Shanika L. Wickramasuriya, George Athanasopoulos, and Rob J. Hyndman. Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization.Journal of the American Statistical Association, 114(526):804–819, 2019. doi: 10.1080/01621459.2018.1448825. URL https://robjhyndman.com/publications/mint/.](https://robjhyndman.com/publications/mint/) # Geographical Aggregation (Tourism) Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/australiandomestictourism.html > Geographical Hierarchical Forecasting on Australian Tourism Data In many applications, a set of time series is hierarchically organized. Examples include the presence of geographic levels, products, or categories that define different types of aggregations. In such scenarios, forecasters are often required to provide predictions for all disaggregate and aggregate series. A natural desire is for those predictions to be **“coherent”**, that is, for the bottom series to add up precisely to the forecasts of the aggregated series. In this notebook we present an example on how to use `HierarchicalForecast` to produce coherent forecasts between geographical levels. We will use the classic Australian Domestic Tourism (`Tourism`) dataset, which contains monthly time series of the number of visitors to each state of Australia. We will first load the `Tourism` data and produce base forecasts using an `AutoETS` model from `StatsForecast`, and then reconciliate the forecasts with several reconciliation algorithms from `HierarchicalForecast`. Finally, we show the performance is comparable with the results reported by the [Forecasting: Principles and Practice](https://otexts.com/fpp3/tourism.html) which uses the R package [fable](https://github.com/tidyverts/fable). You can run these experiments using CPU or GPU with Google Colab. Open In Colab ```python theme={null} !pip install hierarchicalforecast statsforecast ``` ## 1. Load and Process Data In this example we will use the [Tourism](https://otexts.com/fpp3/tourism.html) dataset from the [Forecasting: Principles and Practice](https://otexts.com/fpp3/) book. The dataset only contains the time series at the lowest level, so we need to create the time series for all hierarchies. ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') Y_df = Y_df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) Y_df.insert(0, 'Country', 'Australia') Y_df = Y_df[['Country', 'Region', 'State', 'Purpose', 'ds', 'y']] Y_df['ds'] = Y_df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True) Y_df['ds'] = pd.PeriodIndex(Y_df["ds"], freq='Q').to_timestamp() Y_df.head() ``` | | Country | Region | State | Purpose | ds | y | | - | --------- | -------- | --------------- | -------- | ---------- | ---------- | | 0 | Australia | Adelaide | South Australia | Business | 1998-01-01 | 135.077690 | | 1 | Australia | Adelaide | South Australia | Business | 1998-04-01 | 109.987316 | | 2 | Australia | Adelaide | South Australia | Business | 1998-07-01 | 166.034687 | | 3 | Australia | Adelaide | South Australia | Business | 1998-10-01 | 127.160464 | | 4 | Australia | Adelaide | South Australia | Business | 1999-01-01 | 137.448533 | The dataset can be grouped in the following non-strictly hierarchical structure. ```python theme={null} spec = [ ['Country'], ['Country', 'State'], ['Country', 'Purpose'], ['Country', 'State', 'Region'], ['Country', 'State', 'Purpose'], ['Country', 'State', 'Region', 'Purpose'] ] ``` Using the `aggregate` function from `HierarchicalForecast` we can get the full set of time series. ```python theme={null} from hierarchicalforecast.utils import aggregate ``` ```python theme={null} Y_df, S_df, tags = aggregate(Y_df, spec) ``` ```python theme={null} Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ------------ | | 0 | Australia | 1998-01-01 | 23182.197269 | | 1 | Australia | 1998-04-01 | 20323.380067 | | 2 | Australia | 1998-07-01 | 19826.640511 | | 3 | Australia | 1998-10-01 | 20830.129891 | | 4 | Australia | 1999-01-01 | 22087.353380 | ```python theme={null} S_df.iloc[:5, :5] ``` | | unique\_id | Australia/ACT/Canberra/Business | Australia/ACT/Canberra/Holiday | Australia/ACT/Canberra/Other | Australia/ACT/Canberra/Visiting | | - | ---------------------------- | ------------------------------- | ------------------------------ | ---------------------------- | ------------------------------- | | 0 | Australia | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | Australia/ACT | 1.0 | 1.0 | 1.0 | 1.0 | | 2 | Australia/New South Wales | 0.0 | 0.0 | 0.0 | 0.0 | | 3 | Australia/Northern Territory | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | Australia/Queensland | 0.0 | 0.0 | 0.0 | 0.0 | ```python theme={null} tags['Country/Purpose'] ``` ```text theme={null} array(['Australia/Business', 'Australia/Holiday', 'Australia/Other', 'Australia/Visiting'], dtype=object) ``` ### Split Train/Test sets We use the final two years (8 quarters) as test set. ```python theme={null} Y_test_df = Y_df.groupby('unique_id', as_index=False).tail(8) Y_train_df = Y_df.drop(Y_test_df.index) ``` ```python theme={null} Y_train_df.groupby('unique_id').size() ``` ```text theme={null} unique_id Australia 72 Australia/ACT 72 Australia/ACT/Business 72 Australia/ACT/Canberra 72 Australia/ACT/Canberra/Business 72 .. Australia/Western Australia/Experience Perth/Other 72 Australia/Western Australia/Experience Perth/Visiting 72 Australia/Western Australia/Holiday 72 Australia/Western Australia/Other 72 Australia/Western Australia/Visiting 72 Length: 425, dtype: int64 ``` ## 2. Computing base forecasts The following cell computes the **base forecasts** for each time series in `Y_df` using the `ETS` model. Observe that `Y_hat_df` contains the forecasts but they are not coherent. ```python theme={null} from statsforecast.models import AutoETS from statsforecast.core import StatsForecast ``` ```python theme={null} fcst = StatsForecast(models=[AutoETS(season_length=4, model='ZZA')], freq='QS', n_jobs=-1) Y_hat_df = fcst.forecast(df=Y_train_df, h=8, fitted=True) Y_fitted_df = fcst.forecast_fitted_values() ``` ## 3. Reconcile forecasts The following cell makes the previous forecasts coherent using the `HierarchicalReconciliation` class. Since the hierarchy structure is not strict, we can’t use methods such as `TopDown` or `MiddleOut`. In this example we use `BottomUp` and `MinTrace`. ```python theme={null} from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.core import HierarchicalReconciliation ``` ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method='mint_shrink'), MinTrace(method='ols') ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags) ``` The dataframe `Y_rec_df` contains the reconciled forecasts. ```python theme={null} Y_rec_df.head() ``` | | unique\_id | ds | AutoETS | AutoETS/BottomUp | AutoETS/MinTrace\_method-mint\_shrink | AutoETS/MinTrace\_method-ols | | - | ---------- | ---------- | ------------ | ---------------- | ------------------------------------- | ---------------------------- | | 0 | Australia | 2016-01-01 | 25990.068004 | 24381.911737 | 25428.089783 | 25894.399067 | | 1 | Australia | 2016-04-01 | 24458.490282 | 22903.895964 | 23914.271400 | 24357.301898 | | 2 | Australia | 2016-07-01 | 23974.055984 | 22412.265739 | 23428.462394 | 23865.910647 | | 3 | Australia | 2016-10-01 | 24563.454495 | 23127.349578 | 24089.845955 | 24470.782393 | | 4 | Australia | 2017-01-01 | 25990.068004 | 24518.118006 | 25545.358678 | 25901.362283 | ## 4. Evaluation The `HierarchicalForecast` package includes an `evaluate` function to evaluate the different hierarchies and also is capable of compute scaled metrics compared to a benchmark model. ```python theme={null} from hierarchicalforecast.evaluation import evaluate from utilsforecast.losses import rmse, mase from functools import partial ``` ```python theme={null} eval_tags = {} eval_tags['Total'] = tags['Country'] eval_tags['Purpose'] = tags['Country/Purpose'] eval_tags['State'] = tags['Country/State'] eval_tags['Regions'] = tags['Country/State/Region'] eval_tags['Bottom'] = tags['Country/State/Region/Purpose'] df = Y_rec_df.merge(Y_test_df, on=['unique_id', 'ds']) evaluation = evaluate(df = df, tags = eval_tags, train_df = Y_train_df, metrics = [rmse, partial(mase, seasonality=4)]) evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(mint_shrink)', 'MinTrace(ols)'] numeric_cols = evaluation.select_dtypes(include="number").columns evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64) ``` ### RMSE The following table shows the performance measured using RMSE across levels for each reconciliation method. ```python theme={null} evaluation.query('metric == "rmse"') ``` | | level | metric | Base | BottomUp | MinTrace(mint\_shrink) | MinTrace(ols) | | -- | ------- | ------ | ------- | -------- | ---------------------- | ------------- | | 0 | Total | rmse | 1743.29 | 3028.62 | 2112.73 | 1818.94 | | 2 | Purpose | rmse | 534.75 | 791.19 | 577.14 | 515.53 | | 4 | State | rmse | 308.15 | 413.39 | 316.82 | 287.32 | | 6 | Regions | rmse | 51.66 | 55.13 | 46.55 | 46.28 | | 8 | Bottom | rmse | 19.37 | 19.37 | 17.80 | 18.19 | | 10 | Overall | rmse | 41.12 | 49.82 | 40.47 | 38.75 | ### MASE The following table shows the performance measured using MASE across levels for each reconciliation method. ```python theme={null} evaluation.query('metric == "mase"') ``` | | level | metric | Base | BottomUp | MinTrace(mint\_shrink) | MinTrace(ols) | | -- | ------- | ------ | ---- | -------- | ---------------------- | ------------- | | 1 | Total | mase | 1.59 | 3.16 | 2.06 | 1.67 | | 3 | Purpose | mase | 1.32 | 2.28 | 1.48 | 1.25 | | 5 | State | mase | 1.39 | 1.90 | 1.40 | 1.25 | | 7 | Regions | mase | 1.12 | 1.19 | 1.01 | 0.99 | | 9 | Bottom | mase | 0.98 | 0.98 | 0.94 | 1.01 | | 11 | Overall | mase | 1.02 | 1.06 | 0.97 | 1.02 | ### Comparison with `fable` Observe that we can recover the results reported by the [Forecasting: Principles and Practice](https://otexts.com/fpp3/tourism.html). The original results were calculated using the R package [fable](https://github.com/tidyverts/fable).
Fable’s reconciliation results
Fable’s reconciliation results
### References * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) * [Rob Hyndman, Alan Lee, Earo Wang, Shanika Wickramasuriya, and Maintainer Earo Wang (2021). “hts: Hierarchical and Grouped Time Series”. URL https://CRAN.R-project.org/package=hts. R package version 0.3.1.](https://cran.r-project.org/web/packages/hts/index.html) * [Mitchell O’Hara-Wild, Rob Hyndman, Earo Wang, Gabriel Caceres, Tim-Gunnar Hensel, and Timothy Hyndman (2021). “fable: Forecasting Models for Tidy Time Series”. URL https://CRAN.R-project.org/package=fable. R package version 6.0.2.](https://CRAN.R-project.org/package=fable) # Geographical and Temporal Aggregation (Tourism) Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/australiandomestictourismcrosstemporal.html > Geographical and Temporal Hierarchical Forecasting on Australian > Tourism Data In many applications, a set of time series is hierarchically organized. Examples include the presence of geographic levels, products, or categories that define different types of aggregations. In such scenarios, forecasters are often required to provide predictions for all disaggregate and aggregate series. A natural desire is for those predictions to be **“coherent”**, that is, for the bottom series to add up precisely to the forecasts of the aggregated series. In this notebook we present an example on how to use `HierarchicalForecast` to produce coherent forecasts between both geographical levels and temporal levels. We will use the classic Australian Domestic Tourism (`Tourism`) dataset, which contains monthly time series of the number of visitors to each state of Australia. We will first load the `Tourism` data and produce base forecasts using an `AutoETS` model from `StatsForecast`. Then, we reconciliate the forecasts with several reconciliation algorithms from `HierarchicalForecast` according to the cross-sectional geographical hierarchies. Finally, we reconciliate the forecasts in the temporal dimension according to a temporal hierarchy. You can run these experiments using CPU or GPU with Google Colab. Open In Colab ```python theme={null} !pip install hierarchicalforecast statsforecast ``` ## 1. Load and Process Data In this example we will use the [Tourism](https://otexts.com/fpp3/tourism.html) dataset from the [Forecasting: Principles and Practice](https://otexts.com/fpp3/) book. The dataset only contains the time series at the lowest level, so we need to create the time series for all hierarchies. ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') Y_df = Y_df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) Y_df.insert(0, 'Country', 'Australia') Y_df = Y_df[['Country', 'Region', 'State', 'Purpose', 'ds', 'y']] Y_df['ds'] = Y_df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True) Y_df['ds'] = pd.PeriodIndex(Y_df["ds"], freq='Q').to_timestamp() Y_df.head() ``` | | Country | Region | State | Purpose | ds | y | | - | --------- | -------- | --------------- | -------- | ---------- | ---------- | | 0 | Australia | Adelaide | South Australia | Business | 1998-01-01 | 135.077690 | | 1 | Australia | Adelaide | South Australia | Business | 1998-04-01 | 109.987316 | | 2 | Australia | Adelaide | South Australia | Business | 1998-07-01 | 166.034687 | | 3 | Australia | Adelaide | South Australia | Business | 1998-10-01 | 127.160464 | | 4 | Australia | Adelaide | South Australia | Business | 1999-01-01 | 137.448533 | ## 2. Cross-sectional reconciliation ### 2a. Aggregating the dataset according to cross-sectional hierarchy The dataset can be grouped in the following non-strictly hierarchical structure. ```python theme={null} spec = [ ['Country'], ['Country', 'State'], ['Country', 'Purpose'], ['Country', 'State', 'Region'], ['Country', 'State', 'Purpose'], ['Country', 'State', 'Region', 'Purpose'] ] ``` Using the `aggregate` function from `HierarchicalForecast` we can get the full set of time series. ```python theme={null} from hierarchicalforecast.utils import aggregate ``` ```python theme={null} Y_df_cs, S_df_cs, tags_cs = aggregate(Y_df, spec) ``` ```python theme={null} Y_df_cs ``` | | unique\_id | ds | y | | ----- | ------------------------------------------------- | ---------- | ------------ | | 0 | Australia | 1998-01-01 | 23182.197269 | | 1 | Australia | 1998-04-01 | 20323.380067 | | 2 | Australia | 1998-07-01 | 19826.640511 | | 3 | Australia | 1998-10-01 | 20830.129891 | | 4 | Australia | 1999-01-01 | 22087.353380 | | ... | ... | ... | ... | | 33995 | Australia/Western Australia/Experience Perth/V... | 2016-10-01 | 439.699451 | | 33996 | Australia/Western Australia/Experience Perth/V... | 2017-01-01 | 356.867038 | | 33997 | Australia/Western Australia/Experience Perth/V... | 2017-04-01 | 302.296119 | | 33998 | Australia/Western Australia/Experience Perth/V... | 2017-07-01 | 373.442070 | | 33999 | Australia/Western Australia/Experience Perth/V... | 2017-10-01 | 455.316702 | ```python theme={null} S_df_cs.iloc[:5, :5] ``` | | unique\_id | Australia/ACT/Canberra/Business | Australia/ACT/Canberra/Holiday | Australia/ACT/Canberra/Other | Australia/ACT/Canberra/Visiting | | - | ---------------------------- | ------------------------------- | ------------------------------ | ---------------------------- | ------------------------------- | | 0 | Australia | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | Australia/ACT | 1.0 | 1.0 | 1.0 | 1.0 | | 2 | Australia/New South Wales | 0.0 | 0.0 | 0.0 | 0.0 | | 3 | Australia/Northern Territory | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | Australia/Queensland | 0.0 | 0.0 | 0.0 | 0.0 | ### 2b. Split Train/Test sets We use the final two years (8 quarters) as test set. Consequently, our forecast horizon=8. ```python theme={null} horizon = 8 ``` ```python theme={null} Y_test_df_cs = Y_df_cs.groupby("unique_id", as_index=False).tail(horizon) Y_train_df_cs = Y_df_cs.drop(Y_test_df_cs.index) ``` ### 2c. Computing base forecasts The following cell computes the **base forecasts** for each time series in `Y_df` using the `AutoETS` model. Observe that `Y_hat_df` contains the forecasts but they are not coherent. ```python theme={null} from statsforecast.models import AutoETS from statsforecast.core import StatsForecast ``` ```python theme={null} fcst = StatsForecast(models=[AutoETS(season_length=4, model='ZZA')], freq='QS', n_jobs=-1) Y_hat_df_cs = fcst.forecast(df=Y_train_df_cs, h=horizon, fitted=True) Y_fitted_df_cs = fcst.forecast_fitted_values() ``` ### 2d. Reconcile forecasts The following cell makes the previous forecasts coherent using the `HierarchicalReconciliation` class. Since the hierarchy structure is not strict, we can’t use methods such as `TopDown` or `MiddleOut`. In this example we use `BottomUp` and `MinTrace`. ```python theme={null} from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.core import HierarchicalReconciliation ``` ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method='mint_shrink'), MinTrace(method='ols') ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df_cs = hrec.reconcile(Y_hat_df=Y_hat_df_cs, Y_df=Y_fitted_df_cs, S_df=S_df_cs, tags=tags_cs) ``` The dataframe `Y_rec_df` contains the reconciled forecasts. ```python theme={null} Y_rec_df_cs.head() ``` | | unique\_id | ds | AutoETS | AutoETS/BottomUp | AutoETS/MinTrace\_method-mint\_shrink | AutoETS/MinTrace\_method-ols | | - | ---------- | ---------- | ------------ | ---------------- | ------------------------------------- | ---------------------------- | | 0 | Australia | 2016-01-01 | 25990.068004 | 24381.911737 | 25428.089783 | 25894.399067 | | 1 | Australia | 2016-04-01 | 24458.490282 | 22903.895964 | 23914.271400 | 24357.301898 | | 2 | Australia | 2016-07-01 | 23974.055984 | 22412.265739 | 23428.462394 | 23865.910647 | | 3 | Australia | 2016-10-01 | 24563.454495 | 23127.349578 | 24089.845955 | 24470.782393 | | 4 | Australia | 2017-01-01 | 25990.068004 | 24518.118006 | 25545.358678 | 25901.362283 | ## 3. Temporal reconciliation Next, we aim to reconcile our forecasts also in the temporal domain. ### 3a. Aggregating the dataset according to temporal hierarchy We first define the temporal aggregation spec. The spec is a dictionary in which the keys are the name of the aggregation and the value is the amount of bottom-level timesteps that should be aggregated in that aggregation. For example, `year` consists of `12` months, so we define a key, value pair `"yearly":12`. We can do something similar for other aggregations that we are interested in. In this example, we choose a temporal aggregation of `year`, `semiannual` and `quarter`. The bottom level timesteps have a quarterly frequency. ```python theme={null} spec_temporal = {"year": 4, "semiannual": 2, "quarter": 1} ``` We next compute the temporally aggregated train- and test sets using the `aggregate_temporal` function. Note that we have different aggregation matrices `S` for the train- and test set, as the test set contains temporal hierarchies that are not included in the train set. ```python theme={null} from hierarchicalforecast.utils import aggregate_temporal ``` ```python theme={null} Y_train_df_te, S_train_df_te, tags_te_train = aggregate_temporal(df=Y_train_df_cs, spec=spec_temporal) Y_test_df_te, S_test_df_te, tags_te_test = aggregate_temporal(df=Y_test_df_cs, spec=spec_temporal) ``` ```python theme={null} S_train_df_te.iloc[:5, :5] ``` | | temporal\_id | quarter-1 | quarter-2 | quarter-3 | quarter-4 | | - | ------------ | --------- | --------- | --------- | --------- | | 0 | year-1 | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | year-2 | 0.0 | 0.0 | 0.0 | 0.0 | | 2 | year-3 | 0.0 | 0.0 | 0.0 | 0.0 | | 3 | year-4 | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | year-5 | 0.0 | 0.0 | 0.0 | 0.0 | ```python theme={null} S_test_df_te.iloc[:5, :5] ``` | | temporal\_id | quarter-1 | quarter-2 | quarter-3 | quarter-4 | | - | ------------ | --------- | --------- | --------- | --------- | | 0 | year-1 | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | year-2 | 0.0 | 0.0 | 0.0 | 0.0 | | 2 | semiannual-1 | 1.0 | 1.0 | 0.0 | 0.0 | | 3 | semiannual-2 | 0.0 | 0.0 | 1.0 | 1.0 | | 4 | semiannual-3 | 0.0 | 0.0 | 0.0 | 0.0 | If you don’t have a test set available, as is usually the case when you’re making forecasts, it is necessary to create a future dataframe that holds the correct bottom-level unique\_ids and timestamps so that they can be temporally aggregated. We can use the `make_future_dataframe` helper function for that. ```python theme={null} from hierarchicalforecast.utils import make_future_dataframe ``` ```python theme={null} Y_test_df_te_new = make_future_dataframe(Y_train_df_te, freq="QS", h=horizon) ``` `Y_test_df_te_new` can be then used in `aggregate_temporal` to construct the temporally aggregated structures: ```python theme={null} Y_test_df_te_new, S_test_df_te_new, tags_te_test_new = aggregate_temporal(df=Y_test_df_te_new, spec=spec_temporal) ``` And we can verify that we have the same temporally aggregated test set, except that `Y_test_df_te_new` doesn’t contain the ground truth values `y`. ```python theme={null} Y_test_df_te ``` | | temporal\_id | unique\_id | ds | y | | ---- | ------------ | ------------------------------------ | ---------- | ------------- | | 0 | year-1 | Australia | 2016-10-01 | 101484.586551 | | 1 | year-2 | Australia | 2017-10-01 | 107709.864650 | | 2 | year-1 | Australia/ACT | 2016-10-01 | 2457.401367 | | 3 | year-2 | Australia/ACT | 2017-10-01 | 2734.748452 | | 4 | year-1 | Australia/ACT/Business | 2016-10-01 | 754.139245 | | ... | ... | ... | ... | ... | | 5945 | quarter-4 | Australia/Western Australia/Visiting | 2016-10-01 | 787.030391 | | 5946 | quarter-5 | Australia/Western Australia/Visiting | 2017-01-01 | 702.777251 | | 5947 | quarter-6 | Australia/Western Australia/Visiting | 2017-04-01 | 642.516090 | | 5948 | quarter-7 | Australia/Western Australia/Visiting | 2017-07-01 | 646.521395 | | 5949 | quarter-8 | Australia/Western Australia/Visiting | 2017-10-01 | 813.184778 | ```python theme={null} Y_test_df_te_new ``` | | temporal\_id | unique\_id | ds | | ---- | ------------ | ------------------------------------ | ---------- | | 0 | year-1 | Australia | 2016-10-01 | | 1 | year-2 | Australia | 2017-10-01 | | 2 | year-1 | Australia/ACT | 2016-10-01 | | 3 | year-2 | Australia/ACT | 2017-10-01 | | 4 | year-1 | Australia/ACT/Business | 2016-10-01 | | ... | ... | ... | ... | | 5945 | quarter-4 | Australia/Western Australia/Visiting | 2016-10-01 | | 5946 | quarter-5 | Australia/Western Australia/Visiting | 2017-01-01 | | 5947 | quarter-6 | Australia/Western Australia/Visiting | 2017-04-01 | | 5948 | quarter-7 | Australia/Western Australia/Visiting | 2017-07-01 | | 5949 | quarter-8 | Australia/Western Australia/Visiting | 2017-10-01 | ### 3b. Computing base forecasts Now, we need to compute base forecasts for each temporal aggregation. The following cell computes the **base forecasts** for each temporal aggregation in `Y_train_df_te` using the `AutoETS` model. Observe that `Y_hat_df_te` contains the forecasts but they are not coherent. Note also that both frequency and horizon are different for each temporal aggregation. In this example, the lowest level has a quarterly frequency, and a horizon of `8` (constituting `2` years). The `year` aggregation thus has a yearly frequency with a horizon of `2`. It is of course possible to choose a different model for each level in the temporal aggregation - you can be as creative as you like! ```python theme={null} Y_hat_dfs_te = [] id_cols = ["unique_id", "temporal_id", "ds", "y"] # We will train a model for each temporal level for level, temporal_ids_train in tags_te_train.items(): # Filter the data for the level Y_level_train = Y_train_df_te.query("temporal_id in @temporal_ids_train") temporal_ids_test = tags_te_test[level] Y_level_test = Y_test_df_te.query("temporal_id in @temporal_ids_test") # For each temporal level we have a different frequency and forecast horizon freq_level = pd.infer_freq(Y_level_train["ds"].unique()) horizon_level = Y_level_test["ds"].nunique() # Train a model and create forecasts fcst = StatsForecast(models=[AutoETS(model='ZZZ')], freq=freq_level, n_jobs=-1) Y_hat_df_te_level = fcst.forecast(df=Y_level_train[["ds", "unique_id", "y"]], h=horizon_level) # Add the test set to the forecast Y_hat_df_te_level = Y_hat_df_te_level.merge(Y_level_test, on=["ds", "unique_id"], how="left") # Put cols in the right order (for readability) Y_hat_cols = id_cols + [col for col in Y_hat_df_te_level.columns if col not in id_cols] Y_hat_df_te_level = Y_hat_df_te_level[Y_hat_cols] # Append the forecast to the list Y_hat_dfs_te.append(Y_hat_df_te_level) Y_hat_df_te = pd.concat(Y_hat_dfs_te, ignore_index=True) ``` ### 3c. Reconcile forecasts We can again use the `HierarchicalReconciliation` class to reconcile the forecasts. In this example we use `BottomUp` and `MinTrace`. Note that we have to set `temporal=True` in the `reconcile` function. Note that temporal reconcilation currently isn’t supported for insample reconciliation methods, such as `MinTrace(method='mint_shrink')`. ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method='ols') ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df_te = hrec.reconcile(Y_hat_df=Y_hat_df_te, S_df=S_test_df_te, tags=tags_te_test, temporal=True) ``` ## 4. Evaluation The `HierarchicalForecast` package includes the `evaluate` function to evaluate the different hierarchies. ```python theme={null} from hierarchicalforecast.evaluation import evaluate from utilsforecast.losses import rmse ``` ### 4a. Cross-sectional evaluation We first evaluate the forecasts *across all cross-sectional aggregations*. ```python theme={null} eval_tags = {} eval_tags['Total'] = tags_cs['Country'] eval_tags['Purpose'] = tags_cs['Country/Purpose'] eval_tags['State'] = tags_cs['Country/State'] eval_tags['Regions'] = tags_cs['Country/State/Region'] eval_tags['Bottom'] = tags_cs['Country/State/Region/Purpose'] evaluation = evaluate(df = Y_rec_df_te.drop(columns = 'temporal_id'), tags = eval_tags, metrics = [rmse]) evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(ols)'] numeric_cols = evaluation.select_dtypes(include="number").columns evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64) ``` ```python theme={null} evaluation ``` | | level | metric | Base | BottomUp | MinTrace(ols) | | - | ------- | ------ | ------- | -------- | ------------- | | 0 | Total | rmse | 4249.25 | 4461.95 | 4234.55 | | 1 | Purpose | rmse | 1222.57 | 1273.48 | 1137.57 | | 2 | State | rmse | 635.78 | 546.02 | 611.32 | | 3 | Regions | rmse | 103.67 | 107.00 | 99.23 | | 4 | Bottom | rmse | 33.15 | 33.98 | 32.30 | | 5 | Overall | rmse | 81.89 | 82.41 | 78.97 | As can be seen `MinTrace(ols)` seems to be the best forecasting method across each cross-sectional aggregation. ### 4b. Temporal evaluation We then evaluate the temporally aggregated forecasts *across all temporal aggregations*. ```python theme={null} evaluation = evaluate(df = Y_rec_df_te.drop(columns = 'unique_id'), tags = tags_te_test, metrics = [rmse], id_col="temporal_id") evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(ols)'] numeric_cols = evaluation.select_dtypes(include="number").columns evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64) ``` ```python theme={null} evaluation ``` | | level | metric | Base | BottomUp | MinTrace(ols) | | - | ---------- | ------ | ------ | -------- | ------------- | | 0 | year | rmse | 480.85 | 581.18 | 515.32 | | 1 | semiannual | rmse | 312.33 | 304.98 | 275.30 | | 2 | quarter | rmse | 168.02 | 168.02 | 155.61 | | 3 | Overall | rmse | 253.94 | 266.17 | 241.19 | Again, `MinTrace(ols)` is the best overall method, scoring the lowest `rmse` on the `quarter` aggregated forecasts, and being slightly worse than the `Base` forecasts on the `year` aggregated forecasts. ### 4c. Cross-temporal evaluation Finally, we evaluate cross-temporally. To do so, we first need to obtain the combination of cross-sectional and temporal hierarchies, for which we can use the `get_cross_temporal_tags` helper function. ```python theme={null} from hierarchicalforecast.utils import get_cross_temporal_tags ``` ```python theme={null} Y_rec_df_te, tags_ct = get_cross_temporal_tags(Y_rec_df_te, tags_cs=tags_cs, tags_te=tags_te_test) ``` As we can see, we now have a tag `Country//year` that contains `Australia//year-1` and `Australia//year-2`, indicating the cross-sectional hierarchy `Australia` at the temporal hierarchies `2016` and `2017`. ```python theme={null} tags_ct["Country//year"] ``` ```text theme={null} ['Australia//year-1', 'Australia//year-2'] ``` We now have our dataset and cross-temporal tags ready for evaluation. We define a set of eval\_tags, and now we split each cross-sectional aggregation also by each temporal aggregation. Note that we skip the semiannual temporal aggregation in the below overview. ```python theme={null} eval_tags = {} eval_tags['TotalByYear'] = tags_ct['Country//year'] eval_tags['RegionsByYear'] = tags_ct['Country/State/Region//year'] eval_tags['BottomByYear'] = tags_ct['Country/State/Region/Purpose//year'] eval_tags['TotalByQuarter'] = tags_ct['Country//quarter'] eval_tags['RegionsByQuarter'] = tags_ct['Country/State/Region//quarter'] eval_tags['BottomByQuarter'] = tags_ct['Country/State/Region/Purpose//quarter'] evaluation = evaluate(df = Y_rec_df_te.drop(columns=['unique_id', 'temporal_id']), tags = eval_tags, id_col = 'cross_temporal_id', metrics = [rmse]) evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(ols)'] numeric_cols = evaluation.select_dtypes(include="number").columns evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64) ``` ```python theme={null} evaluation ``` | | level | metric | Base | BottomUp | MinTrace(ols) | | - | ---------------- | ------ | ------- | -------- | ------------- | | 0 | TotalByYear | rmse | 7148.99 | 8243.06 | 7748.40 | | 1 | RegionsByYear | rmse | 151.96 | 175.69 | 158.48 | | 2 | BottomByYear | rmse | 46.98 | 50.78 | 46.72 | | 3 | TotalByQuarter | rmse | 2060.77 | 2060.77 | 1942.32 | | 4 | RegionsByQuarter | rmse | 57.07 | 57.07 | 54.12 | | 5 | BottomByQuarter | rmse | 19.42 | 19.42 | 18.69 | | 6 | Overall | rmse | 43.14 | 45.27 | 42.49 | We find that the best method is the cross-temporally reconciled method `AutoETS/MinTrace_method-ols`, which achieves overall lowest RMSE. ### References * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) * [Rob Hyndman, Alan Lee, Earo Wang, Shanika Wickramasuriya, and Maintainer Earo Wang (2021). “hts: Hierarchical and Grouped Time Series”. URL https://CRAN.R-project.org/package=hts. R package version 0.3.1.](https://cran.r-project.org/web/packages/hts/index.html) * [Mitchell O’Hara-Wild, Rob Hyndman, Earo Wang, Gabriel Caceres, Tim-Gunnar Hensel, and Timothy Hyndman (2021). “fable: Forecasting Models for Tidy Time Series”. URL https://CRAN.R-project.org/package=fable. R package version 6.0.2.](https://CRAN.R-project.org/package=fable) * [Athanasopoulos, G, Hyndman, Rob J., Kourentzes, N., Petropoulos, Fotios (2017). Forecasting with temporal hierarchies. European Journal of Operational Research, 262, 60-74](https://www.sciencedirect.com/science/article/pii/S0377221717301911) # Temporal Aggregation (Tourism) Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/australiandomestictourismtemporal.html > Temporal Hierarchical Forecasting on Australian Tourism Data In many applications, a set of time series is hierarchically organized. Examples include the presence of geographic levels, products, or categories that define different types of aggregations. In such scenarios, forecasters are often required to provide predictions for all disaggregate and aggregate series. A natural desire is for those predictions to be **“coherent”**, that is, for the bottom series to add up precisely to the forecasts of the aggregated series. In this notebook we present an example on how to use `HierarchicalForecast` to produce coherent forecasts between temporal levels. We will use the classic Australian Domestic Tourism (`Tourism`) dataset, which contains monthly time series of the number of visitors to each state of Australia. We will first load the `Tourism` data and produce base forecasts using an `AutoETS` model from `StatsForecast`. Then, we reconciliate the forecasts with several reconciliation algorithms from `HierarchicalForecast` according to a temporal hierarchy. You can run these experiments using CPU or GPU with Google Colab. Open In Colab ```python theme={null} !pip install hierarchicalforecast statsforecast ``` ## 1. Load and Process Data In this example we will use the [Tourism](https://otexts.com/fpp3/tourism.html) dataset from the [Forecasting: Principles and Practice](https://otexts.com/fpp3/) book. The dataset only contains the time series at the lowest level, so we need to create the time series for all hierarchies. ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') Y_df = Y_df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) Y_df.insert(0, 'Country', 'Australia') Y_df = Y_df[['Country', 'Region', 'State', 'Purpose', 'ds', 'y']] Y_df['ds'] = Y_df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True) Y_df['ds'] = pd.PeriodIndex(Y_df["ds"], freq='Q').to_timestamp() Y_df.head() ``` | | Country | Region | State | Purpose | ds | y | | - | --------- | -------- | --------------- | -------- | ---------- | ---------- | | 0 | Australia | Adelaide | South Australia | Business | 1998-01-01 | 135.077690 | | 1 | Australia | Adelaide | South Australia | Business | 1998-04-01 | 109.987316 | | 2 | Australia | Adelaide | South Australia | Business | 1998-07-01 | 166.034687 | | 3 | Australia | Adelaide | South Australia | Business | 1998-10-01 | 127.160464 | | 4 | Australia | Adelaide | South Australia | Business | 1999-01-01 | 137.448533 | ## 2. Temporal reconciliation First, we add a `unique_id` to the data. ```python theme={null} Y_df["unique_id"] = Y_df["Country"] + "/" + Y_df["State"] + "/" + Y_df["Region"] + "/" + Y_df["Purpose"] ``` ### 2a. Split Train/Test sets We use the final two years (8 quarters) as test set. Consequently, our forecast horizon=8. ```python theme={null} horizon = 8 ``` ```python theme={null} Y_test_df = Y_df.groupby("unique_id", as_index=False).tail(horizon) Y_train_df = Y_df.drop(Y_test_df.index) ``` ### 2a. Aggregating the dataset according to temporal hierarchy We first define the temporal aggregation spec. The spec is a dictionary in which the keys are the name of the aggregation and the value is the amount of bottom-level timesteps that should be aggregated in that aggregation. For example, `year` consists of `12` months, so we define a key, value pair `"yearly":12`. We can do something similar for other aggregations that we are interested in. In this example, we choose a temporal aggregation of `year`, `semiannual` and `quarter`. The bottom level timesteps have a quarterly frequency. ```python theme={null} spec_temporal = {"year": 4, "semiannual": 2, "quarter": 1} ``` We next compute the temporally aggregated train- and test sets using the `aggregate_temporal` function. Note that we have different aggregation matrices `S` for the train- and test set, as the test set contains temporal hierarchies that are not included in the train set. ```python theme={null} from hierarchicalforecast.utils import aggregate_temporal ``` ```python theme={null} Y_train_df, S_train_df, tags_train = aggregate_temporal(df=Y_train_df, spec=spec_temporal) Y_test_df, S_test_df, tags_test = aggregate_temporal(df=Y_test_df, spec=spec_temporal) ``` ```python theme={null} tags_train ``` ```text theme={null} {'year': array(['year-1', 'year-2', 'year-3', 'year-4', 'year-5', 'year-6', 'year-7', 'year-8', 'year-9', 'year-10', 'year-11', 'year-12', 'year-13', 'year-14', 'year-15', 'year-16', 'year-17', 'year-18'], dtype=object), 'semiannual': array(['semiannual-1', 'semiannual-2', 'semiannual-3', 'semiannual-4', 'semiannual-5', 'semiannual-6', 'semiannual-7', 'semiannual-8', 'semiannual-9', 'semiannual-10', 'semiannual-11', 'semiannual-12', 'semiannual-13', 'semiannual-14', 'semiannual-15', 'semiannual-16', 'semiannual-17', 'semiannual-18', 'semiannual-19', 'semiannual-20', 'semiannual-21', 'semiannual-22', 'semiannual-23', 'semiannual-24', 'semiannual-25', 'semiannual-26', 'semiannual-27', 'semiannual-28', 'semiannual-29', 'semiannual-30', 'semiannual-31', 'semiannual-32', 'semiannual-33', 'semiannual-34', 'semiannual-35', 'semiannual-36'], dtype=object), 'quarter': array(['quarter-1', 'quarter-2', 'quarter-3', 'quarter-4', 'quarter-5', 'quarter-6', 'quarter-7', 'quarter-8', 'quarter-9', 'quarter-10', 'quarter-11', 'quarter-12', 'quarter-13', 'quarter-14', 'quarter-15', 'quarter-16', 'quarter-17', 'quarter-18', 'quarter-19', 'quarter-20', 'quarter-21', 'quarter-22', 'quarter-23', 'quarter-24', 'quarter-25', 'quarter-26', 'quarter-27', 'quarter-28', 'quarter-29', 'quarter-30', 'quarter-31', 'quarter-32', 'quarter-33', 'quarter-34', 'quarter-35', 'quarter-36', 'quarter-37', 'quarter-38', 'quarter-39', 'quarter-40', 'quarter-41', 'quarter-42', 'quarter-43', 'quarter-44', 'quarter-45', 'quarter-46', 'quarter-47', 'quarter-48', 'quarter-49', 'quarter-50', 'quarter-51', 'quarter-52', 'quarter-53', 'quarter-54', 'quarter-55', 'quarter-56', 'quarter-57', 'quarter-58', 'quarter-59', 'quarter-60', 'quarter-61', 'quarter-62', 'quarter-63', 'quarter-64', 'quarter-65', 'quarter-66', 'quarter-67', 'quarter-68', 'quarter-69', 'quarter-70', 'quarter-71', 'quarter-72'], dtype=object)} ``` Our aggregation matrices aggregate the lowest temporal granularity (quarters) up to years. ```python theme={null} S_train_df.iloc[:5, :5] ``` | | temporal\_id | quarter-1 | quarter-2 | quarter-3 | quarter-4 | | - | ------------ | --------- | --------- | --------- | --------- | | 0 | year-1 | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | year-2 | 0.0 | 0.0 | 0.0 | 0.0 | | 2 | year-3 | 0.0 | 0.0 | 0.0 | 0.0 | | 3 | year-4 | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | year-5 | 0.0 | 0.0 | 0.0 | 0.0 | ```python theme={null} S_test_df.iloc[:5, :5] ``` | | temporal\_id | quarter-1 | quarter-2 | quarter-3 | quarter-4 | | - | ------------ | --------- | --------- | --------- | --------- | | 0 | year-1 | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | year-2 | 0.0 | 0.0 | 0.0 | 0.0 | | 2 | semiannual-1 | 1.0 | 1.0 | 0.0 | 0.0 | | 3 | semiannual-2 | 0.0 | 0.0 | 1.0 | 1.0 | | 4 | semiannual-3 | 0.0 | 0.0 | 0.0 | 0.0 | If you don’t have a test set available, as is usually the case when you’re making forecasts, it is necessary to create a future dataframe that holds the correct bottom-level unique\_ids and timestamps so that they can be temporally aggregated. We can use the `make_future_dataframe` helper function for that. ```python theme={null} from hierarchicalforecast.utils import make_future_dataframe ``` ```python theme={null} Y_test_df_new = make_future_dataframe(Y_train_df, freq="QS", h=horizon) ``` `Y_test_df_new` can be then used in `aggregate_temporal` to construct the temporally aggregated structures: ```python theme={null} Y_test_df_new, S_test_df_new, tags_test_new = aggregate_temporal(df=Y_test_df_new, spec=spec_temporal) ``` And we can verify that we have the same temporally aggregated test set, except that `Y_test_df_new` doesn’t contain the ground truth values `y`. ```python theme={null} S_test_df_new ``` | | temporal\_id | quarter-1 | quarter-2 | quarter-3 | quarter-4 | quarter-5 | quarter-6 | quarter-7 | quarter-8 | | -- | ------------ | --------- | --------- | --------- | --------- | --------- | --------- | --------- | --------- | | 0 | year-1 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 1 | year-2 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 1.0 | 1.0 | | 2 | semiannual-1 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 3 | semiannual-2 | 0.0 | 0.0 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | semiannual-3 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0.0 | 0.0 | | 5 | semiannual-4 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | | 6 | quarter-1 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 7 | quarter-2 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 8 | quarter-3 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 9 | quarter-4 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 10 | quarter-5 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | | 11 | quarter-6 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | | 12 | quarter-7 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | | 13 | quarter-8 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | ```python theme={null} Y_test_df ``` | | temporal\_id | unique\_id | ds | y | | ---- | ------------ | ------------------------------------------------- | ---------- | ---------- | | 0 | year-1 | Australia/ACT/Canberra/Business | 2016-10-01 | 754.139245 | | 1 | year-2 | Australia/ACT/Canberra/Business | 2017-10-01 | 809.950839 | | 2 | year-1 | Australia/ACT/Canberra/Holiday | 2016-10-01 | 735.365896 | | 3 | year-2 | Australia/ACT/Canberra/Holiday | 2017-10-01 | 834.717900 | | 4 | year-1 | Australia/ACT/Canberra/Other | 2016-10-01 | 175.239916 | | ... | ... | ... | ... | ... | | 4251 | quarter-4 | Australia/Western Australia/Experience Perth/V... | 2016-10-01 | 439.699451 | | 4252 | quarter-5 | Australia/Western Australia/Experience Perth/V... | 2017-01-01 | 356.867038 | | 4253 | quarter-6 | Australia/Western Australia/Experience Perth/V... | 2017-04-01 | 302.296119 | | 4254 | quarter-7 | Australia/Western Australia/Experience Perth/V... | 2017-07-01 | 373.442070 | | 4255 | quarter-8 | Australia/Western Australia/Experience Perth/V... | 2017-10-01 | 455.316702 | ```python theme={null} Y_test_df_new ``` | | temporal\_id | unique\_id | ds | | ---- | ------------ | ------------------------------------------------- | ---------- | | 0 | year-1 | Australia/ACT/Canberra/Business | 2016-10-01 | | 1 | year-2 | Australia/ACT/Canberra/Business | 2017-10-01 | | 2 | year-1 | Australia/ACT/Canberra/Holiday | 2016-10-01 | | 3 | year-2 | Australia/ACT/Canberra/Holiday | 2017-10-01 | | 4 | year-1 | Australia/ACT/Canberra/Other | 2016-10-01 | | ... | ... | ... | ... | | 4251 | quarter-4 | Australia/Western Australia/Experience Perth/V... | 2016-10-01 | | 4252 | quarter-5 | Australia/Western Australia/Experience Perth/V... | 2017-01-01 | | 4253 | quarter-6 | Australia/Western Australia/Experience Perth/V... | 2017-04-01 | | 4254 | quarter-7 | Australia/Western Australia/Experience Perth/V... | 2017-07-01 | | 4255 | quarter-8 | Australia/Western Australia/Experience Perth/V... | 2017-10-01 | ### 3b. Computing base forecasts Now, we need to compute base forecasts for each temporal aggregation. The following cell computes the **base forecasts** for each temporal aggregation in `Y_train_df` using the `AutoETS` model. Observe that `Y_hat_df` contains the forecasts but they are not coherent. Note also that both frequency and horizon are different for each temporal aggregation. In this example, the lowest level has a quarterly frequency, and a horizon of `8` (constituting `2` years). The `year` aggregation thus has a yearly frequency with a horizon of `2`. It is of course possible to choose a different model for each level in the temporal aggregation - you can be as creative as you like! ```python theme={null} from statsforecast.models import AutoETS from statsforecast.core import StatsForecast ``` ```python theme={null} Y_hat_dfs = [] id_cols = ["unique_id", "temporal_id", "ds", "y"] # We will train a model for each temporal level for level, temporal_ids_train in tags_train.items(): # Filter the data for the level Y_level_train = Y_train_df.query("temporal_id in @temporal_ids_train") temporal_ids_test = tags_test[level] Y_level_test = Y_test_df.query("temporal_id in @temporal_ids_test") # For each temporal level we have a different frequency and forecast horizon freq_level = pd.infer_freq(Y_level_train["ds"].unique()) horizon_level = Y_level_test["ds"].nunique() # Train a model and create forecasts fcst = StatsForecast(models=[AutoETS(model='ZZZ')], freq=freq_level, n_jobs=-1) Y_hat_df_level = fcst.forecast(df=Y_level_train[["ds", "unique_id", "y"]], h=horizon_level, level=[80, 90]) # Add the test set to the forecast Y_hat_df_level = Y_hat_df_level.merge(Y_level_test, on=["ds", "unique_id"], how="left") # Put cols in the right order (for readability) Y_hat_cols = id_cols + [col for col in Y_hat_df_level.columns if col not in id_cols] Y_hat_df_level = Y_hat_df_level[Y_hat_cols] # Append the forecast to the list Y_hat_dfs.append(Y_hat_df_level) Y_hat_df = pd.concat(Y_hat_dfs, ignore_index=True) ``` ### 3c. Reconcile forecasts We can use the `HierarchicalReconciliation` class to reconcile the forecasts. In this example we use `BottomUp` and `MinTrace`. Note that we have to set `temporal=True` in the `reconcile` function. Note that temporal reconcilation currently isn’t supported for insample reconciliation methods, such as `MinTrace(method='mint_shrink')`. ```python theme={null} from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.core import HierarchicalReconciliation ``` ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method="ols"), ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, S_df=S_test_df, tags=tags_test, temporal=True, level=[80, 90]) ``` ## 4. Evaluation The `HierarchicalForecast` package includes the `evaluate` function to evaluate the different hierarchies. We evaluate the temporally aggregated forecasts *across all temporal aggregations*. ```python theme={null} from hierarchicalforecast.evaluation import evaluate from utilsforecast.losses import mae, scaled_crps ``` ```python theme={null} evaluation = evaluate(df = Y_rec_df.drop(columns = 'unique_id'), tags = tags_test, metrics = [mae, scaled_crps], level = [80, 90], id_col='temporal_id') evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(ols)'] numeric_cols = evaluation.select_dtypes(include="number").columns evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.3}'.format).astype(np.float64) ``` ```python theme={null} evaluation ``` | | level | metric | Base | BottomUp | MinTrace(ols) | | - | ---------- | ------------ | ------- | -------- | ------------- | | 0 | year | mae | 47.0000 | 50.8000 | 46.7000 | | 1 | year | scaled\_crps | 0.0562 | 0.0620 | 0.0666 | | 2 | semiannual | mae | 29.5000 | 30.5000 | 29.1000 | | 3 | semiannual | scaled\_crps | 0.0643 | 0.0681 | 0.0727 | | 4 | quarter | mae | 19.4000 | 19.4000 | 18.7000 | | 5 | quarter | scaled\_crps | 0.0876 | 0.0876 | 0.0864 | | 6 | Overall | mae | 26.2000 | 27.1000 | 25.7000 | | 7 | Overall | scaled\_crps | 0.0765 | 0.0784 | 0.0797 | `MinTrace(ols)` is the best overall point method, scoring the lowest `mae` on the `year` and `semiannual` aggregated forecasts as well as the `quarter` bottom-level aggregated forecasts. However, the `Base` method is better overall on the probabilistic measure `crps`, where it scores the lowest, indicating that the uncertainty levels predicted with the `Base` method are better in this example. ## Appendix: plotting the S matrix ```python theme={null} from hierarchicalforecast.utils import HierarchicalPlot ``` We plot our summing matrix for the test set. It’s fairly straightforward: there are two years in the test set, consisting of 4 quarters each. \* The first row of the `S` matrix shows how the aggregation `2016` can be obtained by summing the 4 quarters in 2016. \* The second row of the `S` matrix shows how the aggregation `2017` can be obtained by summing the 4 quarters in 2017. \* The next 4 rows show how the semi-annual aggregations can be obtained. \* The final rows are the identity matrix for each quarter, denoting the bottom temporal level (each quarter). ```python theme={null} hplot = HierarchicalPlot(S=S_test_df, tags=tags_test, S_id_col="temporal_id") hplot.plot_summing_matrix() ``` # Geographical Aggregation (Prison Population) Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/australianprisonpopulation.html > Geographical Hierarchical Forecasting on Australian Prison Population > Data In many applications, a set of time series is hierarchically organized. Examples include the presence of geographic levels, products, or categories that define different types of aggregations. In such scenarios, forecasters are often required to provide predictions for all disaggregate and aggregate series. A natural desire is for those predictions to be **“coherent”**, that is, for the bottom series to add up precisely to the forecasts of the aggregated series. In this notebook we present an example on how to use `HierarchicalForecast` to produce coherent forecasts between geographical levels. We will use the Australian Prison Population dataset. We will first load the dataset and produce base forecasts using an `ETS` model from `StatsForecast`, and then reconciliate the forecasts with several reconciliation algorithms from `HierarchicalForecast`. Finally, we show the performance is comparable with the results reported by the [Forecasting: Principles and Practice](https://otexts.com/fpp3/tourism.html) which uses the R package [fable](https://github.com/tidyverts/fable). You can run these experiments using CPU or GPU with Google Colab. Open In Colab ```python theme={null} !pip install hierarchicalforecast statsforecast ``` ## 1. Load and Process Data The dataset only contains the time series at the lowest level, so we need to create the time series for all hierarchies. ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} Y_df = pd.read_csv('https://OTexts.com/fpp3/extrafiles/prison_population.csv', storage_options={'User-Agent': 'Mozilla/5.0'}) Y_df = Y_df.rename({'Count': 'y', 'Date': 'ds'}, axis=1) Y_df.insert(0, 'Country', 'Australia') Y_df = Y_df[['Country', 'State', 'Gender', 'Legal', 'Indigenous', 'ds', 'y']] Y_df['ds'] = pd.to_datetime(Y_df['ds']) + pd.DateOffset(months=1) Y_df.head() ``` | | Country | State | Gender | Legal | Indigenous | ds | y | | - | --------- | ----- | ------ | --------- | ---------- | ---------- | - | | 0 | Australia | ACT | Female | Remanded | ATSI | 2005-04-01 | 0 | | 1 | Australia | ACT | Female | Remanded | Non-ATSI | 2005-04-01 | 2 | | 2 | Australia | ACT | Female | Sentenced | ATSI | 2005-04-01 | 0 | | 3 | Australia | ACT | Female | Sentenced | Non-ATSI | 2005-04-01 | 5 | | 4 | Australia | ACT | Male | Remanded | ATSI | 2005-04-01 | 7 | The dataset can be grouped in the following grouped structure. ```python theme={null} hiers = [ ['Country'], ['Country', 'State'], ['Country', 'Gender'], ['Country', 'Legal'], ['Country', 'State', 'Gender', 'Legal'] ] ``` Using the `aggregate` function from `HierarchicalForecast` we can get the full set of time series. ```python theme={null} from hierarchicalforecast.utils import aggregate ``` ```python theme={null} Y_df, S_df, tags = aggregate(Y_df, hiers) Y_df['y'] = Y_df['y']/1e3 ``` ```python theme={null} Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ------ | | 0 | Australia | 2005-04-01 | 24.296 | | 1 | Australia | 2005-07-01 | 24.643 | | 2 | Australia | 2005-10-01 | 24.511 | | 3 | Australia | 2006-01-01 | 24.393 | | 4 | Australia | 2006-04-01 | 24.524 | ```python theme={null} S_df.iloc[:5, :5] ``` | | unique\_id | Australia/ACT/Female/Remanded | Australia/ACT/Female/Sentenced | Australia/ACT/Male/Remanded | Australia/ACT/Male/Sentenced | | - | ------------- | ----------------------------- | ------------------------------ | --------------------------- | ---------------------------- | | 0 | Australia | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | Australia/ACT | 1.0 | 1.0 | 1.0 | 1.0 | | 2 | Australia/NSW | 0.0 | 0.0 | 0.0 | 0.0 | | 3 | Australia/NT | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | Australia/QLD | 0.0 | 0.0 | 0.0 | 0.0 | ```python theme={null} tags ``` ```text theme={null} {'Country': array(['Australia'], dtype=object), 'Country/State': array(['Australia/ACT', 'Australia/NSW', 'Australia/NT', 'Australia/QLD', 'Australia/SA', 'Australia/TAS', 'Australia/VIC', 'Australia/WA'], dtype=object), 'Country/Gender': array(['Australia/Female', 'Australia/Male'], dtype=object), 'Country/Legal': array(['Australia/Remanded', 'Australia/Sentenced'], dtype=object), 'Country/State/Gender/Legal': array(['Australia/ACT/Female/Remanded', 'Australia/ACT/Female/Sentenced', 'Australia/ACT/Male/Remanded', 'Australia/ACT/Male/Sentenced', 'Australia/NSW/Female/Remanded', 'Australia/NSW/Female/Sentenced', 'Australia/NSW/Male/Remanded', 'Australia/NSW/Male/Sentenced', 'Australia/NT/Female/Remanded', 'Australia/NT/Female/Sentenced', 'Australia/NT/Male/Remanded', 'Australia/NT/Male/Sentenced', 'Australia/QLD/Female/Remanded', 'Australia/QLD/Female/Sentenced', 'Australia/QLD/Male/Remanded', 'Australia/QLD/Male/Sentenced', 'Australia/SA/Female/Remanded', 'Australia/SA/Female/Sentenced', 'Australia/SA/Male/Remanded', 'Australia/SA/Male/Sentenced', 'Australia/TAS/Female/Remanded', 'Australia/TAS/Female/Sentenced', 'Australia/TAS/Male/Remanded', 'Australia/TAS/Male/Sentenced', 'Australia/VIC/Female/Remanded', 'Australia/VIC/Female/Sentenced', 'Australia/VIC/Male/Remanded', 'Australia/VIC/Male/Sentenced', 'Australia/WA/Female/Remanded', 'Australia/WA/Female/Sentenced', 'Australia/WA/Male/Remanded', 'Australia/WA/Male/Sentenced'], dtype=object)} ``` ### Split Train/Test sets We use the final two years (8 quarters) as test set. ```python theme={null} Y_test_df = Y_df.groupby('unique_id', as_index=False).tail(8) Y_train_df = Y_df.drop(Y_test_df.index) ``` ## 2. Computing base forecasts The following cell computes the **base forecasts** for each time series in `Y_df` using the `ETS` model. Observe that `Y_hat_df` contains the forecasts but they are not coherent. ```python theme={null} from statsforecast.models import AutoETS from statsforecast.core import StatsForecast ``` ```python theme={null} fcst = StatsForecast(models=[AutoETS(season_length=4, model='ZMZ')], freq='QS', n_jobs=-1) Y_hat_df = fcst.forecast(df=Y_train_df, h=8, fitted=True) Y_fitted_df = fcst.forecast_fitted_values() ``` ```python theme={null} Y_test_df ``` | | unique\_id | ds | y | | ---- | --------------------------- | ---------- | ------ | | 40 | Australia | 2015-04-01 | 35.271 | | 41 | Australia | 2015-07-01 | 35.921 | | 42 | Australia | 2015-10-01 | 36.067 | | 43 | Australia | 2016-01-01 | 36.983 | | 44 | Australia | 2016-04-01 | 37.830 | | ... | ... | ... | ... | | 2155 | Australia/WA/Male/Sentenced | 2016-01-01 | 3.894 | | 2156 | Australia/WA/Male/Sentenced | 2016-04-01 | 3.876 | | 2157 | Australia/WA/Male/Sentenced | 2016-07-01 | 3.969 | | 2158 | Australia/WA/Male/Sentenced | 2016-10-01 | 4.076 | | 2159 | Australia/WA/Male/Sentenced | 2017-01-01 | 4.088 | ```python theme={null} Y_train_df ``` | | unique\_id | ds | y | | ---- | --------------------------- | ---------- | ------ | | 0 | Australia | 2005-04-01 | 24.296 | | 1 | Australia | 2005-07-01 | 24.643 | | 2 | Australia | 2005-10-01 | 24.511 | | 3 | Australia | 2006-01-01 | 24.393 | | 4 | Australia | 2006-04-01 | 24.524 | | ... | ... | ... | ... | | 2147 | Australia/WA/Male/Sentenced | 2014-01-01 | 3.614 | | 2148 | Australia/WA/Male/Sentenced | 2014-04-01 | 3.635 | | 2149 | Australia/WA/Male/Sentenced | 2014-07-01 | 3.692 | | 2150 | Australia/WA/Male/Sentenced | 2014-10-01 | 3.726 | | 2151 | Australia/WA/Male/Sentenced | 2015-01-01 | 3.780 | ## 3. Reconcile forecasts The following cell makes the previous forecasts coherent using the `HierarchicalReconciliation` class. Since the hierarchy structure is not strict, we can’t use methods such as `TopDown` or `MiddleOut`. In this example we use `BottomUp` and `MinTrace`. ```python theme={null} from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.core import HierarchicalReconciliation ``` ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method='mint_shrink') ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags) ``` The dataframe `Y_rec_df` contains the reconciled forecasts. ```python theme={null} Y_rec_df.head() ``` | | unique\_id | ds | AutoETS | AutoETS/BottomUp | AutoETS/MinTrace\_method-mint\_shrink | | - | ---------- | ---------- | --------- | ---------------- | ------------------------------------- | | 0 | Australia | 2015-04-01 | 34.799497 | 34.946476 | 34.923548 | | 1 | Australia | 2015-07-01 | 35.192638 | 35.410342 | 35.432421 | | 2 | Australia | 2015-10-01 | 35.188216 | 35.580849 | 35.473386 | | 3 | Australia | 2016-01-01 | 35.888628 | 35.951878 | 35.939526 | | 4 | Australia | 2016-04-01 | 36.045437 | 36.416829 | 36.245158 | ## 4. Evaluation The `HierarchicalForecast` package includes the `HierarchicalEvaluation` class to evaluate the different hierarchies and also is capable of compute scaled metrics compared to a benchmark model. ```python theme={null} from hierarchicalforecast.evaluation import evaluate from utilsforecast.losses import mase from functools import partial ``` ```python theme={null} eval_tags = {} eval_tags['Total'] = tags['Country'] eval_tags['State'] = tags['Country/State'] eval_tags['Legal status'] = tags['Country/Legal'] eval_tags['Gender'] = tags['Country/Gender'] eval_tags['Bottom'] = tags['Country/State/Gender/Legal'] df = Y_rec_df.merge(Y_test_df, on=['unique_id', 'ds']) evaluation = evaluate(df = df, tags = eval_tags, train_df = Y_train_df, metrics = [partial(mase, seasonality=4)]) numeric_cols = evaluation.select_dtypes(include="number").columns evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64) evaluation.rename(columns={'AutoETS': 'Base'}, inplace=True) ``` ```python theme={null} evaluation ``` | | level | metric | Base | AutoETS/BottomUp | AutoETS/MinTrace\_method-mint\_shrink | | - | ------------ | ------ | ---- | ---------------- | ------------------------------------- | | 0 | Total | mase | 1.36 | 1.07 | 1.17 | | 1 | State | mase | 1.53 | 1.55 | 1.59 | | 2 | Legal status | mase | 2.40 | 2.48 | 2.38 | | 3 | Gender | mase | 1.08 | 0.82 | 0.93 | | 4 | Bottom | mase | 2.16 | 2.16 | 2.14 | | 5 | Overall | mase | 1.99 | 1.98 | 1.98 | ### Fable Comparison Observe that we can recover the results reported by the [Forecasting: Principles and Practice](https://otexts.com/fpp3/prison.html) book. The original results were calculated using the R package [fable](https://github.com/tidyverts/fable).
Fable’s reconciliation results
Fable’s reconciliation results
### References * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) * [Rob Hyndman, Alan Lee, Earo Wang, Shanika Wickramasuriya, and Maintainer Earo Wang (2021). “hts: Hierarchical and Grouped Time Series”. URL https://CRAN.R-project.org/package=hts. R package version 0.3.1.](https://cran.r-project.org/web/packages/hts/index.html) * [Mitchell O’Hara-Wild, Rob Hyndman, Earo Wang, Gabriel Caceres, Tim-Gunnar Hensel, and Timothy Hyndman (2021). “fable: Forecasting Models for Tidy Time Series”. URL https://CRAN.R-project.org/package=fable. R package version 6.0.2.](https://CRAN.R-project.org/package=fable) # Exogenous Variables Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/exogenousvariables.html > Aggregating Exogenous Variables for Hierarchical Forecasting When building forecasting models with exogenous (external) features, it is important that these features are also coherently aggregated across the hierarchy — just like the target variable. For example, if `marketing_spend` is available at the bottom level, the aggregated series at the state level should contain the *sum* of marketing spend across its child regions; while a rate-like variable such as `cpi` should be *averaged*. The `aggregate` function in `HierarchicalForecast` supports this through its `exog_vars` parameter: a dictionary mapping column names to their aggregation strategy (e.g., `'sum'`, `'mean'`, `'min'`, `'max'`). This lets you aggregate exogenous variables alongside the target in a single call, and then use them as inputs to models that support exogenous regressors (e.g., `AutoARIMA`). In this notebook we demonstrate this workflow on the Australian Domestic Tourism dataset and compare forecasting performance with and without exogenous variables. You can run these experiments using CPU or GPU with Google Colab. Open In Colab ```python theme={null} !pip install hierarchicalforecast statsforecast ``` ## 1. Load Data and Add Exogenous Variables We start from the [Tourism](https://otexts.com/fpp3/tourism.html) dataset and add two synthetic exogenous variables that illustrate different aggregation needs: * **`cpi`** — Consumer Price Index. This is a national-level economic indicator, identical across regions within each quarter. Because it is a rate (not a count), it should be *averaged* when aggregating. * **`marketing_spend`** — Regional marketing budget. This varies by state and grows over time. Because it represents a total amount, it should be *summed* when aggregating. ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') Y_df = Y_df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) Y_df.insert(0, 'Country', 'Australia') Y_df = Y_df[['Country', 'Region', 'State', 'Purpose', 'ds', 'y']] Y_df['ds'] = Y_df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True) Y_df['ds'] = pd.PeriodIndex(Y_df['ds'], freq='Q').to_timestamp() Y_df.head() ``` | | Country | Region | State | Purpose | ds | y | | - | --------- | -------- | --------------- | -------- | ---------- | ---------- | | 0 | Australia | Adelaide | South Australia | Business | 1998-01-01 | 135.077690 | | 1 | Australia | Adelaide | South Australia | Business | 1998-04-01 | 109.987316 | | 2 | Australia | Adelaide | South Australia | Business | 1998-07-01 | 166.034687 | | 3 | Australia | Adelaide | South Australia | Business | 1998-10-01 | 127.160464 | | 4 | Australia | Adelaide | South Australia | Business | 1999-01-01 | 137.448533 | ```python theme={null} np.random.seed(42) # CPI: a national-level index, same across all bottom-level series per quarter dates = Y_df['ds'].unique() cpi_values = 100 + np.cumsum(np.random.normal(0.5, 0.3, len(dates))) cpi_map = dict(zip(dates, cpi_values)) Y_df['cpi'] = Y_df['ds'].map(cpi_map) # Marketing spend: varies by state, grows over time state_spend_base = {s: np.random.uniform(50, 200) for s in Y_df['State'].unique()} Y_df['marketing_spend'] = Y_df.apply( lambda r: state_spend_base[r['State']] * (1 + 0.02 * (r['ds'].year - 1998)) + np.random.normal(0, 5), axis=1 ).round(1) Y_df.head() ``` | | Country | Region | State | Purpose | ds | y | cpi | marketing\_spend | | - | --------- | -------- | --------------- | -------- | ---------- | ---------- | ---------- | ---------------- | | 0 | Australia | Adelaide | South Australia | Business | 1998-01-01 | 135.077690 | 100.649014 | 190.7 | | 1 | Australia | Adelaide | South Australia | Business | 1998-04-01 | 109.987316 | 101.107535 | 187.8 | | 2 | Australia | Adelaide | South Australia | Business | 1998-07-01 | 166.034687 | 101.801842 | 183.5 | | 3 | Australia | Adelaide | South Australia | Business | 1998-10-01 | 127.160464 | 102.758750 | 188.7 | | 4 | Australia | Adelaide | South Australia | Business | 1999-01-01 | 137.448533 | 103.188504 | 190.3 | ## 2. Aggregate with Exogenous Variables The `exog_vars` parameter of `aggregate` takes a dictionary where: - **keys** are the column names of the exogenous variables. - **values** are the aggregation function(s) to apply — any valid Pandas aggregation function name such as `'sum'`, `'mean'`, `'min'`, `'max'`, `'std'`. The aggregated columns are named `{column}_{function}` (e.g., `cpi_mean`, `marketing_spend_sum`). ```python theme={null} spec = [ ['Country'], ['Country', 'State'], ['Country', 'Purpose'], ['Country', 'State', 'Region'], ['Country', 'State', 'Purpose'], ['Country', 'State', 'Region', 'Purpose'] ] ``` ```python theme={null} from hierarchicalforecast.utils import aggregate Y_agg, S_df, tags = aggregate( Y_df, spec, exog_vars={ 'cpi': 'mean', # rate variable -> average 'marketing_spend': 'sum', # additive variable -> sum } ) ``` The result is a `Y_agg` DataFrame that contains both the aggregated target (`y`) and the aggregated exogenous variables (`cpi_mean`, `marketing_spend_sum`) for every level of the hierarchy. ```python theme={null} # Top level: all of Australia Y_agg[Y_agg['unique_id'] == 'Australia'].head() ``` | | unique\_id | ds | y | cpi\_mean | marketing\_spend\_sum | | - | ---------- | ---------- | ------------ | ---------- | --------------------- | | 0 | Australia | 1998-01-01 | 23182.197269 | 100.649014 | 36528.4 | | 1 | Australia | 1998-04-01 | 20323.380067 | 101.107535 | 36438.2 | | 2 | Australia | 1998-07-01 | 19826.640511 | 101.801842 | 36613.1 | | 3 | Australia | 1998-10-01 | 20830.129891 | 102.758750 | 36648.8 | | 4 | Australia | 1999-01-01 | 22087.353380 | 103.188504 | 37338.3 | ```python theme={null} # State level: Victoria Y_agg[Y_agg['unique_id'] == 'Australia/Victoria'].head() ``` | | unique\_id | ds | y | cpi\_mean | marketing\_spend\_sum | | --- | ------------------ | ---------- | ----------- | ---------- | --------------------- | | 560 | Australia/Victoria | 1998-01-01 | 6010.424491 | 100.649014 | 13711.9 | | 561 | Australia/Victoria | 1998-04-01 | 4795.246755 | 101.107535 | 13640.1 | | 562 | Australia/Victoria | 1998-07-01 | 4316.845170 | 101.801842 | 13742.9 | | 563 | Australia/Victoria | 1998-10-01 | 4674.829118 | 102.758750 | 13768.8 | | 564 | Australia/Victoria | 1999-01-01 | 5304.334195 | 103.188504 | 14047.9 | ```python theme={null} # Bottom level: a specific Region/Purpose combination Y_agg[Y_agg['unique_id'] == 'Australia/Victoria/Melbourne/Holiday'].head() ``` | | unique\_id | ds | y | cpi\_mean | marketing\_spend\_sum | | ----- | ------------------------------------ | ---------- | ---------- | ---------- | --------------------- | | 29920 | Australia/Victoria/Melbourne/Holiday | 1998-01-01 | 427.827800 | 100.649014 | 160.2 | | 29921 | Australia/Victoria/Melbourne/Holiday | 1998-04-01 | 424.191925 | 101.107535 | 171.2 | | 29922 | Australia/Victoria/Melbourne/Holiday | 1998-07-01 | 449.551508 | 101.801842 | 168.8 | | 29923 | Australia/Victoria/Melbourne/Holiday | 1998-10-01 | 450.699582 | 102.758750 | 172.7 | | 29924 | Australia/Victoria/Melbourne/Holiday | 1999-01-01 | 412.358542 | 103.188504 | 175.8 | Notice that: - `cpi_mean` is the same at every level (since CPI is identical across bottom series within each quarter). - `marketing_spend_sum` grows as we move up the hierarchy, because it sums the spend of all child series. ### Multiple aggregations per variable You can also apply multiple aggregation functions to a single variable by passing a list of strings. ```python theme={null} Y_multi, _, _ = aggregate( Y_df, spec, exog_vars={ 'marketing_spend': ['sum', 'mean', 'std'], } ) Y_multi[Y_multi['unique_id'] == 'Australia/Victoria'][ ['unique_id', 'ds', 'y', 'marketing_spend_sum', 'marketing_spend_mean', 'marketing_spend_std'] ].head() ``` | | unique\_id | ds | y | marketing\_spend\_sum | marketing\_spend\_mean | marketing\_spend\_std | | --- | ------------------ | ---------- | ----------- | --------------------- | ---------------------- | --------------------- | | 560 | Australia/Victoria | 1998-01-01 | 6010.424491 | 13711.9 | 163.236905 | 4.878618 | | 561 | Australia/Victoria | 1998-04-01 | 4795.246755 | 13640.1 | 162.382143 | 5.116737 | | 562 | Australia/Victoria | 1998-07-01 | 4316.845170 | 13742.9 | 163.605952 | 4.361285 | | 563 | Australia/Victoria | 1998-10-01 | 4674.829118 | 13768.8 | 163.914286 | 5.038578 | | 564 | Australia/Victoria | 1999-01-01 | 5304.334195 | 14047.9 | 167.236905 | 5.759074 | ## 3. Train/Test Split We use the final two years (8 quarters) as test set. We also aggregate a version **without** exogenous variables to compare later. ```python theme={null} Y_agg_no_exog, S_df_no_exog, tags_no_exog = aggregate(Y_df, spec) ``` ```python theme={null} horizon = 8 # With exogenous Y_test_df = Y_agg.groupby('unique_id', as_index=False).tail(horizon) Y_train_df = Y_agg.drop(Y_test_df.index) X_test_df = Y_test_df[['unique_id', 'ds', 'cpi_mean', 'marketing_spend_sum']] # Without exogenous Y_test_no_exog = Y_agg_no_exog.groupby('unique_id', as_index=False).tail(horizon) Y_train_no_exog = Y_agg_no_exog.drop(Y_test_no_exog.index) print(f'Train: {Y_train_df.shape[0]:,} rows, Test: {Y_test_df.shape[0]:,} rows') ``` ```text theme={null} Train: 30,600 rows, Test: 3,400 rows ``` ## 4. Base Forecasts We use `AutoARIMA` from `StatsForecast`, which supports exogenous regressors. Any column in the training data besides `unique_id`, `ds`, and `y` is treated as an exogenous feature. Future values are passed via `X_df`. We produce base forecasts **with** and **without** the exogenous variables for comparison. ```python theme={null} from statsforecast.models import AutoARIMA from statsforecast.core import StatsForecast ``` ```text theme={null} /home/osprangers/Repositories/hierarchicalforecast/.venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm ``` ```python theme={null} # With exogenous variables fcst_exog = StatsForecast(models=[AutoARIMA(season_length=4)], freq='QS', n_jobs=-1) Y_hat_exog = fcst_exog.forecast(df=Y_train_df, h=horizon, X_df=X_test_df, fitted=True) Y_fitted_exog = fcst_exog.forecast_fitted_values() Y_hat_exog.head() ``` | | unique\_id | ds | AutoARIMA | | - | ---------- | ---------- | ------------ | | 0 | Australia | 2016-01-01 | 26166.873073 | | 1 | Australia | 2016-04-01 | 24907.791984 | | 2 | Australia | 2016-07-01 | 24537.521142 | | 3 | Australia | 2016-10-01 | 25631.143759 | | 4 | Australia | 2017-01-01 | 26987.070921 | ```python theme={null} # Without exogenous variables fcst_no_exog = StatsForecast(models=[AutoARIMA(season_length=4)], freq='QS', n_jobs=-1) Y_hat_no_exog = fcst_no_exog.forecast(df=Y_train_no_exog, h=horizon, fitted=True) Y_fitted_no_exog = fcst_no_exog.forecast_fitted_values() Y_hat_no_exog.head() ``` | | unique\_id | ds | AutoARIMA | | - | ---------- | ---------- | ------------ | | 0 | Australia | 2016-01-01 | 26212.553553 | | 1 | Australia | 2016-04-01 | 25033.667125 | | 2 | Australia | 2016-07-01 | 24507.027198 | | 3 | Australia | 2016-10-01 | 25598.928613 | | 4 | Australia | 2017-01-01 | 26982.576796 | ## 5. Reconcile Forecasts Reconciliation works exactly the same way regardless of whether exogenous variables were used to produce the base forecasts. The reconciliation step adjusts the base forecasts to be coherent across the hierarchy. ```python theme={null} from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.core import HierarchicalReconciliation reconcilers = [ BottomUp(), MinTrace(method='mint_shrink'), ] ``` ```python theme={null} # With exogenous hrec_exog = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_exog = hrec_exog.reconcile( Y_hat_df=Y_hat_exog, Y_df=Y_fitted_exog, S_df=S_df, tags=tags ) Y_rec_exog.head() ``` | | unique\_id | ds | AutoARIMA | AutoARIMA/BottomUp | AutoARIMA/MinTrace\_method-mint\_shrink | | - | ---------- | ---------- | ------------ | ------------------ | --------------------------------------- | | 0 | Australia | 2016-01-01 | 26166.873073 | 24461.292072 | 25436.706581 | | 1 | Australia | 2016-04-01 | 24907.791984 | 22795.931817 | 24076.317511 | | 2 | Australia | 2016-07-01 | 24537.521142 | 22151.772435 | 23272.424691 | | 3 | Australia | 2016-10-01 | 25631.143759 | 22936.268850 | 24404.217321 | | 4 | Australia | 2017-01-01 | 26987.070921 | 24292.353932 | 25663.420421 | ```python theme={null} # Without exogenous hrec_no_exog = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_no_exog = hrec_no_exog.reconcile( Y_hat_df=Y_hat_no_exog, Y_df=Y_fitted_no_exog, S_df=S_df_no_exog, tags=tags_no_exog ) Y_rec_no_exog.head() ``` | | unique\_id | ds | AutoARIMA | AutoARIMA/BottomUp | AutoARIMA/MinTrace\_method-mint\_shrink | | - | ---------- | ---------- | ------------ | ------------------ | --------------------------------------- | | 0 | Australia | 2016-01-01 | 26212.553553 | 24495.919988 | 25322.734082 | | 1 | Australia | 2016-04-01 | 25033.667125 | 22660.036262 | 23873.068038 | | 2 | Australia | 2016-07-01 | 24507.027198 | 22307.887506 | 23396.655072 | | 3 | Australia | 2016-10-01 | 25598.928613 | 22853.294863 | 24303.043112 | | 4 | Australia | 2017-01-01 | 26982.576796 | 24264.065071 | 25520.638165 | ## 6. Evaluation We compare RMSE and MASE across hierarchy levels for the models **with** and **without** exogenous variables. ```python theme={null} from hierarchicalforecast.evaluation import evaluate from utilsforecast.losses import rmse, mase from functools import partial eval_tags = { 'Total': tags['Country'], 'Purpose': tags['Country/Purpose'], 'State': tags['Country/State'], 'Regions': tags['Country/State/Region'], 'Bottom': tags['Country/State/Region/Purpose'], } ``` ```python theme={null} # Evaluate with exogenous (drop exog columns before merge to avoid them being treated as models) df_exog = Y_rec_exog.merge(Y_test_df[['unique_id', 'ds', 'y']], on=['unique_id', 'ds']) eval_exog = evaluate( df=df_exog, tags=eval_tags, train_df=Y_train_df[['unique_id', 'ds', 'y']], metrics=[rmse, partial(mase, seasonality=4)], ) eval_exog.columns = ['level', 'metric', 'Base (w/ exog)', 'BottomUp (w/ exog)', 'MinTrace (w/ exog)'] ``` ```python theme={null} # Evaluate without exogenous df_no_exog = Y_rec_no_exog.merge(Y_test_no_exog, on=['unique_id', 'ds']) eval_no_exog = evaluate( df=df_no_exog, tags=eval_tags, train_df=Y_train_no_exog, metrics=[rmse, partial(mase, seasonality=4)], ) eval_no_exog.columns = ['level', 'metric', 'Base (no exog)', 'BottomUp (no exog)', 'MinTrace (no exog)'] ``` ```python theme={null} comparison = eval_exog.merge(eval_no_exog, on=['level', 'metric']) comparison = comparison[[ 'level', 'metric', 'Base (no exog)', 'Base (w/ exog)', 'BottomUp (no exog)', 'BottomUp (w/ exog)', 'MinTrace (no exog)', 'MinTrace (w/ exog)', ]] numeric_cols = comparison.select_dtypes(include='number').columns comparison[numeric_cols] = comparison[numeric_cols].map('{:.2f}'.format).astype(np.float64) ``` ### RMSE ```python theme={null} comparison.query('metric == "rmse"') ``` | | level | metric | Base (no exog) | Base (w/ exog) | BottomUp (no exog) | BottomUp (w/ exog) | MinTrace (no exog) | MinTrace (w/ exog) | | -- | ------- | ------ | -------------- | -------------- | ------------------ | ------------------ | ------------------ | ------------------ | | 0 | Total | rmse | 773.97 | 731.03 | 3247.04 | 3218.18 | 2029.46 | 1946.96 | | 2 | Purpose | rmse | 471.46 | 516.06 | 871.70 | 859.13 | 571.76 | 545.22 | | 4 | State | rmse | 257.90 | 251.32 | 434.51 | 431.99 | 308.33 | 298.49 | | 6 | Regions | rmse | 56.53 | 55.53 | 58.19 | 57.79 | 46.43 | 46.08 | | 8 | Bottom | rmse | 20.39 | 20.44 | 20.39 | 20.44 | 18.13 | 18.22 | | 10 | Overall | rmse | 38.72 | 38.78 | 53.00 | 52.71 | 40.26 | 39.58 | ### MASE ```python theme={null} comparison.query('metric == "mase"') ``` | | level | metric | Base (no exog) | Base (w/ exog) | BottomUp (no exog) | BottomUp (w/ exog) | MinTrace (no exog) | MinTrace (w/ exog) | | -- | ------- | ------ | -------------- | -------------- | ------------------ | ------------------ | ------------------ | ------------------ | | 1 | Total | mase | 0.76 | 0.73 | 3.39 | 3.36 | 2.01 | 1.91 | | 3 | Purpose | mase | 1.14 | 1.41 | 2.48 | 2.49 | 1.45 | 1.48 | | 5 | State | mase | 1.46 | 1.26 | 1.97 | 1.93 | 1.42 | 1.31 | | 7 | Regions | mase | 1.21 | 1.20 | 1.25 | 1.24 | 1.02 | 1.00 | | 9 | Bottom | mase | 1.02 | 1.02 | 1.02 | 1.02 | 0.94 | 0.95 | | 11 | Overall | mase | 1.06 | 1.06 | 1.10 | 1.10 | 0.97 | 0.97 | ### References * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) # Hierarchical Forecasting at Scale Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/hierarchicalforecastingatscale.html > Practical tips to improve performance when reconciling large > hierarchies When working with hierarchies containing thousands or millions of time series, default settings can lead to slow reconciliation and high memory usage. This guide covers concrete steps to improve performance. We will cover: 1. Using Polars instead of Pandas 2. Using sparse S matrices via `aggregate(..., sparse_s=True)` 3. Telling the library your data is balanced 4. Using sparse reconciliation methods 5. Choosing the right reconciliation method for your scale 6. Parallelizing non-negative reconciliation 7. Avoiding unnecessary computation 8. Profiling your pipeline ## 1. Libraries ```python theme={null} !pip install hierarchicalforecast statsforecast datasetsforecast ``` ```python theme={null} import time import numpy as np import polars as pl from datasetsforecast.hierarchical import HierarchicalData, HierarchicalInfo from statsforecast.core import StatsForecast from statsforecast.models import AutoARIMA, Naive from hierarchicalforecast.core import HierarchicalReconciliation from hierarchicalforecast.methods import ( BottomUp, BottomUpSparse, MinTrace, MinTraceSparse, ) from hierarchicalforecast.utils import CodeTimer ``` ## 2. Load Data We use the `TourismSmall` dataset for demonstration. The same principles apply to much larger hierarchies. ```python theme={null} group_name = 'TourismSmall' group = HierarchicalInfo.get_group(group_name) Y_df, S_df, tags = HierarchicalData.load('./data', group_name) # Convert to Polars (see Tip 1 below) Y_df = pl.from_pandas(Y_df).with_columns(pl.col('ds').cast(pl.Date)) S_df = pl.from_pandas(S_df.reset_index(names='unique_id')) # Train/test split Y_test_df = Y_df.group_by('unique_id').tail(group.horizon) Y_train_df = Y_df.filter(pl.col('ds') < Y_test_df['ds'].min()) ``` ## 3. Base Forecasts ```python theme={null} fcst = StatsForecast( models=[AutoARIMA(season_length=group.seasonality), Naive()], freq='1q', n_jobs=-1, ) Y_hat_df = fcst.forecast(df=Y_train_df, h=group.horizon, fitted=True) Y_fitted_df = fcst.forecast_fitted_values() ``` ### Tip 1: Use Polars instead of Pandas `HierarchicalForecast` supports both Pandas and Polars DataFrames transparently via [Narwhals](https://narwhals-dev.github.io/narwhals/). Polars is generally faster for the DataFrame operations used internally (sorting, joining, pivoting) and uses less memory due to Apache Arrow columnar storage. Simply pass Polars DataFrames to `reconcile()` — no code changes needed beyond the data loading step: ```python theme={null} # Instead of pandas DataFrames, convert to Polars: import polars as pl Y_df = pl.from_pandas(Y_df) S_df = pl.from_pandas(S_df) ``` If you use `aggregate()` to build your hierarchy, pass a Polars DataFrame directly: ```python theme={null} from hierarchicalforecast.utils import aggregate Y_df, S_df, tags = aggregate(df=pl.from_pandas(df), spec=[...]) ``` ### Tip 2: Use `sparse_s=True` in `aggregate()` By default, `aggregate()` returns the summing matrix **S** as a dense DataFrame. For large hierarchies this can be expensive — a hierarchy with 100k series and 50k bottom-level series produces a 100k x 50k dense matrix (\~40 GB in float64). Setting `sparse_s=True` returns an `SMatrix` object that keeps **S** as a `scipy.sparse` matrix throughout the pipeline, materialising dense arrays only when needed: ```python theme={null} from hierarchicalforecast.utils import aggregate Y_df, S_df, tags = aggregate(df=df, spec=spec, sparse_s=True) # S_df is now an SMatrix — pass it directly to reconcile() ``` `SMatrix` works transparently with both Pandas and Polars inputs, and is accepted directly by `reconcile()`: ```python theme={null} hrec = HierarchicalReconciliation(reconcilers=[MinTraceSparse(method='ols')]) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, S_df=S_df, tags=tags) ``` **When to use**: Always beneficial for large hierarchies (thousands of bottom-level series). For small hierarchies the overhead is negligible either way, so `sparse_s=True` is a safe default. ### Tip 3: Set `is_balanced=True` If all your time series have the same number of observations (same start and end dates), set `is_balanced=True` in `reconcile()`. This skips an expensive `pivot()` operation and uses a fast `reshape` instead. ```python theme={null} Y_rec_df = hrec.reconcile( Y_hat_df=Y_hat_df, S_df=S_df, tags=tags, is_balanced=True, # <-- skip pivot, use fast reshape ) ``` **When can you use this?** Most hierarchical datasets are balanced — every series has observations at every time step. If you built your hierarchy with `aggregate()`, the result is always balanced. ```python theme={null} # Demonstrate the speedup from is_balanced reconcilers = [BottomUp()] hrec = HierarchicalReconciliation(reconcilers=reconcilers) with CodeTimer('is_balanced=False (default)', verbose=True): _ = hrec.reconcile( Y_hat_df=Y_hat_df, S_df=S_df, tags=tags, is_balanced=False, ) with CodeTimer('is_balanced=True', verbose=True): _ = hrec.reconcile( Y_hat_df=Y_hat_df, S_df=S_df, tags=tags, is_balanced=True, ) ``` ### Tip 4: Use Sparse Reconciliation Methods For large hierarchies, the dense summing matrix **S** (shape: `n_hiers × n_bottom`) can consume significant memory and slow down matrix operations. Sparse methods use `scipy.sparse` matrices and iterative solvers instead of dense linear algebra. The library provides sparse variants of the main methods: | Dense Method | Sparse Variant | Notes | | ------------------------------- | ------------------------------------- | -------------------------------- | | `BottomUp()` | `BottomUpSparse()` | Drop-in replacement | | `TopDown(...)` | `TopDownSparse(...)` | Strictly hierarchical data only | | `MiddleOut(...)` | `MiddleOutSparse(...)` | Strictly hierarchical data only | | `MinTrace(method='ols')` | `MinTraceSparse(method='ols')` | Uses iterative `bicgstab` solver | | `MinTrace(method='wls_struct')` | `MinTraceSparse(method='wls_struct')` | Uses iterative `bicgstab` solver | | `MinTrace(method='wls_var')` | `MinTraceSparse(method='wls_var')` | Uses iterative `bicgstab` solver | `MinTraceSparse` constructs a `scipy.sparse.linalg.LinearOperator` for the projection matrix **P** and solves the system iteratively with `bicgstab`, avoiding materialization of the full dense P matrix. **When to use sparse methods**: When your hierarchy has thousands of bottom-level series or more. For small hierarchies (\< 500 series), the overhead of sparse operations may negate the savings. **Combine with `sparse_s=True`**: For maximum benefit, use sparse methods together with `aggregate(..., sparse_s=True)` (Tip 2). This keeps the entire pipeline sparse — from S construction through reconciliation. ```python theme={null} # Compare dense vs sparse methods dense_reconcilers = [ BottomUp(), MinTrace(method='ols'), MinTrace(method='wls_struct'), ] sparse_reconcilers = [ BottomUpSparse(), MinTraceSparse(method='ols'), MinTraceSparse(method='wls_struct'), ] hrec_dense = HierarchicalReconciliation(reconcilers=dense_reconcilers) hrec_sparse = HierarchicalReconciliation(reconcilers=sparse_reconcilers) with CodeTimer('Dense methods', verbose=True): Y_rec_dense = hrec_dense.reconcile( Y_hat_df=Y_hat_df, S_df=S_df, tags=tags, is_balanced=True, ) with CodeTimer('Sparse methods', verbose=True): Y_rec_sparse = hrec_sparse.reconcile( Y_hat_df=Y_hat_df, S_df=S_df, tags=tags, is_balanced=True, ) ``` ### Tip 5: Choose the Right Reconciliation Method Reconciliation methods vary significantly in computational cost. Here is a rough ranking from fastest to slowest: | Speed | Method | Requires insample data? | Notes | | ------- | ------------------------------------------- | ----------------------- | -------------------------------------------------- | | Fastest | `BottomUp` / `BottomUpSparse` | No | Simple aggregation, no optimization | | Fast | `TopDown` / `TopDownSparse` | Depends on variant | Strictly hierarchical only | | Fast | `MinTrace(method='ols')` | No | Closed-form, `np.linalg.solve` | | Fast | `MinTrace(method='wls_struct')` | No | Closed-form, weighted by hierarchy structure | | Medium | `MinTraceSparse(method='ols'/'wls_struct')` | No | Iterative solver; better for very large S | | Medium | `MinTrace(method='wls_var')` | Yes | Diagonal covariance from residuals | | Slow | `MinTrace(method='mint_shrink')` | Yes | Full covariance with shrinkage (Numba-accelerated) | | Slow | `MinTrace(method='mint_cov')` | Yes | Full empirical covariance (Numba-accelerated) | | Slow | `ERM(method='closed')` | Yes | Pseudoinverse on large matrices | | Slowest | `ERM(method='reg')` | Yes | Lasso coordinate descent (iterative) | | +Cost | Any method with `nonnegative=True` | — | Adds a QP solve per horizon step | **Key takeaways**: - `BottomUp` and `MinTrace(method='ols')` are the cheapest and don’t require insample data (`Y_df`). - Methods requiring insample residuals (`wls_var`, `mint_shrink`, `mint_cov`) need the `Y_df` argument passed to `reconcile()`. Omitting `Y_df` when not needed saves computation. - For very large hierarchies, prefer `MinTraceSparse` over `MinTrace` to avoid dense matrix operations. - `mint_shrink` and `mint_cov` compute a full n×n covariance matrix — this scales quadratically with the number of series. ### Tip 6: Parallelize Non-Negative Reconciliation When using `nonnegative=True`, a quadratic programming (QP) problem is solved for each forecast horizon step. Use `num_threads` to parallelize these independent QP calls: ```python theme={null} MinTrace(method='ols', nonnegative=True, num_threads=4) MinTraceSparse(method='ols', nonnegative=True, num_threads=4) ``` The `num_threads` parameter controls the `ThreadPoolExecutor` pool size. Set it to the number of available CPU cores. Note that `num_threads` only takes effect when `nonnegative=True`. `MinTraceSparse` also offers a `qp` parameter (default `True`). Setting `qp=False` replaces the full QP solve with simple clipping of negative values — much faster but lower quality: ```python theme={null} # Fast non-negative approximation (clipping) MinTraceSparse(method='ols', nonnegative=True, qp=False) # Full QP solve with parallelism (more accurate) MinTraceSparse(method='ols', nonnegative=True, qp=True, num_threads=4) ``` ### Tip 7: Avoid Unnecessary Computation **Skip probabilistic forecasts when not needed.** If you only need point forecasts, don’t pass the `level` parameter: ```python theme={null} # Point forecasts only (fast) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, S_df=S_df, tags=tags) # With prediction intervals (slower) Y_rec_df = hrec.reconcile( Y_hat_df=Y_hat_df, S_df=S_df, tags=tags, Y_df=Y_fitted_df, level=[80, 95], intervals_method='normality', # cheapest interval method ) ``` If you do need intervals, `intervals_method='normality'` is the cheapest option. The `'bootstrap'` and `'permbu'` methods require many re-evaluations and are significantly slower. **Skip diagnostics in production.** The `diagnostics=True` flag computes coherence checks across all forecasts — useful for debugging but adds overhead: ```python theme={null} # During development Y_rec_df = hrec.reconcile(..., diagnostics=True) # In production Y_rec_df = hrec.reconcile(..., diagnostics=False) # default ``` **Don’t pass insample data when not needed.** Methods like `BottomUp`, `TopDown(method='forecast_proportions')`, `MinTrace(method='ols')`, and `MinTrace(method='wls_struct')` don’t use insample data. Omitting `Y_df` skips the data preparation for insample values: ```python theme={null} # These methods don't need Y_df reconcilers = [BottomUp(), MinTrace(method='ols')] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, S_df=S_df, tags=tags) # ^^^^^^ no Y_df argument needed ``` ### Tip 8: Profile Your Pipeline `HierarchicalForecast` provides built-in profiling tools to identify bottlenecks. **`execution_times` attribute**: After calling `reconcile()`, the `HierarchicalReconciliation` instance exposes a dictionary of per-method execution times: ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method='ols'), MinTrace(method='wls_var'), MinTrace(method='mint_shrink'), ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile( Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags, is_balanced=True, ) # Inspect per-method timing for method_name, elapsed in hrec.execution_times.items(): print(f'{method_name}: {elapsed:.4f}s') ``` ```text theme={null} AutoARIMA/BottomUp: 0.0008s Naive/BottomUp: 0.0005s AutoARIMA/MinTrace_method-ols: 0.0012s Naive/MinTrace_method-ols: 0.0010s AutoARIMA/MinTrace_method-wls_var: 0.0011s Naive/MinTrace_method-wls_var: 0.0009s AutoARIMA/MinTrace_method-mint_shrink: 0.0186s Naive/MinTrace_method-mint_shrink: 0.0122s ``` **`CodeTimer` context manager**: Use this to time any block of code in your pipeline: ```python theme={null} with CodeTimer('Full reconciliation pipeline', verbose=True): reconcilers = [MinTraceSparse(method='ols')] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile( Y_hat_df=Y_hat_df, S_df=S_df, tags=tags, is_balanced=True, ) ``` ## 5. Summary Checklist Here is a quick checklist to optimize your hierarchical forecasting pipeline: | Tip | Action | Impact | | ------------------ | ------------------------------------------------- | -------------------------------------------- | | Use Polars | Pass Polars DataFrames instead of Pandas | Faster DataFrame ops, lower memory | | `sparse_s=True` | Use `aggregate(..., sparse_s=True)` | Keeps S sparse, avoids dense materialisation | | `is_balanced=True` | Set when all series have equal length | Skips expensive pivot | | Sparse methods | Use `BottomUpSparse`, `MinTraceSparse`, etc. | Less memory, iterative solvers | | Right method | Start with `BottomUp` or `MinTrace(method='ols')` | Avoid unnecessary covariance computation | | `num_threads` | Set >1 when using `nonnegative=True` | Parallel QP solves | | Skip intervals | Omit `level` parameter for point forecasts | Avoids sampling/interval computation | | Skip `Y_df` | Omit when reconciler doesn’t need insample data | Skips data preparation | | Profile | Check `hrec.execution_times` and use `CodeTimer` | Find bottlenecks | ### References * [Wickramasuriya, S. L., Athanasopoulos, G., & Hyndman, R. J. (2019). “Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization”. Journal of the American Statistical Association, 114, 804-819.](https://robjhyndman.com/publications/mint/) * [Wickramasuriya, S.L., Turlach, B.A. & Hyndman, R.J. (2020). “Optimal non-negative forecast reconciliation”. Stat Comput 30, 1167-1182.](https://robjhyndman.com/publications/nnmint/) * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia.](https://otexts.com/fpp3/hierarchical.html) # Tutorials Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/index.html Click through to any of these tutorials to get started with `HierarchicalForecast`’s features. # Install | HierarchicalForecast Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/installation.html > Install HierachicalForecast with pip or conda We recommend using `uv` as Python package manager, for which you can find installation instructions [here](https://docs.astral.sh/uv/getting-started/installation/). You can then install the *released version* of `HierachicalForecast`: ```python theme={null} uv pip install hierarchicalforecast ``` Alternatively, you can directly install from the [Python package index](https://pypi.org) with: ```python theme={null} pip install hierarchicalforecast ``` or within a `conda` environment: ```python theme={null} conda install -c conda-forge hierarchicalforecast ``` > **Tip** > > We recommend installing your libraries inside a python virtual or > [conda > environment](https://docs.conda.io/projects/conda/en/latest/user-guide/install/macos.html). #### Installing from source We recommend using `uv` as Python package manager, for which you can find installation instructions [here](https://docs.astral.sh/uv/getting-started/installation/). 1. Clone the HierachicalForecast repo: ```bash theme={null} $ git clone https://github.com/Nixtla/hierarchicalforecast.git && cd hierarchicalforecast ``` 1. Create the environment: ```bash theme={null} $ uv venv --python 3.10 ``` 1. Activate the environment: * on MacOS / Linux: ```bash theme={null} $ source .venv/bin/activate ``` * on Windows: ```bash theme={null} $ .\.venv\Scripts\activate ``` 1. Install the dependencies and the library ```bash theme={null} uv pip install -r setup.py uv pip install . ``` # Introduction Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/introduction.html > Introduction to Hierarchical Forecasting using `HierarchicalForecast` You can run these experiments using CPU or GPU with Google Colab. Open In Colab ## 1. Hierarchical Series In many applications, a set of time series is hierarchically organized. Examples include the presence of geographic levels, products, or categories that define different types of aggregations. In such scenarios, forecasters are often required to provide predictions for all disaggregate and aggregate series. A natural desire is for those predictions to be **“coherent”**, that is, for the bottom series to add up precisely to the forecasts of the aggregated series. The above figure shows a simple hierarchical structure where we have four bottom-level series, two middle-level series, and the top level representing the total aggregation. Its hierarchical aggregations or coherency constraints are: $$ y_{\mathrm{Total},\tau} = y_{\beta_{1},\tau}+y_{\beta_{2},\tau}+y_{\beta_{3},\tau}+y_{\beta_{4},\tau} \qquad \qquad \qquad \qquad \qquad \\ \mathbf{y}_{[a],\tau}=\left[y_{\mathrm{Total},\tau},\; y_{\beta_{1},\tau}+y_{\beta_{2},\tau},\;y_{\beta_{3},\tau}+y_{\beta_{4},\tau}\right]^{\intercal} \qquad \mathbf{y}_{[b],\tau}=\left[ y_{\beta_{1},\tau},\; y_{\beta_{2},\tau},\; y_{\beta_{3},\tau},\; y_{\beta_{4},\tau} \right]^{\intercal} $$ Luckily these constraints can be compactly expressed with the following matrices: $$ \mathbf{S}_{[a,b][b]} = \begin{bmatrix} \mathbf{A}_{\mathrm{[a][b]}} \\ \\ \\ \mathbf{I}_{\mathrm{[b][b]}} \\ \\ \end{bmatrix} = \begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & 1 & 0 & 0 \\ 0 & 0 & 1 & 1 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} $$ where $\mathbf{A}_{[a,b][b]}$ aggregates the bottom series to the upper levels, and $\mathbf{I}_{\mathrm{[b][b]}}$ is an identity matrix. The representation of the hierarchical series is then: $$ \mathbf{y}_{[a,b],\tau} = \mathbf{S}_{[a,b][b]} \mathbf{y}_{[b],\tau} $$ To visualize an example, in Figure 2, one can think of the hierarchical time series structure levels to represent different geographical aggregations. For example, in Figure 2, the top level is the total aggregation of series within a country, the middle level being its states and the bottom level its regions. ## 2. Hierarchical Forecast To achieve **“coherency”**, most statistical solutions to the hierarchical forecasting challenge implement a two-stage reconciliation process. 1. First, we obtain a set of the base forecast $\mathbf{\hat{y}}_{[a,b],\tau}$ 2. Later, we reconcile them into coherent forecasts $\mathbf{\tilde{y}}_{[a,b],\tau}$. Most hierarchical reconciliation methods can be expressed by the following transformations: $$ \tilde{\mathbf{y}}_{[a,b],\tau} = \mathbf{S}_{[a,b][b]} \mathbf{P}_{[b][a,b]} \hat{\mathbf{y}}_{[a,b],\tau} $$ The HierarchicalForecast library offers a Python collection of reconciliation methods, datasets, evaluation and visualization tools for the task. Among its available reconciliation methods we have `BottomUp`, `TopDown`, `MiddleOut`, `MinTrace`, `ERM`. Among its probabilistic coherent methods we have `Normality`, `Bootstrap`, `PERMBU`. ## 3. Minimal Example ```python theme={null} !pip install hierarchicalforecast statsforecast datasetsforecast ``` ### Wrangling Data ```python theme={null} import numpy as np import pandas as pd ``` We are going to create a synthetic data set to illustrate a hierarchical time series structure like the one in Figure 1. We will create a two level structure with four bottom series where aggregations of the series are self evident. ```python theme={null} # Create Figure 1. synthetic bottom data ds = pd.date_range(start="2000-01-01", end="2000-08-01", freq="MS") y_base = np.arange(1, 9) r1 = y_base * (10 ** 1) r2 = y_base * (10 ** 1) r3 = y_base * (10 ** 2) r4 = y_base * (10 ** 2) ys = np.concatenate([r1, r2, r3, r4]) ds = np.tile(ds, 4) unique_ids = ["r1"] * 8 + ["r2"] * 8 + ["r3"] * 8 + ["r4"] * 8 top_level = "Australia" middle_level = ["State1"] * 16 + ["State2"] * 16 bottom_level = unique_ids bottom_df = dict( ds=ds, top_level=top_level, middle_level=middle_level, bottom_level=bottom_level, y=ys, ) bottom_df = pd.DataFrame(bottom_df) bottom_df.groupby("bottom_level").head(2) ``` The previously introduced hierarchical series $\mathbf{y}_{[a,b]\tau}$ is captured within the `Y_hier_df` dataframe. The aggregation constraints matrix $\mathbf{S}_{[a][b]}$ is captured within the `S_df` dataframe. Finally `tags` contains a dictionary of lists within `Y_hier_df` composing each hierarchical level, for example the `tags['top_level']` contains `Australia`’s aggregated series index. ```python theme={null} from hierarchicalforecast.utils import aggregate ``` ```python theme={null} # Create hierarchical structure and constraints hierarchy_levels = [ ["top_level"], ["top_level", "middle_level"], ["top_level", "middle_level", "bottom_level"], ] Y_hier_df, S_df, tags = aggregate(df=bottom_df, spec=hierarchy_levels) print("S_df.shape", S_df.shape) print("Y_hier_df.shape", Y_hier_df.shape) print("tags['top_level']", tags["top_level"]) ``` ```python theme={null} Y_hier_df.groupby("unique_id").head(2) ``` ```python theme={null} S_df ``` ### Base Predictions Next, we compute the *base forecast* for each time series using the `naive` model. Observe that `Y_hat_df` contains the forecasts but they are not coherent. ```python theme={null} from statsforecast.models import Naive from statsforecast.core import StatsForecast ``` ```python theme={null} # Split train/test sets Y_test_df = Y_hier_df.groupby("unique_id", as_index=False).tail(4) Y_train_df = Y_hier_df.drop(Y_test_df.index) # Compute base Naive predictions # Careful identifying correct data freq, this data monthly 'M' fcst = StatsForecast(models=[Naive()], freq="MS", n_jobs=-1) Y_hat_df = fcst.forecast(df=Y_train_df, h=4, fitted=True) Y_fitted_df = fcst.forecast_fitted_values() ``` ### Reconciliation ```python theme={null} from hierarchicalforecast.methods import BottomUp from hierarchicalforecast.core import HierarchicalReconciliation ``` ```python theme={null} # You can select a reconciler from our collection reconcilers = [BottomUp()] # MinTrace(method='mint_shrink') hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags) Y_rec_df.groupby("unique_id").head(2) ``` ## References * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html)
* [Orcutt, G.H., Watts, H.W., & Edwards, J.B.(1968). Data aggregation and information loss. The American Economic Review, 58 , 773(787).](http://www.jstor.org/stable/1815532)
* [Disaggregation methods to expedite product line forecasting. Journal of Forecasting, 9 , 233–254. doi:10.1002/for.3980090304.](https://onlinelibrary.wiley.com/doi/abs/10.1002/for.3980090304)
* [Wickramasuriya, S. L., Athanasopoulos, G., & Hyndman, R. J. (2019). "Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization". Journal of the American Statistical Association, 114 , 804–819. doi:10.1080/01621459.2018.1448825.](https://robjhyndman.com/publications/mint/)
* [Ben Taieb, S., & Koo, B. (2019). Regularized regression for hierarchical forecasting without unbiasedness conditions. In Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining KDD ’19 (p. 1337(1347). New York, NY, USA: Association for Computing Machinery.](https://doi.org/10.1145/3292500.3330976)
# Local vs Global Temporal Aggregation Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/localglobalaggregation.html > Temporal Hierarchical Aggregation on a local or global level. In this notebook we explain the difference between temporally aggregating timeseries locally and globally. You can run these experiments using CPU or GPU with Google Colab. Open In Colab ```python theme={null} !pip install hierarchicalforecast utilsforecast ``` ## 1. Generate Data In this example we will generate synthetic series to explain the difference between local- and global temporal aggregation. We will generate 2 series with a daily frequency. ```python theme={null} from utilsforecast.data import generate_series ``` ```python theme={null} freq = "D" n_series = 2 df = generate_series(n_series=n_series, freq=freq, min_length=2 * 365, max_length=4 * 365, equal_ends=True) ``` Note that our two timeseries do not have the same number of timesteps: ```python theme={null} df.groupby('unique_id', observed=True)["ds"].count() ``` ```text theme={null} unique_id 0 1414 1 1289 Name: ds, dtype: int64 ``` We then define a spec for our temporal aggregations. ```python theme={null} spec = {"year": 365, "quarter": 91, "month": 30, "week": 7, "day": 1} ``` ## 2. Local aggregation (default) In local aggregation, we treat the timestamps of each timeseries individually. It means that the temporal aggregation is performed by only looking at the timestamps of each series, disregarding the timestamps of other series. ```python theme={null} from hierarchicalforecast.utils import aggregate_temporal ``` ```python theme={null} Y_df_local, S_df_local, tags_local = aggregate_temporal(df, spec) ``` We have created temporal aggregations *per timeseries*, as the temporal aggregation `month-1` doesn’t correspond to the same (year, month) for both timeseries. This is because the series with `unique_id=1` is shorter and has its first datapoint in July 2000, in contrast to the series with `unique_id=0`, which is longer and has its first timestamp in March 2000. ```python theme={null} Y_df_local.query("temporal_id == 'month-1'") ``` | | temporal\_id | unique\_id | ds | y | | -- | ------------ | ---------- | ---------- | --------- | | 39 | month-1 | 0 | 2000-03-16 | 93.574676 | | 87 | month-1 | 1 | 2000-07-19 | 91.506421 | ## 2. Global aggregation In global aggregation, we examine all unique timestamps across all timeseries, and base our temporal aggregations on the unique list of timestamps across all timeseries. We can specify the aggregation type by setting the `aggregation_type` attritbue in `aggregate_temporal`. ```python theme={null} Y_df_global, S_df_global, tags_globval = aggregate_temporal(df, spec, aggregation_type="global") ``` We have created temporal aggregations *across all timeseries*, as the temporal aggregation `month-1` corresponds to the same (year, month)-combination for both timeseries. Since `month-1` isn’t present in the second timeseries (as it is shorter), we have only one record for the aggregation. ```python theme={null} Y_df_global.query("temporal_id == 'month-1'") ``` | | temporal\_id | unique\_id | ds | y | | -- | ------------ | ---------- | ---------- | --------- | | 39 | month-1 | 0 | 2000-03-16 | 93.574676 | For `month-5` however, we have a record for both timeseries, as the second series has its first datapoint in that month. ```python theme={null} Y_df_global.query("temporal_id == 'month-5'") ``` | | temporal\_id | unique\_id | ds | y | | -- | ------------ | ---------- | ---------- | --------- | | 43 | month-5 | 0 | 2000-07-14 | 95.169659 | | 87 | month-5 | 1 | 2000-07-14 | 74.502584 | Hence, the global aggregation ensures temporal alignment across all series. ## 3. What to choose? * If all timeseries have the same length and same timestamps, `global` and `local` yield the same results. * The default behavior is `local`. This means that temporal aggregations between timeseries can’t be compared unless the series have the same length and timestamp. This behavior is generally safer, and advised to use when time series are not necessarily related, and you are building per-series models using e.g. `StatsForecast`. * The `global` behavior can be useful when dealing with timeseries where we expect relationships between the timeseries. For example, in case of forecasting daily product demand individual products may not always have sales for all timesteps, but one is interested in the overall temporal yearly aggregation across all products. The `global` setting has more room for error, so be careful and check the aggregation result carefully. This would typically be the setting used in combination with models from `MLForecast` or `NeuralForecast`. # Temporal Aggregation with THIEF Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/m3withthief.html > Temporal Hierarchical Forecasting on M3 monthly and quarterly data > with THIEF In this notebook we present an example on how to use `HierarchicalForecast` to produce coherent forecasts between temporal levels. We will use the monthly and quarterly timeseries of the `M3` dataset. We will first load the `M3` data and produce base forecasts using an `AutoETS` model from `StatsForecast`. Then, we reconcile the forecasts with `THIEF` (Temporal HIerarchical Forecasting) from `HierarchicalForecast` according to a specified temporal hierarchy. ### References [Athanasopoulos, G, Hyndman, Rob J., Kourentzes, N., Petropoulos, Fotios (2017). Forecasting with temporal hierarchies. European Journal of Operational Research, 262, 60-74](https://www.sciencedirect.com/science/article/pii/S0377221717301911) You can run these experiments using CPU or GPU with Google Colab. Open In Colab ```python theme={null} !pip install hierarchicalforecast statsforecast datasetsforecast ``` ## 1. Load and Process Data ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} from datasetsforecast.m3 import M3 ``` ```python theme={null} m3_monthly, _, _ = M3.load(directory='data', group='Monthly') m3_quarterly, _, _ = M3.load(directory='data', group='Quarterly') ``` We will be making aggregations up to yearly levels, so for both monthly and quarterly data we make sure each time series has an integer multiple of bottom-level timesteps. For example, the first time series in m3\_monthly (with `unique_id='M1'`) has 68 timesteps. This is not a multiple of 12 (12 months in one year), so we would not be able to aggregate all timesteps into full years. Hence, we truncate (remove) the first 8 timesteps, resulting in 60 timesteps for this series. We do something similar for the quarterly data, albeit with a multiple of 4 (4 quarters in one year). Depending on the highest temporal aggregation in your reconciliation problem, you may want to truncate your data differently. ```python theme={null} m3_monthly = m3_monthly.groupby("unique_id", group_keys=False)\ .apply(lambda x: x.tail(len(x) // 12 * 12))\ .reset_index(drop=True) m3_quarterly = m3_quarterly.groupby("unique_id", group_keys=False)\ .apply(lambda x: x.tail(len(x) // 4 * 4))\ .reset_index(drop=True) ``` ## 2. Temporal reconciliation ### 2a. Split Train/Test sets We use as test samples the last 24 observations from the Monthly series and the last 8 observations of each quarterly series, following the original THIEF paper. ```python theme={null} horizon_monthly = 24 horizon_quarterly = 8 ``` ```python theme={null} m3_monthly_test = m3_monthly.groupby("unique_id", as_index=False).tail(horizon_monthly) m3_monthly_train = m3_monthly.drop(m3_monthly_test.index) m3_quarterly_test = m3_quarterly.groupby("unique_id", as_index=False).tail(horizon_quarterly) m3_quarterly_train = m3_quarterly.drop(m3_quarterly_test.index) ``` ### 2a. Aggregating the dataset according to temporal hierarchy We first define the temporal aggregation spec. The spec is a dictionary in which the keys are the name of the aggregation and the value is the amount of bottom-level timesteps that should be aggregated in that aggregation. For example, `year` consists of `12` months, so we define a key, value pair `"yearly":12`. We can do something similar for other aggregations that we are interested in. ```python theme={null} spec_temporal_monthly = {"yearly": 12, "semiannually": 6, "fourmonthly": 4, "quarterly": 3, "bimonthly": 2, "monthly": 1} spec_temporal_quarterly = {"yearly": 4, "semiannually": 2, "quarterly": 1} ``` We next compute the temporally aggregated train- and test sets using the `aggregate_temporal` function. Note that we have different aggregation matrices `S` for the train- and test set, as the test set contains temporal hierarchies that are not included in the train set. ```python theme={null} from hierarchicalforecast.utils import aggregate_temporal ``` ```python theme={null} # Monthly Y_monthly_train, S_monthly_train, tags_monthly_train = aggregate_temporal(df=m3_monthly_train, spec=spec_temporal_monthly) Y_monthly_test, S_monthly_test, tags_monthly_test = aggregate_temporal(df=m3_monthly_test, spec=spec_temporal_monthly) # Quarterly Y_quarterly_train, S_quarterly_train, tags_quarterly_train = aggregate_temporal(df=m3_quarterly_train, spec=spec_temporal_quarterly) Y_quarterly_test, S_quarterly_test, tags_quarterly_test = aggregate_temporal(df=m3_quarterly_test, spec=spec_temporal_quarterly) ``` Our aggregation matrices aggregate the lowest temporal granularity (quarters) up to years, for the train- and test set. ```python theme={null} S_monthly_train.iloc[:5, :5] ``` | | temporal\_id | monthly-1 | monthly-2 | monthly-3 | monthly-4 | | - | ------------ | --------- | --------- | --------- | --------- | | 0 | yearly-1 | 0.0 | 0.0 | 0.0 | 0.0 | | 1 | yearly-2 | 0.0 | 0.0 | 0.0 | 0.0 | | 2 | yearly-3 | 0.0 | 0.0 | 0.0 | 0.0 | | 3 | yearly-4 | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | yearly-5 | 0.0 | 0.0 | 0.0 | 0.0 | ```python theme={null} S_monthly_test.iloc[:5, :5] ``` | | temporal\_id | monthly-1 | monthly-2 | monthly-3 | monthly-4 | | - | -------------- | --------- | --------- | --------- | --------- | | 0 | yearly-1 | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | yearly-2 | 0.0 | 0.0 | 0.0 | 0.0 | | 2 | semiannually-1 | 1.0 | 1.0 | 1.0 | 1.0 | | 3 | semiannually-2 | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | semiannually-3 | 0.0 | 0.0 | 0.0 | 0.0 | ### 2b. Computing base forecasts Now, we need to compute base forecasts for each temporal aggregation. The following cell computes the **base forecasts** for each temporal aggregation in `Y_monthly_train` and `Y_quarterly_train` using the `AutoARIMA` model. Observe that `Y_hats` contains the forecasts but they are not coherent. Note also that both frequency and horizon are different for each temporal aggregation. For the monthly data, the lowest level has a monthly frequency, and a horizon of `24` (constituting 2 years). However, as example, the `year` aggregation has a yearly frequency with a horizon of 2. It is of course possible to choose a different model for each level in the temporal aggregation - you can be as creative as you like! ```python theme={null} from statsforecast.models import AutoARIMA from statsforecast.core import StatsForecast ``` ```python theme={null} Y_hats = [] id_cols = ["unique_id", "temporal_id", "ds", "y"] # We loop over the monthly and quarterly data for tags_train, tags_test, Y_train, Y_test in zip([tags_monthly_train, tags_quarterly_train], [tags_monthly_test, tags_quarterly_test], [Y_monthly_train, Y_quarterly_train], [Y_monthly_test, Y_quarterly_test]): # We will train a model for each temporal level Y_hats_tags = [] for level, temporal_ids_train in tags_train.items(): # Filter the data for the level Y_level_train = Y_train.query("temporal_id in @temporal_ids_train") temporal_ids_test = tags_test[level] Y_level_test = Y_test.query("temporal_id in @temporal_ids_test") # For each temporal level we have a different frequency and forecast horizon. We use the timestamps of the first timeseries to automatically derive the frequency & horizon of the temporally aggregated series. unique_id = Y_level_train["unique_id"].iloc[0] freq_level = pd.infer_freq(Y_level_train.query("unique_id == @unique_id")["ds"]) horizon_level = Y_level_test.query("unique_id == @unique_id")["ds"].nunique() # Train a model and create forecasts fcst = StatsForecast(models=[AutoARIMA()], freq=freq_level, n_jobs=-1) Y_hat_level = fcst.forecast(df=Y_level_train[["ds", "unique_id", "y"]], h=horizon_level) # Add the test set to the forecast Y_hat_level = pd.concat([Y_level_test.reset_index(drop=True), Y_hat_level.drop(columns=["unique_id", "ds"])], axis=1) # Put cols in the right order (for readability) Y_hat_cols = id_cols + [col for col in Y_hat_level.columns if col not in id_cols] Y_hat_level = Y_hat_level[Y_hat_cols] # Append the forecast to the list Y_hats_tags.append(Y_hat_level) Y_hat_tag = pd.concat(Y_hats_tags, ignore_index=True) Y_hats.append(Y_hat_tag) ``` ### 2c. Reconcile forecasts We can use the `HierarchicalReconciliation` class to reconcile the forecasts. In this example we use `BottomUp` and `MinTrace(wls_struct)`. The latter is the ‘structural scaling’ method introduced in [Forecasting with temporal hierarchies](https://robjhyndman.com/publications/temporal-hierarchies/). Note that we have to set `temporal=True` in the `reconcile` function. ```python theme={null} from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.core import HierarchicalReconciliation ``` ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method="wls_struct"), ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_recs = [] # We loop over the monthly and quarterly data for Y_hat, S, tags in zip(Y_hats, [S_monthly_test, S_quarterly_test], [tags_monthly_test, tags_quarterly_test]): Y_rec = hrec.reconcile(Y_hat_df=Y_hat, S_df=S, tags=tags, temporal=True) Y_recs.append(Y_rec) ``` ## 3. Evaluation The `HierarchicalForecast` package includes the `evaluate` function to evaluate the different hierarchies. We evaluate the temporally aggregated forecasts *across all temporal aggregations*. ```python theme={null} from hierarchicalforecast.evaluation import evaluate from utilsforecast.losses import mae ``` ### 3a. Monthly ```python theme={null} Y_rec_monthly = Y_recs[0] evaluation = evaluate(df = Y_rec_monthly.drop(columns = 'unique_id'), tags = tags_monthly_test, metrics = [mae], id_col='temporal_id', benchmark="AutoARIMA") evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(wls_struct)'] numeric_cols = evaluation.select_dtypes(include="number").columns evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64) evaluation ``` | | level | metric | Base | BottomUp | MinTrace(wls\_struct) | | - | ------------ | ---------- | ---- | -------- | --------------------- | | 0 | yearly | mae-scaled | 1.0 | 0.78 | 0.75 | | 1 | semiannually | mae-scaled | 1.0 | 0.99 | 0.95 | | 2 | fourmonthly | mae-scaled | 1.0 | 0.96 | 0.93 | | 3 | quarterly | mae-scaled | 1.0 | 0.95 | 0.93 | | 4 | bimonthly | mae-scaled | 1.0 | 0.96 | 0.94 | | 5 | monthly | mae-scaled | 1.0 | 1.00 | 0.99 | | 6 | Overall | mae-scaled | 1.0 | 0.94 | 0.92 | `MinTrace(wls_struct)` is the best overall method, scoring the lowest `mae` on all levels. ### 3b. Quarterly ```python theme={null} Y_rec_quarterly = Y_recs[1] evaluation = evaluate(df = Y_rec_quarterly.drop(columns = 'unique_id'), tags = tags_quarterly_test, metrics = [mae], id_col='temporal_id', benchmark="AutoARIMA") evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(wls_struct)'] numeric_cols = evaluation.select_dtypes(include="number").columns evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64) evaluation ``` | | level | metric | Base | BottomUp | MinTrace(wls\_struct) | | - | ------------ | ---------- | ---- | -------- | --------------------- | | 0 | yearly | mae-scaled | 1.0 | 0.87 | 0.85 | | 1 | semiannually | mae-scaled | 1.0 | 1.03 | 1.00 | | 2 | quarterly | mae-scaled | 1.0 | 1.00 | 0.97 | | 3 | Overall | mae-scaled | 1.0 | 0.97 | 0.94 | Again, `MinTrace(wls_struct)` is the best overall method, scoring the lowest `mae` on all levels. # Neural/MLForecast Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/mlframeworksexample.html This example notebook demonstrates the compatibility of HierarchicalForecast’s reconciliation methods with popular machine-learning libraries, specifically [NeuralForecast](https://github.com/Nixtla/neuralforecast) and [MLForecast](https://github.com/Nixtla/mlforecast). The notebook utilizes NBEATS and XGBRegressor models to create base forecasts for the TourismLarge Hierarchical Dataset. After that, we use HierarchicalForecast to reconcile the base predictions. **References**
- [Boris N. Oreshkin, Dmitri Carpov, Nicolas Chapados, Yoshua Bengio (2019). “N-BEATS: Neural basis expansion analysis for interpretable time series forecasting”. url: https://arxiv.org/abs/1905.10437](https://arxiv.org/abs/1905.10437)
- [Tianqi Chen and Carlos Guestrin. “XGBoost: A Scalable Tree Boosting System”. In: Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining. KDD ’16. San Francisco, California, USA: Association for Computing Machinery, 2016, pp. 785–794. isbn: 9781450342322. doi: 10.1145/2939672.2939785. url: https://doi.org/10.1145/2939672.2939785 (cit. on p. 26).](https://doi.org/10.1145/2939672.2939785)
You can run these experiments using CPU or GPU with Google Colab. Open In Colab ## 1. Installing packages ```python theme={null} !pip install datasetsforecast hierarchicalforecast mlforecast neuralforecast ``` ```python theme={null} import numpy as np import pandas as pd from datasetsforecast.hierarchical import HierarchicalData from neuralforecast import NeuralForecast from neuralforecast.models import NBEATS from neuralforecast.losses.pytorch import GMM from mlforecast import MLForecast from mlforecast.utils import PredictionIntervals import xgboost as xgb #obtain hierarchical reconciliation methods and evaluation from hierarchicalforecast.methods import BottomUp, ERM, MinTrace from hierarchicalforecast.utils import HierarchicalPlot from hierarchicalforecast.core import HierarchicalReconciliation from hierarchicalforecast.evaluation import evaluate ``` ## 2. Load hierarchical dataset This detailed Australian Tourism Dataset comes from the National Visitor Survey, managed by the Tourism Research Australia, it is composed of 555 monthly series from 1998 to 2016, it is organized geographically, and purpose of travel. The natural geographical hierarchy comprises seven states, divided further in 27 zones and 76 regions. The purpose of travel categories are holiday, visiting friends and relatives (VFR), business and other. The MinT (Wickramasuriya et al., 2019), among other hierarchical forecasting studies has used the dataset it in the past. The dataset can be accessed in the [MinT reconciliation webpage](https://robjhyndman.com/publications/mint/), although other sources are available. | Geographical Division | Number of series per division | Number of series per purpose | Total | | --------------------- | ----------------------------- | ---------------------------- | ----- | | Australia | 1 | 4 | 5 | | States | 7 | 28 | 35 | | Zones | 27 | 108 | 135 | | Regions | 76 | 304 | 380 | | Total | 111 | 444 | 555 | ```python theme={null} Y_df, S_df, tags = HierarchicalData.load('./data', 'TourismLarge') Y_df['ds'] = pd.to_datetime(Y_df['ds']) S_df = S_df.reset_index(names="unique_id") ``` ```python theme={null} Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ------------ | | 0 | TotalAll | 1998-01-01 | 45151.071280 | | 1 | TotalAll | 1998-02-01 | 17294.699551 | | 2 | TotalAll | 1998-03-01 | 20725.114184 | | 3 | TotalAll | 1998-04-01 | 25388.612353 | | 4 | TotalAll | 1998-05-01 | 20330.035211 | Visualize the aggregation matrix. ```python theme={null} hplot = HierarchicalPlot(S=S_df, tags=tags) hplot.plot_summing_matrix() ``` Split the dataframe in train/test splits. ```python theme={null} horizon = 12 Y_test_df = Y_df.groupby('unique_id', as_index=False).tail(horizon) Y_train_df = Y_df.drop(Y_test_df.index) ``` ## 3. Fit and Predict Models HierarchicalForecast is compatible with many different ML models. Here, we show two examples:
1. NBEATS, a MLP-based deep neural architecture.
2. XGBRegressor, a tree-based architecture.
```python theme={null} level = np.arange(0, 100, 2) qs = [[50-lv/2, 50+lv/2] for lv in level] quantiles = np.sort(np.concatenate(qs)[1:]/100) #fit/predict NBEATS from NeuralForecast nbeats = NBEATS(h=horizon, input_size=2*horizon, loss=GMM(n_components=10, quantiles=quantiles), scaler_type='robust', max_steps=2000) nf = NeuralForecast(models=[nbeats], freq='MS') nf.fit(df=Y_train_df) Y_hat_nf = nf.predict() insample_nf = nf.predict_insample(step_size=horizon) #fit/predict XGBRegressor from MLForecast mf = MLForecast(models=[xgb.XGBRegressor()], freq='MS', lags=[1,2,12,24], date_features=['month'], ) mf.fit(Y_train_df, fitted=True, prediction_intervals=PredictionIntervals(n_windows=10, h=horizon)) Y_hat_mf = mf.predict(horizon, level=level) insample_mf = mf.forecast_fitted_values() ``` ```python theme={null} Y_hat_nf ``` | | unique\_id | ds | NBEATS | NBEATS-lo-98.0 | NBEATS-lo-96.0 | NBEATS-lo-94.0 | NBEATS-lo-92.0 | NBEATS-lo-90.0 | NBEATS-lo-88.0 | NBEATS-lo-86.0 | ... | NBEATS-hi-80.0 | NBEATS-hi-82.0 | NBEATS-hi-84.0 | NBEATS-hi-86.0 | NBEATS-hi-88.0 | NBEATS-hi-90.0 | NBEATS-hi-92.0 | NBEATS-hi-94.0 | NBEATS-hi-96.0 | NBEATS-hi-98.0 | | ---- | ---------- | ---------- | ----------- | -------------- | -------------- | -------------- | -------------- | -------------- | -------------- | -------------- | --- | -------------- | -------------- | -------------- | -------------- | -------------- | -------------- | -------------- | -------------- | -------------- | -------------- | | 0 | AAAAll | 2016-01-01 | 2843.298584 | 1764.249023 | 1806.885132 | 1864.019043 | 1906.171021 | 1945.994629 | 1965.081421 | 1998.606812 | ... | 3497.682373 | 3520.107666 | 3561.643799 | 3600.121094 | 3646.954346 | 3703.382324 | 3774.084473 | 3813.719238 | 3902.713867 | 3991.594238 | | 1 | AAAAll | 2016-02-01 | 1753.340698 | 1394.245850 | 1414.474976 | 1439.167480 | 1458.228394 | 1474.655640 | 1480.433472 | 1489.651245 | ... | 2024.560791 | 2049.965576 | 2066.480957 | 2090.285156 | 2120.172852 | 2145.964844 | 2201.716064 | 2253.415039 | 2364.905029 | 2441.167480 | | 2 | AAAAll | 2016-03-01 | 1878.675171 | 1446.630371 | 1491.637817 | 1513.890137 | 1524.787842 | 1532.539917 | 1547.460205 | 1559.098389 | ... | 2172.270996 | 2189.489990 | 2216.255859 | 2236.661377 | 2286.617676 | 2370.431152 | 2411.910156 | 2477.557373 | 2579.611084 | 2722.415283 | | 3 | AAAAll | 2016-04-01 | 2140.948486 | 1661.737793 | 1706.259399 | 1724.914551 | 1736.446045 | 1754.887695 | 1765.482056 | 1772.123901 | ... | 2470.206543 | 2483.571045 | 2493.527588 | 2517.062744 | 2547.355713 | 2577.867676 | 2610.180908 | 2637.010498 | 2700.801758 | 2864.596924 | | 4 | AAAAll | 2016-05-01 | 1834.694946 | 1466.314209 | 1485.427002 | 1500.715210 | 1518.462036 | 1535.386475 | 1543.525635 | 1554.429810 | ... | 2093.700684 | 2120.782471 | 2137.882812 | 2154.052002 | 2164.069824 | 2189.309326 | 2234.271973 | 2311.157715 | 2436.267090 | 2659.653809 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 6655 | TotalVis | 2016-08-01 | 7362.455078 | 5799.121582 | 5960.676270 | 6073.553223 | 6230.090820 | 6294.191406 | 6365.950684 | 6400.492676 | ... | 8120.279785 | 8144.139648 | 8185.699219 | 8212.809570 | 8255.871094 | 8291.191406 | 8374.907227 | 8435.806641 | 8568.060547 | 8770.566406 | | 6656 | TotalVis | 2016-09-01 | 7803.098145 | 6455.050293 | 6612.847168 | 6690.960938 | 6804.897461 | 6848.432617 | 6873.607422 | 6904.770020 | ... | 8562.215820 | 8594.000000 | 8642.083984 | 8715.201172 | 8795.628906 | 8924.573242 | 9053.747070 | 9250.514648 | 9410.338867 | 9818.623047 | | 6657 | TotalVis | 2016-10-01 | 8478.570312 | 6592.350098 | 6818.883789 | 7075.323730 | 7223.682129 | 7300.230957 | 7336.740723 | 7391.779785 | ... | 9558.611328 | 9586.333984 | 9658.816406 | 9761.448242 | 9802.087891 | 9870.294922 | 9956.144531 | 10070.672852 | 10195.408203 | 10342.619141 | | 6658 | TotalVis | 2016-11-01 | 8251.816406 | 6471.753906 | 6551.861328 | 6621.647461 | 6694.992188 | 6740.827148 | 6798.824707 | 6825.794434 | ... | 9519.825195 | 9557.507812 | 9624.822266 | 9720.269531 | 9811.011719 | 9907.259766 | 10132.628906 | 10362.583984 | 10896.478516 | 11394.652344 | | 6659 | TotalVis | 2016-12-01 | 9023.334961 | 6798.515625 | 6978.411621 | 7165.805176 | 7250.106934 | 7333.168457 | 7395.183594 | 7457.470215 | ... | 10221.937500 | 10290.527344 | 10334.883789 | 10399.726562 | 10553.360352 | 10645.852539 | 10806.295898 | 10992.416016 | 11328.151367 | 11933.357422 | ```python theme={null} Y_hat_mf ``` | | unique\_id | ds | XGBRegressor | XGBRegressor-lo-98 | XGBRegressor-lo-96 | XGBRegressor-lo-94 | XGBRegressor-lo-92 | XGBRegressor-lo-90 | XGBRegressor-lo-88 | XGBRegressor-lo-86 | ... | XGBRegressor-hi-80 | XGBRegressor-hi-82 | XGBRegressor-hi-84 | XGBRegressor-hi-86 | XGBRegressor-hi-88 | XGBRegressor-hi-90 | XGBRegressor-hi-92 | XGBRegressor-hi-94 | XGBRegressor-hi-96 | XGBRegressor-hi-98 | | ---- | ---------- | ---------- | ------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | --- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | | 0 | AAAAll | 2016-01-01 | 3240.743164 | 2566.404620 | 2638.984995 | 2711.565370 | 2784.145745 | 2856.726120 | 2876.514198 | 2877.447884 | ... | 3601.237386 | 3602.171072 | 3603.104758 | 3604.038444 | 3604.972130 | 3624.760208 | 3697.340583 | 3769.920958 | 3842.501333 | 3915.081708 | | 1 | AAAAll | 2016-02-01 | 1583.065063 | 1247.414469 | 1248.895343 | 1250.376217 | 1251.857091 | 1253.337965 | 1263.627340 | 1277.062610 | ... | 1848.761709 | 1862.196978 | 1875.632248 | 1889.067517 | 1902.502787 | 1912.792162 | 1914.273036 | 1915.753910 | 1917.234784 | 1918.715658 | | 2 | AAAAll | 2016-03-01 | 2030.168213 | 1345.896497 | 1386.655046 | 1427.413595 | 1468.172144 | 1508.930693 | 1546.207337 | 1582.240444 | ... | 2369.996660 | 2406.029767 | 2442.062874 | 2478.095981 | 2514.129089 | 2551.405733 | 2592.164282 | 2632.922831 | 2673.681380 | 2714.439928 | | 3 | AAAAll | 2016-04-01 | 2152.282227 | 1767.276611 | 1772.956049 | 1778.635487 | 1784.314926 | 1789.994364 | 1798.503584 | 1808.023439 | ... | 2467.981448 | 2477.501303 | 2487.021159 | 2496.541014 | 2506.060870 | 2514.570089 | 2520.249527 | 2525.928966 | 2531.608404 | 2537.287842 | | 4 | AAAAll | 2016-05-01 | 1970.894775 | 1476.761973 | 1510.667430 | 1544.572887 | 1578.478344 | 1612.383801 | 1625.448072 | 1631.069062 | ... | 2293.857519 | 2299.478509 | 2305.099499 | 2310.720489 | 2316.341479 | 2329.405750 | 2363.311207 | 2397.216664 | 2431.122121 | 2465.027578 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 6655 | TotalVis | 2016-08-01 | 7810.465820 | 6251.079674 | 6268.924727 | 6286.769780 | 6304.614833 | 6322.459886 | 6375.977772 | 6442.235956 | ... | 8979.921135 | 9046.179318 | 9112.437501 | 9178.695685 | 9244.953868 | 9298.471754 | 9316.316807 | 9334.161860 | 9352.006913 | 9369.851967 | | 6656 | TotalVis | 2016-09-01 | 6887.893555 | 5346.477959 | 5397.795065 | 5449.112170 | 5500.429275 | 5551.746380 | 5604.124112 | 5656.880638 | ... | 7960.636893 | 8013.393419 | 8066.149945 | 8118.906472 | 8171.662998 | 8224.040729 | 8275.357834 | 8326.674940 | 8377.992045 | 8429.309150 | | 6657 | TotalVis | 2016-10-01 | 7763.275879 | 6138.534738 | 6267.740281 | 6396.945824 | 6526.151367 | 6655.356910 | 6706.009194 | 6728.606744 | ... | 8730.152366 | 8752.749916 | 8775.347465 | 8797.945014 | 8820.542563 | 8871.194848 | 9000.400391 | 9129.605934 | 9258.811477 | 9388.017020 | | 6658 | TotalVis | 2016-11-01 | 7432.722168 | 5703.395148 | 5726.926242 | 5750.457336 | 5773.988430 | 5797.519524 | 5929.164698 | 6099.422043 | ... | 8255.250258 | 8425.507603 | 8595.764948 | 8766.022293 | 8936.279638 | 9067.924811 | 9091.455905 | 9114.986999 | 9138.518093 | 9162.049187 | | 6659 | TotalVis | 2016-12-01 | 9624.172852 | 8115.705498 | 8217.381077 | 8319.056655 | 8420.732234 | 8522.407812 | 8566.581883 | 8590.219701 | ... | 10587.212548 | 10610.850366 | 10634.488184 | 10658.126002 | 10681.763820 | 10725.937891 | 10827.613470 | 10929.289048 | 11030.964626 | 11132.640205 | ## 4. Reconcile Predictions With minimal parsing, we can reconcile the raw output predictions with different HierarchicalForecast reconciliation methods. ```python theme={null} reconcilers = [ ERM(method='closed'), BottomUp(), MinTrace('mint_shrink'), ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_nf = hrec.reconcile(Y_hat_df=Y_hat_nf, Y_df=insample_nf, S_df=S_df, tags=tags, level=level) Y_rec_mf = hrec.reconcile(Y_hat_df=Y_hat_mf, Y_df=insample_mf, S_df=S_df, tags=tags, level=level) ``` ## 5. Evaluation To evaluate we use a scaled variation of the CRPS, as proposed by Rangapuram (2021), to measure the accuracy of predicted quantiles `y_hat` compared to the observation `y`. $$ \mathrm{sCRPS}(\hat{F}_{\tau}, \mathbf{y}_{\tau}) = \frac{2}{N} \sum_{i} \int^{1}_{0} \frac{\mathrm{QL}(\hat{F}_{i,\tau}, y_{i,\tau})_{q}}{\sum_{i} | y_{i,\tau} |} dq $$ We find that XGB with MinTrace(mint\_shrink) reconciliation result in the lowest CRPS score on the test set, thus giving us the best probabilistic forecasts. ```python theme={null} from utilsforecast.losses import scaled_crps ``` ```python theme={null} rec_model_names_nf = ['NBEATS/BottomUp', 'NBEATS/MinTrace_method-mint_shrink', 'NBEATS/ERM_method-closed_lambda_reg-0.01'] evaluation_nf = evaluate(df = Y_rec_nf.merge(Y_test_df, on=['unique_id', 'ds']), tags = tags, metrics = [scaled_crps], models= rec_model_names_nf, level = list(range(0, 100, 2)), ) rec_model_names_mf = ['XGBRegressor/BottomUp', 'XGBRegressor/MinTrace_method-mint_shrink', 'XGBRegressor/ERM_method-closed_lambda_reg-0.01'] evaluation_mf = evaluate(df = Y_rec_mf.merge(Y_test_df, on=['unique_id', 'ds']), tags = tags, metrics = [scaled_crps], models= rec_model_names_mf, level = list(range(0, 100, 2)), ) ``` ```python theme={null} name = 'NBEATS/BottomUp' quantile_columns = [col for col in Y_rec_mf.columns if (name+'-lo') in col or (name+'-hi') in col] ``` ```python theme={null} evaluation_nf.query("level == 'Overall'") ``` | | level | metric | NBEATS/BottomUp | NBEATS/MinTrace\_method-mint\_shrink | NBEATS/ERM\_method-closed\_lambda\_reg-0.01 | | - | ------- | ------------ | --------------- | ------------------------------------ | ------------------------------------------- | | 8 | Overall | scaled\_crps | 2.523212 | 2.43205 | 2.645045 | ```python theme={null} evaluation_mf.query("level == 'Overall'") ``` | | level | metric | XGBRegressor/BottomUp | XGBRegressor/MinTrace\_method-mint\_shrink | XGBRegressor/ERM\_method-closed\_lambda\_reg-0.01 | | - | ------- | ------------ | --------------------- | ------------------------------------------ | ------------------------------------------------- | | 8 | Overall | scaled\_crps | 1.98255 | 1.44981 | 1.910014 | ## 6. Visualizations ```python theme={null} plot_nf = Y_df.merge(Y_rec_nf, on=['unique_id', 'ds'], how="outer") plot_mf = Y_df.merge(Y_rec_mf, on=['unique_id', 'ds'], how="outer") ``` ```python theme={null} hplot.plot_series( series='TotalVis', Y_df=plot_nf, models=['y', 'NBEATS', 'NBEATS/BottomUp', 'NBEATS/MinTrace_method-mint_shrink', 'NBEATS/ERM_method-closed_lambda_reg-0.01'], level=[80] ) ``` ```python theme={null} hplot.plot_series( series='TotalVis', Y_df=plot_mf, models=['y', 'XGBRegressor', 'XGBRegressor/BottomUp', 'XGBRegressor/MinTrace_method-mint_shrink', 'XGBRegressor/ERM_method-closed_lambda_reg-0.01'], level=[80] ) ``` # Non-Negative MinTrace Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/nonnegativereconciliation.html Large collections of time series organized into structures at different aggregation levels often require their forecasts to follow their aggregation constraints and to be nonnegative, which poses the challenge of creating novel algorithms capable of coherent forecasts. The `HierarchicalForecast` package provides a wide collection of Python implementations of hierarchical forecasting algorithms that follow nonnegative hierarchical reconciliation. In this notebook, we will show how to use the `HierarchicalForecast` package to perform nonnegative reconciliation of forecasts on `Wiki2` dataset. You can run these experiments using CPU or GPU with Google Colab. Open In Colab ```python theme={null} !pip install hierarchicalforecast statsforecast datasetsforecast ``` ## 1. Load Data In this example we will use the `Wiki2` dataset. The following cell gets the time series for the different levels in the hierarchy, the summing dataframe `S_df` which recovers the full dataset from the bottom level hierarchy and the indices of each hierarchy denoted by `tags`. ```python theme={null} import numpy as np import pandas as pd from datasetsforecast.hierarchical import HierarchicalData ``` ```python theme={null} Y_df, S_df, tags = HierarchicalData.load('./data', 'Wiki2') Y_df['ds'] = pd.to_datetime(Y_df['ds']) S_df = S_df.reset_index(names="unique_id") ``` ```python theme={null} Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ------ | | 0 | Total | 2016-01-01 | 156508 | | 1 | Total | 2016-01-02 | 129902 | | 2 | Total | 2016-01-03 | 138203 | | 3 | Total | 2016-01-04 | 115017 | | 4 | Total | 2016-01-05 | 126042 | ```python theme={null} S_df.iloc[:5, :5] ``` | | unique\_id | de\_AAC\_AAG\_001 | de\_AAC\_AAG\_010 | de\_AAC\_AAG\_014 | de\_AAC\_AAG\_045 | | - | ---------- | ----------------- | ----------------- | ----------------- | ----------------- | | 0 | Total | 1 | 1 | 1 | 1 | | 1 | de | 1 | 1 | 1 | 1 | | 2 | en | 0 | 0 | 0 | 0 | | 3 | fr | 0 | 0 | 0 | 0 | | 4 | ja | 0 | 0 | 0 | 0 | ```python theme={null} tags ``` ```text theme={null} {'Views': array(['Total'], dtype=object), 'Views/Country': array(['de', 'en', 'fr', 'ja', 'ru', 'zh'], dtype=object), 'Views/Country/Access': array(['de_AAC', 'de_DES', 'de_MOB', 'en_AAC', 'en_DES', 'en_MOB', 'fr_AAC', 'fr_DES', 'fr_MOB', 'ja_AAC', 'ja_DES', 'ja_MOB', 'ru_AAC', 'ru_DES', 'ru_MOB', 'zh_AAC', 'zh_DES', 'zh_MOB'], dtype=object), 'Views/Country/Access/Agent': array(['de_AAC_AAG', 'de_AAC_SPD', 'de_DES_AAG', 'de_MOB_AAG', 'en_AAC_AAG', 'en_AAC_SPD', 'en_DES_AAG', 'en_MOB_AAG', 'fr_AAC_AAG', 'fr_AAC_SPD', 'fr_DES_AAG', 'fr_MOB_AAG', 'ja_AAC_AAG', 'ja_AAC_SPD', 'ja_DES_AAG', 'ja_MOB_AAG', 'ru_AAC_AAG', 'ru_AAC_SPD', 'ru_DES_AAG', 'ru_MOB_AAG', 'zh_AAC_AAG', 'zh_AAC_SPD', 'zh_DES_AAG', 'zh_MOB_AAG'], dtype=object), 'Views/Country/Access/Agent/Topic': array(['de_AAC_AAG_001', 'de_AAC_AAG_010', 'de_AAC_AAG_014', 'de_AAC_AAG_045', 'de_AAC_AAG_063', 'de_AAC_AAG_100', 'de_AAC_AAG_110', 'de_AAC_AAG_123', 'de_AAC_AAG_143', 'de_AAC_SPD_012', 'de_AAC_SPD_074', 'de_AAC_SPD_080', 'de_AAC_SPD_105', 'de_AAC_SPD_115', 'de_AAC_SPD_133', 'de_DES_AAG_064', 'de_DES_AAG_116', 'de_DES_AAG_131', 'de_MOB_AAG_015', 'de_MOB_AAG_020', 'de_MOB_AAG_032', 'de_MOB_AAG_059', 'de_MOB_AAG_062', 'de_MOB_AAG_088', 'de_MOB_AAG_095', 'de_MOB_AAG_109', 'de_MOB_AAG_122', 'de_MOB_AAG_149', 'en_AAC_AAG_044', 'en_AAC_AAG_049', 'en_AAC_AAG_075', 'en_AAC_AAG_114', 'en_AAC_AAG_119', 'en_AAC_AAG_141', 'en_AAC_SPD_004', 'en_AAC_SPD_011', 'en_AAC_SPD_026', 'en_AAC_SPD_048', 'en_AAC_SPD_067', 'en_AAC_SPD_126', 'en_AAC_SPD_140', 'en_DES_AAG_016', 'en_DES_AAG_024', 'en_DES_AAG_042', 'en_DES_AAG_069', 'en_DES_AAG_082', 'en_DES_AAG_102', 'en_MOB_AAG_018', 'en_MOB_AAG_022', 'en_MOB_AAG_101', 'en_MOB_AAG_124', 'fr_AAC_AAG_029', 'fr_AAC_AAG_046', 'fr_AAC_AAG_070', 'fr_AAC_AAG_087', 'fr_AAC_AAG_098', 'fr_AAC_AAG_104', 'fr_AAC_AAG_111', 'fr_AAC_AAG_112', 'fr_AAC_AAG_142', 'fr_AAC_SPD_025', 'fr_AAC_SPD_027', 'fr_AAC_SPD_035', 'fr_AAC_SPD_077', 'fr_AAC_SPD_084', 'fr_AAC_SPD_097', 'fr_AAC_SPD_130', 'fr_DES_AAG_023', 'fr_DES_AAG_043', 'fr_DES_AAG_051', 'fr_DES_AAG_058', 'fr_DES_AAG_061', 'fr_DES_AAG_091', 'fr_DES_AAG_093', 'fr_DES_AAG_094', 'fr_DES_AAG_136', 'fr_MOB_AAG_006', 'fr_MOB_AAG_030', 'fr_MOB_AAG_066', 'fr_MOB_AAG_117', 'fr_MOB_AAG_120', 'fr_MOB_AAG_121', 'fr_MOB_AAG_135', 'fr_MOB_AAG_147', 'ja_AAC_AAG_038', 'ja_AAC_AAG_047', 'ja_AAC_AAG_055', 'ja_AAC_AAG_076', 'ja_AAC_AAG_099', 'ja_AAC_AAG_128', 'ja_AAC_AAG_132', 'ja_AAC_AAG_134', 'ja_AAC_AAG_137', 'ja_AAC_SPD_013', 'ja_AAC_SPD_034', 'ja_AAC_SPD_050', 'ja_AAC_SPD_060', 'ja_AAC_SPD_078', 'ja_AAC_SPD_106', 'ja_DES_AAG_079', 'ja_DES_AAG_081', 'ja_DES_AAG_113', 'ja_MOB_AAG_065', 'ja_MOB_AAG_073', 'ja_MOB_AAG_092', 'ja_MOB_AAG_127', 'ja_MOB_AAG_129', 'ja_MOB_AAG_144', 'ru_AAC_AAG_008', 'ru_AAC_AAG_145', 'ru_AAC_AAG_146', 'ru_AAC_SPD_000', 'ru_AAC_SPD_090', 'ru_AAC_SPD_148', 'ru_DES_AAG_003', 'ru_DES_AAG_007', 'ru_DES_AAG_017', 'ru_DES_AAG_041', 'ru_DES_AAG_071', 'ru_DES_AAG_072', 'ru_MOB_AAG_002', 'ru_MOB_AAG_040', 'ru_MOB_AAG_083', 'ru_MOB_AAG_086', 'ru_MOB_AAG_103', 'ru_MOB_AAG_107', 'ru_MOB_AAG_118', 'ru_MOB_AAG_125', 'zh_AAC_AAG_021', 'zh_AAC_AAG_033', 'zh_AAC_AAG_037', 'zh_AAC_AAG_052', 'zh_AAC_AAG_057', 'zh_AAC_AAG_085', 'zh_AAC_AAG_108', 'zh_AAC_SPD_039', 'zh_AAC_SPD_096', 'zh_DES_AAG_009', 'zh_DES_AAG_019', 'zh_DES_AAG_053', 'zh_DES_AAG_054', 'zh_DES_AAG_056', 'zh_DES_AAG_068', 'zh_DES_AAG_089', 'zh_DES_AAG_139', 'zh_MOB_AAG_005', 'zh_MOB_AAG_028', 'zh_MOB_AAG_031', 'zh_MOB_AAG_036', 'zh_MOB_AAG_138'], dtype=object)} ``` We split the dataframe in train/test splits. ```python theme={null} Y_test_df = Y_df.groupby('unique_id', as_index=False).tail(7) Y_train_df = Y_df.drop(Y_test_df.index) ``` ## 2. Base Forecasts The following cell computes the *base forecast* for each time series using the `AutoETS` model. Observe that `Y_hat_df` contains the forecasts but they are not coherent. ```python theme={null} from statsforecast.models import AutoETS, Naive from statsforecast.core import StatsForecast ``` ```python theme={null} fcst = StatsForecast( models=[AutoETS(season_length=7, model='ZAA'), Naive()], freq='D', n_jobs=-1 ) Y_hat_df = fcst.forecast(df=Y_train_df, h=7) ``` Observe that the `AutoETS` model computes negative forecasts for some series. ```python theme={null} Y_hat_df.query('AutoETS < 0') ``` | | unique\_id | ds | AutoETS | Naive | | ---- | ----------------- | ---------- | ----------- | ------ | | 28 | de\_AAC\_AAG\_001 | 2016-12-25 | -523.766907 | 340.0 | | 29 | de\_AAC\_AAG\_001 | 2016-12-26 | -245.337433 | 340.0 | | 30 | de\_AAC\_AAG\_001 | 2016-12-27 | -194.253815 | 340.0 | | 33 | de\_AAC\_AAG\_001 | 2016-12-30 | -315.425659 | 340.0 | | 34 | de\_AAC\_AAG\_001 | 2016-12-31 | -806.920105 | 340.0 | | ... | ... | ... | ... | ... | | 1217 | zh\_AAC\_AAG\_033 | 2016-12-31 | -86.466789 | 37.0 | | 1345 | zh\_MOB | 2016-12-26 | -199.534882 | 1036.0 | | 1346 | zh\_MOB | 2016-12-27 | -69.527260 | 1036.0 | | 1352 | zh\_MOB\_AAG | 2016-12-26 | -199.534882 | 1036.0 | | 1353 | zh\_MOB\_AAG | 2016-12-27 | -69.527260 | 1036.0 | ## 3. Non-Negative Reconciliation The following cell makes the previous forecasts coherent and nonnegative using the `HierarchicalReconciliation` class. ```python theme={null} from hierarchicalforecast.methods import MinTrace from hierarchicalforecast.core import HierarchicalReconciliation ``` ```python theme={null} reconcilers = [ MinTrace(method='ols'), MinTrace(method='ols', nonnegative=True) ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_train_df, S_df=S_df, tags=tags) ``` Observe that the nonnegative reconciliation method obtains nonnegative forecasts. ```python theme={null} Y_rec_df ``` | | unique\_id | ds | AutoETS | Naive | AutoETS/MinTrace\_method-ols | Naive/MinTrace\_method-ols | AutoETS/MinTrace\_method-ols\_nonnegative-True | Naive/MinTrace\_method-ols\_nonnegative-True | | ---- | ----------------- | ---------- | ------------- | ------- | ---------------------------- | -------------------------- | ---------------------------------------------- | -------------------------------------------- | | 0 | Total | 2016-12-25 | 94523.164062 | 95743.0 | 95852.000421 | 95743.0 | 9.664245e+04 | 95743.0 | | 1 | Total | 2016-12-26 | 87734.367188 | 95743.0 | 89525.238276 | 95743.0 | 9.028857e+04 | 95743.0 | | 2 | Total | 2016-12-27 | 87751.125000 | 95743.0 | 89638.119184 | 95743.0 | 9.056593e+04 | 95743.0 | | 3 | Total | 2016-12-28 | 133237.968750 | 95743.0 | 131051.839057 | 95743.0 | 1.314028e+05 | 95743.0 | | 4 | Total | 2016-12-29 | 126501.796875 | 95743.0 | 121214.048604 | 95743.0 | 1.218000e+05 | 95743.0 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 1388 | zh\_MOB\_AAG\_138 | 2016-12-27 | 62.049744 | 65.0 | -147.399760 | 65.0 | 0.000000e+00 | 65.0 | | 1389 | zh\_MOB\_AAG\_138 | 2016-12-28 | 54.934032 | 65.0 | 7.561682 | 65.0 | 4.397229e-15 | 65.0 | | 1390 | zh\_MOB\_AAG\_138 | 2016-12-29 | 60.452618 | 65.0 | 114.253489 | 65.0 | 9.321380e+01 | 65.0 | | 1391 | zh\_MOB\_AAG\_138 | 2016-12-30 | 50.356693 | 65.0 | 96.446754 | 65.0 | 7.565171e+01 | 65.0 | | 1392 | zh\_MOB\_AAG\_138 | 2016-12-31 | 66.735626 | 65.0 | 208.184648 | 65.0 | 1.851130e+02 | 65.0 | ```python theme={null} Y_rec_df.query('`AutoETS/MinTrace_method-ols_nonnegative-True` < 0') ``` | | unique\_id | ds | AutoETS | Naive | AutoETS/MinTrace\_method-ols | Naive/MinTrace\_method-ols | AutoETS/MinTrace\_method-ols\_nonnegative-True | Naive/MinTrace\_method-ols\_nonnegative-True | | - | ---------- | -- | ------- | ----- | ---------------------------- | -------------------------- | ---------------------------------------------- | -------------------------------------------- | The free reconciliation method gets negative forecasts. ```python theme={null} Y_rec_df.query('`AutoETS/MinTrace_method-ols` < 0') ``` | | unique\_id | ds | AutoETS | Naive | AutoETS/MinTrace\_method-ols | Naive/MinTrace\_method-ols | AutoETS/MinTrace\_method-ols\_nonnegative-True | Naive/MinTrace\_method-ols\_nonnegative-True | | ---- | ----------------- | ---------- | ------------ | ----- | ---------------------------- | -------------------------- | ---------------------------------------------- | -------------------------------------------- | | 56 | de\_DES | 2016-12-25 | -2553.932861 | 495.0 | -3818.990043 | 495.0 | 0.000000e+00 | 495.0 | | 57 | de\_DES | 2016-12-26 | -2155.228271 | 495.0 | -3309.806933 | 495.0 | 1.909922e-30 | 495.0 | | 58 | de\_DES | 2016-12-27 | -2720.993896 | 495.0 | -3965.351121 | 495.0 | 1.140223e-13 | 495.0 | | 60 | de\_DES | 2016-12-29 | -3429.432617 | 495.0 | -3042.502484 | 495.0 | 3.049601e+02 | 495.0 | | 61 | de\_DES | 2016-12-30 | -3963.202637 | 495.0 | -3476.273292 | 495.0 | 2.877829e+02 | 495.0 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 1380 | zh\_MOB\_AAG\_036 | 2016-12-26 | 75.298317 | 115.0 | -166.245228 | 115.0 | 0.000000e+00 | 115.0 | | 1381 | zh\_MOB\_AAG\_036 | 2016-12-27 | 72.895554 | 115.0 | -136.553950 | 115.0 | 1.699002e-14 | 115.0 | | 1386 | zh\_MOB\_AAG\_138 | 2016-12-25 | 94.796623 | 65.0 | -49.410174 | 65.0 | 0.000000e+00 | 65.0 | | 1387 | zh\_MOB\_AAG\_138 | 2016-12-26 | 71.293983 | 65.0 | -170.249562 | 65.0 | 0.000000e+00 | 65.0 | | 1388 | zh\_MOB\_AAG\_138 | 2016-12-27 | 62.049744 | 65.0 | -147.399760 | 65.0 | 0.000000e+00 | 65.0 | ## 4. Evaluation The `HierarchicalForecast` package includes the `evaluate` function to evaluate the different hierarchies. We use `utilsforecast` to compute the mean absolute error. ```python theme={null} from hierarchicalforecast.evaluation import evaluate from utilsforecast.losses import mse ``` ```python theme={null} evaluation = evaluate(df = Y_rec_df.merge(Y_test_df, on=['unique_id', 'ds']), tags = tags, train_df = Y_train_df, metrics = [mse], benchmark="Naive") evaluation.set_index(["level", "metric"]).filter(like='ETS') ``` | | | AutoETS | AutoETS/MinTrace\_method-ols | AutoETS/MinTrace\_method-ols\_nonnegative-True | | -------------------------------- | ---------- | -------- | ---------------------------- | ---------------------------------------------- | | level | metric | | | | | Views | mse-scaled | 0.735800 | 0.697371 | 0.675672 | | Views/Country | mse-scaled | 1.190354 | 1.053631 | 0.994758 | | Views/Country/Access | mse-scaled | 1.086102 | 1.133507 | 1.172270 | | Views/Country/Access/Agent | mse-scaled | 1.067394 | 1.100215 | 1.127960 | | Views/Country/Access/Agent/Topic | mse-scaled | 1.435105 | 1.381990 | 1.163428 | | Overall | mse-scaled | 1.010801 | 0.977667 | 0.939286 | Observe that the nonnegative reconciliation method performs better (lower error) than its unconstrained counterpart. ### References * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) * [Wickramasuriya, S. L., Athanasopoulos, G., & Hyndman, R. J. (2019). "Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization". Journal of the American Statistical Association, 114 , 804–819. doi:10.1080/01621459.2018.1448825.](https://robjhyndman.com/publications/mint/). * [Wickramasuriya, S.L., Turlach, B.A. & Hyndman, R.J. (2020). "Optimal non-negative forecast reconciliation”. Stat Comput 30, 1167–1182, https://doi.org/10.1007/s11222-020-09930-0](https://robjhyndman.com/publications/nnmint/). # Probabilistic Reconciliation Methods Comparison Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/probabilistic-reconciliation-comparison.html This notebook compares the different probabilistic reconciliation methods available in HierarchicalForecast: - **Normality**: Gaussian-based, parametric approach - **Bootstrap**: Non-parametric residual resampling - **PERMBU**: Empirical copula-based with rank permutation - **Conformal**: Distribution-free with coverage guarantees under exchangeability ```python theme={null} # Install dependencies if needed # !pip install hierarchicalforecast statsforecast ``` ```python theme={null} import numpy as np import pandas as pd import matplotlib.pyplot as plt from statsforecast.models import AutoARIMA from statsforecast.core import StatsForecast from hierarchicalforecast.utils import aggregate from hierarchicalforecast.core import HierarchicalReconciliation from hierarchicalforecast.methods import BottomUp, MinTrace ``` ## 1. Load and Prepare Data We use the Australian Tourism dataset for this example. ```python theme={null} # Load tourism data Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv') Y_df = Y_df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1) Y_df.insert(0, 'Country', 'Australia') Y_df = Y_df[['Country', 'Region', 'State', 'Purpose', 'ds', 'y']] Y_df['ds'] = Y_df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True) Y_df['ds'] = pd.PeriodIndex(Y_df["ds"], freq='Q').to_timestamp() Y_df.head() ``` | | Country | Region | State | Purpose | ds | y | | - | --------- | -------- | --------------- | -------- | ---------- | ---------- | | 0 | Australia | Adelaide | South Australia | Business | 1998-01-01 | 135.077690 | | 1 | Australia | Adelaide | South Australia | Business | 1998-04-01 | 109.987316 | | 2 | Australia | Adelaide | South Australia | Business | 1998-07-01 | 166.034687 | | 3 | Australia | Adelaide | South Australia | Business | 1998-10-01 | 127.160464 | | 4 | Australia | Adelaide | South Australia | Business | 1999-01-01 | 137.448533 | ```python theme={null} # Define hierarchical structure spec = [ ['Country'], ['Country', 'State'], ['Country', 'State', 'Region'], ] Y_df, S_df, tags = aggregate(df=Y_df, spec=spec) print(f"Hierarchy has {S_df.shape[0]} series ({S_df.shape[1]} bottom-level)") ``` ```text theme={null} Hierarchy has 85 series (77 bottom-level) ``` ```python theme={null} # Train/test split Y_test_df = Y_df.groupby('unique_id').tail(8) Y_train_df = Y_df.drop(Y_test_df.index) print(f"Training: {len(Y_train_df)} observations") print(f"Testing: {len(Y_test_df)} observations") ``` ```text theme={null} Training: 6120 observations Testing: 680 observations ``` ## 2. Compute Base Forecasts ```python theme={null} # Fit base forecaster fcst = StatsForecast( models=[AutoARIMA(season_length=4)], freq='QS', n_jobs=-1 ) # Get forecasts and fitted values for probabilistic methods # Note: level is required for normality intervals to reverse-engineer sigmah Y_hat_df = fcst.forecast(df=Y_train_df, h=8, fitted=True, level=[90]) Y_fitted_df = fcst.forecast_fitted_values() ``` ## 3. Probabilistic Reconciliation Methods ### Method Comparison Table | Method | Distributional Assumptions | Residual Usage | Reconciliation Timing | Coverage Guarantee | Hierarchy Requirement | | ------------- | ------------------------------- | ---------------------------------------------- | --------------------- | ----------------------------------------- | -------------------------- | | **Normality** | Gaussian errors | Computes covariance from residuals | N/A (analytical) | Asymptotic | Any | | **Bootstrap** | None | Block resamples raw residuals, then reconciles | After perturbation | Asymptotic | Any | | **PERMBU** | None (uses empirical marginals) | Rank permutations for copula | Bottom-up aggregation | Asymptotic | Strictly hierarchical only | | **Conformal** | Exchangeability | Scores from reconciled residuals | Before perturbation | Finite-sample: $(1-\alpha) \cdot n/(n+1)$ | Any | ### Key Technical Differences **Bootstrap vs Conformal residual handling:** - **Bootstrap**: Adds residual blocks to *raw* forecasts, then applies reconciliation (`SP @ (y_hat + residuals)`) - **Conformal**: Computes scores from *reconciled* forecasts, adds to reconciled predictions (`y_rec + scores`) ```python theme={null} # Reconcile with different probabilistic methods # Using MinTrace for most methods, BottomUp for PERMBU (which uses bottom-up aggregation internally) reconcilers_mintrace = [MinTrace(method='mint_shrink')] reconcilers_bottomup = [BottomUp()] hrec_mintrace = HierarchicalReconciliation(reconcilers=reconcilers_mintrace) hrec_bottomup = HierarchicalReconciliation(reconcilers=reconcilers_bottomup) ``` ```python theme={null} # Normality-based intervals Y_rec_normality = hrec_mintrace.reconcile( Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags, level=[90], intervals_method='normality' ) print("Normality reconciliation complete") ``` ```text theme={null} Normality reconciliation complete ``` ```python theme={null} # Bootstrap-based intervals Y_rec_bootstrap = hrec_mintrace.reconcile( Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags, level=[90], intervals_method='bootstrap' ) print("Bootstrap reconciliation complete") ``` ```text theme={null} Bootstrap reconciliation complete ``` ```python theme={null} # PERMBU-based intervals (requires strictly hierarchical structure and BottomUp reconciler) Y_rec_permbu = hrec_bottomup.reconcile( Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags, level=[90], intervals_method='permbu' ) print("PERMBU reconciliation complete") ``` ```text theme={null} PERMBU reconciliation complete ``` ```python theme={null} # Conformal-based intervals Y_rec_conformal = hrec_mintrace.reconcile( Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags, level=[90], intervals_method='conformal' ) print("Conformal reconciliation complete") ``` ```text theme={null} Conformal reconciliation complete ``` ## 4. Visualize Prediction Intervals ```python theme={null} def plot_intervals(series_id, ax, rec_df, method_name, color, col_mean): """Helper to plot prediction intervals for a series.""" df = rec_df[rec_df['unique_id'] == series_id].copy() col_lo = f'{col_mean}-lo-90' col_hi = f'{col_mean}-hi-90' ax.fill_between(df['ds'], df[col_lo], df[col_hi], alpha=0.3, color=color, label=f'{method_name} 90% PI') ax.plot(df['ds'], df[col_mean], color=color, linewidth=2) # Plot comparison for top-level series series_id = 'Australia' fig, axes = plt.subplots(2, 2, figsize=(12, 8)) # MinTrace methods use 'AutoARIMA/MinTrace_method-mint_shrink' column col_mintrace = 'AutoARIMA/MinTrace_method-mint_shrink' # BottomUp (PERMBU) uses 'AutoARIMA/BottomUp' column col_bottomup = 'AutoARIMA/BottomUp' plot_intervals(series_id, axes[0, 0], Y_rec_normality, 'Normality', 'blue', col_mintrace) axes[0, 0].set_title('Normality (MinTrace)') axes[0, 0].legend() plot_intervals(series_id, axes[0, 1], Y_rec_bootstrap, 'Bootstrap', 'green', col_mintrace) axes[0, 1].set_title('Bootstrap (MinTrace)') axes[0, 1].legend() plot_intervals(series_id, axes[1, 0], Y_rec_permbu, 'PERMBU', 'red', col_bottomup) axes[1, 0].set_title('PERMBU (BottomUp)') axes[1, 0].legend() plot_intervals(series_id, axes[1, 1], Y_rec_conformal, 'Conformal', 'orange', col_mintrace) axes[1, 1].set_title('Conformal (MinTrace)') axes[1, 1].legend() fig.suptitle(f'Prediction Intervals Comparison: {series_id}', fontsize=14) plt.tight_layout() plt.show() ``` ## 5. Evaluate Probabilistic Forecasts We evaluate the probabilistic forecasts using the **Scaled Continuous Ranked Probability Score (CRPS)**, which measures the quality of probabilistic predictions. Lower values indicate better calibrated prediction intervals. ```python theme={null} from utilsforecast.losses import scaled_crps from hierarchicalforecast.evaluation import evaluate # Prepare evaluation dataframes by merging with test actuals Y_rec_normality_eval = Y_rec_normality.merge(Y_test_df, on=['unique_id', 'ds']) Y_rec_bootstrap_eval = Y_rec_bootstrap.merge(Y_test_df, on=['unique_id', 'ds']) Y_rec_permbu_eval = Y_rec_permbu.merge(Y_test_df, on=['unique_id', 'ds']) Y_rec_conformal_eval = Y_rec_conformal.merge(Y_test_df, on=['unique_id', 'ds']) # Define evaluation tags for each hierarchy level eval_tags = { 'Country': tags['Country'], 'State': tags['Country/State'], 'Region': tags['Country/State/Region'], } # Evaluate each method def eval_method(df, model_name, method_label): """Evaluate a single method and return results.""" result = evaluate( df=df, tags=eval_tags, train_df=Y_train_df, metrics=[scaled_crps], models=[model_name], level=[90], ) result = result.rename(columns={model_name: method_label}) return result # Evaluate all methods eval_normality = eval_method(Y_rec_normality_eval, 'AutoARIMA/MinTrace_method-mint_shrink', 'Normality') eval_bootstrap = eval_method(Y_rec_bootstrap_eval, 'AutoARIMA/MinTrace_method-mint_shrink', 'Bootstrap') eval_permbu = eval_method(Y_rec_permbu_eval, 'AutoARIMA/BottomUp', 'PERMBU') eval_conformal = eval_method(Y_rec_conformal_eval, 'AutoARIMA/MinTrace_method-mint_shrink', 'Conformal') # Combine results evaluation = eval_normality.copy() evaluation['Bootstrap'] = eval_bootstrap['Bootstrap'] evaluation['PERMBU'] = eval_permbu['PERMBU'] evaluation['Conformal'] = eval_conformal['Conformal'] # Format and display print("Scaled CRPS by Hierarchy Level (lower is better):") print("=" * 60) evaluation ``` ```text theme={null} Scaled CRPS by Hierarchy Level (lower is better): ============================================================ ``` | | level | metric | Normality | Bootstrap | PERMBU | Conformal | | - | ------- | ------------ | --------- | --------- | -------- | --------- | | 0 | Country | scaled\_crps | 0.027256 | 0.028403 | 0.086038 | 0.028478 | | 1 | State | scaled\_crps | 0.033189 | 0.031690 | 0.065403 | 0.031761 | | 2 | Region | scaled\_crps | 0.046120 | 0.050435 | 0.055291 | 0.049632 | | 3 | Overall | scaled\_crps | 0.044681 | 0.048412 | 0.056605 | 0.047701 | ## 6. Method Pros and Cons ### Normality **Pros:** - Fast computation using closed-form solutions - Well-understood theoretical properties - Works with any reconciliation method and hierarchy structure - Supports different covariance estimators (`ols`, `wls_var`, `mint_shrink`, etc.) **Cons:** - Assumes Gaussian distribution of errors - May underestimate uncertainty for heavy-tailed or skewed distributions - Covariance estimation can be unstable with limited data *** ### Bootstrap **Pros:** - Non-parametric: no distributional assumptions required - Captures empirical error distribution shape (skewness, heavy tails) - Preserves temporal correlation through block resampling - Works with any hierarchy structure **Cons:** - Requires sufficient historical residuals (at least horizon + some buffer) - Computationally more expensive than Normality - Coverage is asymptotic (no finite-sample guarantees) *** ### PERMBU **Pros:** - Preserves empirical dependencies using copula-based approach - Respects marginal distributions at each level - Captures complex cross-series dependencies **Cons:** - **Only works with strictly hierarchical structures** (not grouped hierarchies) - Computationally intensive for large hierarchies - Requires careful handling of the permutation ordering *** ### Conformal **Pros:** - Distribution-free: no parametric assumptions required - Finite-sample coverage guarantee under exchangeability - Simple interpretation: intervals based on empirical quantiles of scores - Works with any hierarchy structure **Cons:** - **Requires proper calibration set**: Coverage guarantees assume the calibration data is independent from training data. When using in-sample residuals (fitted values from the same data used to train the model), this assumption is violated, potentially leading to overly optimistic (narrow) intervals - Exchangeability assumption may not hold for time series with trends or structural breaks - Coverage guarantees are marginal (per-series), not simultaneous across the hierarchy - May produce wider intervals than well-specified parametric methods **⚠️ Important Caveat for Conformal:** In this example (and the default HierarchicalForecast API), we use in-sample fitted values as the calibration set. This is convenient but technically violates the split conformal prediction framework. For rigorous coverage guarantees, you should use a held-out validation set that was not used for model training. ## 7. Recommendations | Scenario | Recommended Method | Notes | | ------------------------------------------------- | ------------------------------ | ------------------------------------------- | | Quick analysis, errors appear Gaussian | **Normality** | Fastest, well-understood | | Unknown error distribution, general use | **Bootstrap** | Safe default, no assumptions | | Strict hierarchies, need correlation preservation | **PERMBU** | Best for capturing dependencies | | Have proper held-out calibration set | **Conformal** | Valid finite-sample guarantees | | Using in-sample residuals, want simplicity | **Bootstrap** or **Normality** | Conformal caveats apply with in-sample data | ### Decision Flowchart 1. **Is your hierarchy strictly hierarchical (no cross-classifications)?** * Yes → Consider PERMBU if you need correlation preservation * No → Use Normality, Bootstrap, or Conformal 2. **Do you have a proper held-out calibration set?** * Yes → Conformal provides finite-sample coverage guarantees * No (using in-sample) → Bootstrap or Normality are more appropriate 3. **Do your residuals appear Gaussian?** * Yes → Normality is fast and efficient * No / Unknown → Bootstrap adapts to any distribution # Reconciliation Diagnostics Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/reconciliationdiagnostics.html > Understanding and debugging hierarchical forecast reconciliation After reconciling hierarchical forecasts, practitioners often need to answer questions like: * **How incoherent were my base forecasts?** Did they significantly violate the hierarchical constraints? * **How much did reconciliation change the forecasts?** Which levels were adjusted the most? * **Did reconciliation introduce problems?** Such as negative values where they shouldn’t exist? * **Are the reconciled forecasts numerically coherent?** Within acceptable tolerance? The `HierarchicalReconciliation` class provides an optional `diagnostics=True` parameter that generates a comprehensive report answering these questions. This notebook demonstrates the diagnostics feature through three practical use cases. You can run these experiments using CPU or GPU with Google Colab. Open In Colab ## Setup ```python theme={null} !pip install hierarchicalforecast statsforecast datasetsforecast ``` ```python theme={null} import numpy as np import pandas as pd from datasetsforecast.hierarchical import HierarchicalData, HierarchicalInfo from statsforecast import StatsForecast from statsforecast.models import AutoARIMA, Naive from hierarchicalforecast.core import HierarchicalReconciliation from hierarchicalforecast.methods import BottomUp, TopDown, MinTrace ``` ## Load Data We’ll use the TourismSmall dataset which has a 4-level hierarchy: - Country (1 node) - Country/Purpose (4 nodes) - Country/Purpose/State (28 nodes) - Country/Purpose/State/CityNonCity (56 nodes - bottom level) ```python theme={null} group_name = 'TourismSmall' group = HierarchicalInfo.get_group(group_name) Y_df, S_df, tags = HierarchicalData.load('./data', group_name) S_df = S_df.reset_index(names="unique_id") Y_df['ds'] = pd.to_datetime(Y_df['ds']) # Train/test split Y_test_df = Y_df.groupby('unique_id').tail(group.horizon) Y_train_df = Y_df.drop(Y_test_df.index) print(f"Hierarchy levels: {list(tags.keys())}") print(f"Total series: {len(S_df)}") print(f"Bottom series: {S_df.shape[1] - 1}") ``` ```text theme={null} Hierarchy levels: ['Country', 'Country/Purpose', 'Country/Purpose/State', 'Country/Purpose/State/CityNonCity'] Total series: 89 Bottom series: 56 ``` ## Generate Base Forecasts ```python theme={null} fcst = StatsForecast( models=[AutoARIMA(season_length=group.seasonality), Naive()], freq="QE", n_jobs=-1 ) Y_hat_df = fcst.forecast(df=Y_train_df, h=group.horizon) Y_hat_df.head() ``` | | unique\_id | ds | AutoARIMA | Naive | | - | ---------- | ---------- | ------------ | ------- | | 0 | bus | 2006-03-31 | 8918.478516 | 11547.0 | | 1 | bus | 2006-06-30 | 9581.925781 | 11547.0 | | 2 | bus | 2006-09-30 | 11194.676758 | 11547.0 | | 3 | bus | 2006-12-31 | 10678.958008 | 11547.0 | | 4 | hol | 2006-03-31 | 42805.347656 | 26418.0 | *** ## Use Case 1: Verifying Reconciliation Quality **Scenario:** You’ve just run reconciliation and want to verify that it worked correctly - that base forecasts were indeed incoherent and reconciliation fixed them. The diagnostics report answers: - Were the base forecasts incoherent? (coherence residuals before > 0) - Are the reconciled forecasts coherent? (coherence residuals after ≈ 0) - Is numerical coherence satisfied within tolerance? ```python theme={null} # Run reconciliation with diagnostics hrec = HierarchicalReconciliation(reconcilers=[BottomUp(), MinTrace(method='ols')]) Y_rec_df = hrec.reconcile( Y_hat_df=Y_hat_df, Y_df=Y_train_df, S_df=S_df, tags=tags, diagnostics=True # Enable diagnostics ) ``` ```python theme={null} # View overall coherence verification coherence_metrics = hrec.diagnostics.query( "level == 'Overall' and metric in " "['coherence_residual_mae_before', 'coherence_residual_mae_after', 'is_coherent', 'coherence_max_violation']" ) coherence_metrics ``` | | level | metric | AutoARIMA/BottomUp | Naive/BottomUp | AutoARIMA/MinTrace\_method-ols | Naive/MinTrace\_method-ols | | -- | ------- | -------------------------------- | ------------------ | -------------- | ------------------------------ | -------------------------- | | 48 | Overall | coherence\_residual\_mae\_before | 91.123692 | 0.0 | 91.123692 | 0.0 | | 50 | Overall | coherence\_residual\_mae\_after | 0.000000 | 0.0 | 0.000000 | 0.0 | | 60 | Overall | is\_coherent | 1.000000 | 1.0 | 1.000000 | 1.0 | | 61 | Overall | coherence\_max\_violation | 0.000000 | 0.0 | 0.000000 | 0.0 | **Interpretation:** - `coherence_residual_mae_before > 0`: Base forecasts violated hierarchical constraints - `coherence_residual_mae_after ≈ 0`: Reconciliation fixed the incoherence - `is_coherent = 1.0`: Reconciled forecasts satisfy constraints within tolerance - `coherence_max_violation`: Maximum deviation from perfect coherence (should be tiny) ```python theme={null} # View coherence residuals by hierarchy level residuals_by_level = hrec.diagnostics.query( "metric in ['coherence_residual_mae_before', 'coherence_residual_mae_after']" ).pivot(index='level', columns='metric') residuals_by_level ``` | | AutoARIMA/BottomUp | | Naive/BottomUp | | AutoARIMA/MinTrace\_method-ols | | Naive/MinTrace\_method-ols | | | --------------------------------- | ------------------------------- | -------------------------------- | ------------------------------- | -------------------------------- | ------------------------------- | -------------------------------- | ------------------------------- | -------------------------------- | | metric | coherence\_residual\_mae\_after | coherence\_residual\_mae\_before | coherence\_residual\_mae\_after | coherence\_residual\_mae\_before | coherence\_residual\_mae\_after | coherence\_residual\_mae\_before | coherence\_residual\_mae\_after | coherence\_residual\_mae\_before | | level | | | | | | | | | | Country | 0.0 | 1551.154858 | 0.0 | 0.0 | 0.0 | 1551.154858 | 0.0 | 0.0 | | Country/Purpose | 0.0 | 996.859118 | 0.0 | 0.0 | 0.0 | 996.859118 | 0.0 | 0.0 | | Country/Purpose/State | 0.0 | 91.836329 | 0.0 | 0.0 | 0.0 | 91.836329 | 0.0 | 0.0 | | Country/Purpose/State/CityNonCity | 0.0 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.0 | 0.0 | | Overall | 0.0 | 91.123692 | 0.0 | 0.0 | 0.0 | 91.123692 | 0.0 | 0.0 | Note that bottom-level series always have 0 coherence residual (they define the hierarchy), while aggregate levels show how much they deviated from the sum of their children. *** ## Use Case 2: Comparing Reconciliation Methods **Scenario:** You want to understand how different reconciliation methods affect your forecasts differently. Which method makes smaller adjustments? Which levels are most impacted? The diagnostics report helps compare: - Adjustment magnitude (MAE, RMSE, max) across methods - Which hierarchy levels each method adjusts the most ```python theme={null} # Run multiple reconciliation methods hrec_compare = HierarchicalReconciliation(reconcilers=[ BottomUp(), TopDown(method='forecast_proportions'), MinTrace(method='ols'), MinTrace(method='wls_struct'), ]) Y_rec_compare = hrec_compare.reconcile( Y_hat_df=Y_hat_df, Y_df=Y_train_df, S_df=S_df, tags=tags, diagnostics=True ) ``` ```python theme={null} # Compare adjustment magnitude across methods (Overall level) adjustment_comparison = hrec_compare.diagnostics.query( "level == 'Overall' and metric in ['adjustment_mae', 'adjustment_rmse', 'adjustment_max']" ) adjustment_comparison ``` | | level | metric | AutoARIMA/BottomUp | Naive/BottomUp | AutoARIMA/TopDown\_method-forecast\_proportions | Naive/TopDown\_method-forecast\_proportions | AutoARIMA/MinTrace\_method-ols | Naive/MinTrace\_method-ols | AutoARIMA/MinTrace\_method-wls\_struct | Naive/MinTrace\_method-wls\_struct | | -- | ------- | ---------------- | ------------------ | -------------- | ----------------------------------------------- | ------------------------------------------- | ------------------------------ | -------------------------- | -------------------------------------- | ---------------------------------- | | 52 | Overall | adjustment\_mae | 91.123692 | 0.0 | 152.381830 | 0.0 | 125.796357 | 7.790422e-13 | 92.567005 | 3.649316e-13 | | 53 | Overall | adjustment\_rmse | 361.699708 | 0.0 | 327.852747 | 0.0 | 235.618628 | 1.956331e-12 | 297.653444 | 7.211469e-13 | | 54 | Overall | adjustment\_max | 3563.736473 | 0.0 | 2354.425237 | 0.0 | 1367.921921 | 1.455192e-11 | 2621.788616 | 3.637979e-12 | **Key insights:** - **BottomUp** only adjusts aggregate levels (bottom level has 0 adjustment) - **TopDown** only adjusts bottom levels (top level has 0 adjustment) - **MinTrace** methods distribute adjustments across all levels, typically with smaller overall adjustments ```python theme={null} # Compare adjustments by hierarchy level for AutoARIMA forecasts adjustment_by_level = hrec_compare.diagnostics.query("metric == 'adjustment_mae'") # Pivot for easier comparison adjustment_pivot = adjustment_by_level.set_index('level').drop(columns=['metric']) adjustment_pivot.columns = [c.replace('AutoARIMA/', '') for c in adjustment_pivot.columns] adjustment_pivot = adjustment_pivot[[c for c in adjustment_pivot.columns if 'AutoARIMA' in c or 'Naive' not in c]] adjustment_pivot ``` | | BottomUp | TopDown\_method-forecast\_proportions | MinTrace\_method-ols | MinTrace\_method-wls\_struct | | --------------------------------- | ----------- | ------------------------------------- | -------------------- | ---------------------------- | | level | | | | | | Country | 1551.154858 | 0.000000 | 924.028186 | 1953.754301 | | Country/Purpose | 996.859118 | 1106.796143 | 875.789096 | 666.870396 | | Country/Purpose/State | 91.836329 | 151.248239 | 114.460983 | 61.695544 | | Country/Purpose/State/CityNonCity | 0.000000 | 87.497279 | 63.638995 | 33.745576 | | Overall | 91.123692 | 152.381830 | 125.796357 | 92.567005 | This shows how each method distributes adjustments across hierarchy levels. BottomUp concentrates changes at aggregate levels, TopDown at bottom levels, and MinTrace spreads adjustments more evenly. *** ## Use Case 3: Detecting Negative Value Issues **Scenario:** Your forecasts represent quantities that cannot be negative (e.g., sales, visitors). You need to check if reconciliation introduced negative values. The diagnostics report tracks: - `negative_count_before/after`: Count of negative values before and after reconciliation - `negative_introduced`: Negatives created by reconciliation - `negative_removed`: Negatives fixed by reconciliation ```python theme={null} # Create forecasts with some negative values to demonstrate Y_hat_with_negatives = Y_hat_df.copy() # Introduce some negative base forecasts at random locations in the bottom level bottom_ids = tags['Country/Purpose/State/CityNonCity'] mask = Y_hat_with_negatives['unique_id'].isin(bottom_ids[:10]) Y_hat_with_negatives.loc[mask, 'AutoARIMA'] -= 5000 Y_hat_with_negatives.loc[mask, 'Naive'] -= 5000 print(f"Negative forecasts introduced for AutoARIMA: {(Y_hat_with_negatives['AutoARIMA'] < 0).sum()}") print(f"Negative forecasts introduced for Naive: {(Y_hat_with_negatives['Naive'] < 0).sum()}") ``` ```text theme={null} Negative forecasts introduced for AutoARIMA: 33 Negative forecasts introduced for Naive: 36 ``` ```python theme={null} # Run reconciliation with diagnostics hrec_neg = HierarchicalReconciliation(reconcilers=[ BottomUp(), MinTrace(method='ols'), MinTrace(method='ols', nonnegative=True), # Non-negative constraint ]) Y_rec_neg = hrec_neg.reconcile( Y_hat_df=Y_hat_with_negatives, Y_df=Y_train_df, S_df=S_df, tags=tags, diagnostics=True ) ``` ```python theme={null} # Check negative value metrics at Overall level negative_metrics = hrec_neg.diagnostics.query( "level == 'Overall' and metric in " "['negative_count_before', 'negative_count_after', 'negative_introduced', 'negative_removed']" ) negative_metrics ``` | | level | metric | AutoARIMA/BottomUp | Naive/BottomUp | AutoARIMA/MinTrace\_method-ols | Naive/MinTrace\_method-ols | AutoARIMA/MinTrace\_method-ols\_nonnegative-True | Naive/MinTrace\_method-ols\_nonnegative-True | | -- | ------- | ----------------------- | ------------------ | -------------- | ------------------------------ | -------------------------- | ------------------------------------------------ | -------------------------------------------- | | 56 | Overall | negative\_count\_before | 33.0 | 36.0 | 33.0 | 36.0 | 33.0 | 36.0 | | 57 | Overall | negative\_count\_after | 55.0 | 60.0 | 3.0 | 4.0 | 0.0 | 0.0 | | 58 | Overall | negative\_introduced | 22.0 | 24.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 59 | Overall | negative\_removed | 0.0 | 0.0 | 30.0 | 32.0 | 33.0 | 36.0 | **Interpretation:** - `negative_count_before`: Negatives in base forecasts - `negative_count_after`: Negatives after reconciliation - `negative_introduced`: New negatives created by reconciliation (bad!) - `negative_removed`: Negatives fixed by reconciliation (good!) Notice how `MinTrace` with `nonnegative=True` eliminates all negative values. ```python theme={null} # Check which levels have negative value issues negatives_by_level = hrec_neg.diagnostics.query( "metric in ['negative_count_before', 'negative_count_after']" ).pivot(index='level', columns='metric') negatives_by_level ``` | | AutoARIMA/BottomUp | | Naive/BottomUp | | AutoARIMA/MinTrace\_method-ols | | Naive/MinTrace\_method-ols | | AutoARIMA/MinTrace\_method-ols\_nonnegative-True | | Naive/MinTrace\_method-ols\_nonnegative-True | | | --------------------------------- | ---------------------- | ----------------------- | ---------------------- | ----------------------- | ------------------------------ | ----------------------- | -------------------------- | ----------------------- | ------------------------------------------------ | ----------------------- | -------------------------------------------- | ----------------------- | | metric | negative\_count\_after | negative\_count\_before | negative\_count\_after | negative\_count\_before | negative\_count\_after | negative\_count\_before | negative\_count\_after | negative\_count\_before | negative\_count\_after | negative\_count\_before | negative\_count\_after | negative\_count\_before | | level | | | | | | | | | | | | | | Country | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | Country/Purpose | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | Country/Purpose/State | 7.0 | 0.0 | 8.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | Country/Purpose/State/CityNonCity | 15.0 | 15.0 | 16.0 | 16.0 | 0.0 | 15.0 | 0.0 | 16.0 | 0.0 | 15.0 | 0.0 | 16.0 | | Overall | 22.0 | 15.0 | 24.0 | 16.0 | 0.0 | 15.0 | 0.0 | 16.0 | 0.0 | 15.0 | 0.0 | 16.0 | This shows that BottomUp propagates negatives from bottom to aggregate levels, while standard MinTrace may spread negatives further. The nonnegative MinTrace variant addresses this. *** ## Exporting Diagnostics The diagnostics DataFrame can be exported to CSV for CI pipelines, benchmarks, or sharing with stakeholders. ```python theme={null} # Export full diagnostics report # hrec.diagnostics.to_csv("reconciliation_diagnostics.csv", index=False) # Or export a summary summary = hrec.diagnostics.query("level == 'Overall'").copy() summary ``` | | level | metric | AutoARIMA/BottomUp | Naive/BottomUp | AutoARIMA/MinTrace\_method-ols | Naive/MinTrace\_method-ols | | -- | ------- | --------------------------------- | ------------------ | -------------- | ------------------------------ | -------------------------- | | 48 | Overall | coherence\_residual\_mae\_before | 91.123692 | 0.0 | 91.123692 | 0.000000e+00 | | 49 | Overall | coherence\_residual\_rmse\_before | 361.699708 | 0.0 | 361.699708 | 0.000000e+00 | | 50 | Overall | coherence\_residual\_mae\_after | 0.000000 | 0.0 | 0.000000 | 0.000000e+00 | | 51 | Overall | coherence\_residual\_rmse\_after | 0.000000 | 0.0 | 0.000000 | 0.000000e+00 | | 52 | Overall | adjustment\_mae | 91.123692 | 0.0 | 125.796357 | 7.790422e-13 | | 53 | Overall | adjustment\_rmse | 361.699708 | 0.0 | 235.618628 | 1.956331e-12 | | 54 | Overall | adjustment\_max | 3563.736473 | 0.0 | 1367.921921 | 1.455192e-11 | | 55 | Overall | adjustment\_mean | 29.283713 | 0.0 | 46.279825 | -5.114311e-13 | | 56 | Overall | negative\_count\_before | 0.000000 | 0.0 | 0.000000 | 0.000000e+00 | | 57 | Overall | negative\_count\_after | 0.000000 | 0.0 | 2.000000 | 0.000000e+00 | | 58 | Overall | negative\_introduced | 0.000000 | 0.0 | 2.000000 | 0.000000e+00 | | 59 | Overall | negative\_removed | 0.000000 | 0.0 | 0.000000 | 0.000000e+00 | | 60 | Overall | is\_coherent | 1.000000 | 1.0 | 1.000000 | 1.000000e+00 | | 61 | Overall | coherence\_max\_violation | 0.000000 | 0.0 | 0.000000 | 0.000000e+00 | *** ## Summary of Diagnostic Metrics | Metric | Description | Interpretation | | -------------------------------------- | ---------------------------------------------------- | --------------------------------------- | | `coherence_residual_mae_before` | Mean absolute incoherence before reconciliation | Higher = more incoherent base forecasts | | `coherence_residual_mae_after` | Mean absolute incoherence after reconciliation | Should be \~0 | | `coherence_residual_rmse_before/after` | RMSE variant of above | More sensitive to large violations | | `adjustment_mae` | Mean absolute change made by reconciliation | Higher = more forecast modification | | `adjustment_rmse` | RMSE of adjustments | More sensitive to large changes | | `adjustment_max` | Maximum absolute adjustment | Identifies extreme changes | | `adjustment_mean` | Mean adjustment (signed) | Shows directional bias | | `negative_count_before` | Count of negatives in base forecasts | - | | `negative_count_after` | Count of negatives after reconciliation | Should be 0 for non-negative data | | `negative_introduced` | Negatives created by reconciliation | Warning sign if > 0 | | `negative_removed` | Negatives fixed by reconciliation | Good if > 0 | | `is_coherent` | Whether forecasts satisfy constraints (Overall only) | 1.0 = coherent | | `coherence_max_violation` | Maximum coherence violation (Overall only) | Should be \< tolerance | ## References * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”](https://otexts.com/fpp3/hierarchical.html) * [Wickramasuriya, S. L., Athanasopoulos, G., & Hyndman, R. J. (2019). Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization.](https://robjhyndman.com/papers/mint.pdf) # Probabilistic Forecast Evaluation Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/tourismlarge-evaluation.html > Hierarchical Forecast’s reconciliation and evaluation. This notebook offers a step to step guide to create a hierarchical forecasting pipeline. In the pipeline we will use `HierarchicalForecast` and `StatsForecast` core class, to create base predictions, reconcile and evaluate them. We will use the TourismL dataset that summarizes large Australian national visitor survey. Outline 1. Installing Packages 2. Prepare TourismL dataset - Read and aggregate - StatsForecast’s Base Predictions 3. Reconciliar 4. Evaluar Open In Colab ## 1. Installing HierarchicalForecast We assume you have StatsForecast and HierarchicalForecast already installed, if not check this guide for instructions on how to install HierarchicalForecast. ```python theme={null} !pip install hierarchicalforecast statsforecast datasetsforecast ``` ```python theme={null} import os import numpy as np import pandas as pd import matplotlib.pyplot as plt from statsforecast.core import StatsForecast from statsforecast.models import AutoARIMA, Naive from hierarchicalforecast.core import HierarchicalReconciliation from hierarchicalforecast.methods import BottomUp, TopDown, MinTrace, ERM from hierarchicalforecast.utils import is_strictly_hierarchical from hierarchicalforecast.utils import HierarchicalPlot, CodeTimer from datasetsforecast.hierarchical import HierarchicalData, HierarchicalInfo ``` ## 2. Preparing TourismL Dataset ### 2.1 Read Hierarchical Dataset ```python theme={null} # ['Labour', 'Traffic', 'TourismSmall', 'TourismLarge', 'Wiki2'] dataset = 'TourismSmall' # 'TourismLarge' verbose = True intervals_method = 'bootstrap' LEVEL = np.arange(0, 100, 2) ``` ```python theme={null} with CodeTimer('Read and Parse data ', verbose): print(f'{dataset}') if not os.path.exists('./data'): os.makedirs('./data') dataset_info = HierarchicalInfo[dataset] Y_df, S_df, tags = HierarchicalData.load(directory=f'./data/{dataset}', group=dataset) Y_df['ds'] = pd.to_datetime(Y_df['ds']) # Train/Test Splits horizon = dataset_info.horizon seasonality = dataset_info.seasonality Y_test_df = Y_df.groupby('unique_id', as_index=False).tail(horizon) Y_train_df = Y_df.drop(Y_test_df.index) S_df = S_df.reset_index(names="unique_id") ``` ```text theme={null} TourismSmall Code block 'Read and Parse data ' took: 0.00653 seconds ``` ```python theme={null} dataset_info.seasonality ``` ```text theme={null} 4 ``` ```python theme={null} hplot = HierarchicalPlot(S=S_df, tags=tags) hplot.plot_summing_matrix() ``` ```python theme={null} Y_train_df ``` | | unique\_id | ds | y | | ---- | -------------- | ---------- | ----- | | 0 | total | 1998-03-31 | 84503 | | 1 | total | 1998-06-30 | 65312 | | 2 | total | 1998-09-30 | 72753 | | 3 | total | 1998-12-31 | 70880 | | 4 | total | 1999-03-31 | 86893 | | ... | ... | ... | ... | | 3191 | nt-oth-noncity | 2003-12-31 | 132 | | 3192 | nt-oth-noncity | 2004-03-31 | 12 | | 3193 | nt-oth-noncity | 2004-06-30 | 40 | | 3194 | nt-oth-noncity | 2004-09-30 | 186 | | 3195 | nt-oth-noncity | 2004-12-31 | 144 | ### 2.2 StatsForecast’s Base Predictions This cell computes the base predictions `Y_hat_df` for all the series in `Y_df` using StatsForecast’s `AutoARIMA`. Additionally we obtain insample predictions `Y_fitted_df` for the methods that require them. ```python theme={null} with CodeTimer('Fit/Predict Model ', verbose): # Read to avoid unnecesary AutoARIMA computation yhat_file = f'./data/{dataset}/Y_hat.csv' yfitted_file = f'./data/{dataset}/Y_fitted.csv' if os.path.exists(yhat_file): Y_hat_df = pd.read_csv(yhat_file, parse_dates=['ds']) Y_fitted_df = pd.read_csv(yfitted_file, parse_dates=['ds']) else: fcst = StatsForecast( models=[AutoARIMA(season_length=seasonality)], fallback_model=[Naive()], freq=dataset_info.freq, n_jobs=-1 ) Y_hat_df = fcst.forecast(df=Y_train_df, h=horizon, fitted=True, level=LEVEL) Y_fitted_df = fcst.forecast_fitted_values() Y_hat_df.to_csv(yhat_file, index=False) Y_fitted_df.to_csv(yfitted_file, index=False) ``` ## 3. Reconcile Predictions ```python theme={null} with CodeTimer('Reconcile Predictions ', verbose): if is_strictly_hierarchical(S=S_df.drop(columns="unique_id").values.astype(np.float32), tags={key: S_df["unique_id"].isin(val).values.nonzero()[0] for key, val in tags.items()}): reconcilers = [ BottomUp(), TopDown(method='average_proportions'), TopDown(method='proportion_averages'), MinTrace(method='ols'), MinTrace(method='wls_var'), MinTrace(method='mint_shrink'), ERM(method='closed'), ] else: reconcilers = [ BottomUp(), MinTrace(method='ols'), MinTrace(method='wls_var'), MinTrace(method='mint_shrink'), ERM(method='closed'), ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.bootstrap_reconcile(Y_hat_df=Y_hat_df, Y_df=Y_fitted_df, S_df=S_df, tags=tags, level=LEVEL, intervals_method=intervals_method, num_samples=10, num_seeds=10) Y_rec_df = Y_rec_df.merge(Y_test_df, on=['unique_id', 'ds'], how="left") ``` ```text theme={null} Code block 'Reconcile Predictions ' took: 7.49314 seconds ``` Qualitative evaluation, of parsed quantiles ```python theme={null} unique_id = "total" plot_df = Y_rec_df.query("unique_id == @unique_id").groupby(["unique_id", "ds"], as_index=False).mean() for col in hrec.level_names['AutoARIMA/BottomUp']: plt.plot(plot_df["ds"], plot_df[col], color="orange") plt.plot(plot_df["ds"], plot_df["y"], label="True") plt.title(f"AutoARIMA/BottomUp - {unique_id}") plt.legend() ``` ## 4. Evaluation ```python theme={null} from utilsforecast.losses import scaled_crps, msse from hierarchicalforecast.evaluation import evaluate from functools import partial ``` ```python theme={null} with CodeTimer('Evaluate Models CRPS and MSSE ', verbose): metrics_seeds = [] for seed in Y_rec_df.seed.unique(): df_seed = Y_rec_df.query("seed == @seed") metrics_seed = evaluate(df = df_seed, tags = tags, metrics = [scaled_crps, partial(msse, seasonality=4)], models= hrec.level_names.keys(), level = LEVEL, train_df = Y_train_df, ) metrics_seed['seed'] = seed metrics_seeds.append(metrics_seed) metrics_seeds = pd.concat(metrics_seeds) metrics_mean = metrics_seeds.groupby(["level", "metric"], as_index=False).mean() metrics_std = metrics_seeds.groupby(["level", "metric"], as_index=False).std() results = metrics_mean[hrec.level_names.keys()].round(3).astype(str) + "±" + metrics_std[hrec.level_names.keys()].round(4).astype(str) results.insert(0, "metric", metrics_mean["metric"]) results.insert(0, "level", metrics_mean["level"]) results.sort_values(by=["metric", "level"]) ``` ```text theme={null} Code block 'Evaluate Models CRPS and MSSE ' took: 4.25192 seconds ``` | | level | metric | AutoARIMA/BottomUp | AutoARIMA/TopDown\_method-average\_proportions | AutoARIMA/TopDown\_method-proportion\_averages | AutoARIMA/MinTrace\_method-ols | AutoARIMA/MinTrace\_method-wls\_var | AutoARIMA/MinTrace\_method-mint\_shrink | AutoARIMA/ERM\_method-closed\_lambda\_reg-0.01 | | - | --------------------------------- | ------------ | ------------------ | ---------------------------------------------- | ---------------------------------------------- | ------------------------------ | ----------------------------------- | --------------------------------------- | ---------------------------------------------- | | 0 | Country | msse | 1.777±0.0 | 2.488±0.0 | 2.488±0.0 | 2.752±0.0 | 2.569±0.0 | 2.775±0.0 | 3.427±0.0 | | 2 | Country/Purpose | msse | 1.726±0.0 | 3.181±0.0 | 3.169±0.0 | 2.184±0.0 | 1.876±0.0 | 1.96±0.0 | 3.067±0.0 | | 4 | Country/Purpose/State | msse | 0.881±0.0 | 1.657±0.0 | 1.652±0.0 | 0.98±0.0 | 0.857±0.0 | 0.867±0.0 | 1.559±0.0 | | 6 | Country/Purpose/State/CityNonCity | msse | 0.95±0.0 | 1.271±0.0 | 1.269±0.0 | 1.033±0.0 | 0.903±0.0 | 0.912±0.0 | 1.635±0.0 | | 8 | Overall | msse | 0.973±0.0 | 1.492±0.0 | 1.488±0.0 | 1.087±0.0 | 0.951±0.0 | 0.966±0.0 | 1.695±0.0 | | 1 | Country | scaled\_crps | 0.043±0.0009 | 0.048±0.0006 | 0.048±0.0006 | 0.05±0.0006 | 0.051±0.0006 | 0.053±0.0006 | 0.054±0.0009 | | 3 | Country/Purpose | scaled\_crps | 0.077±0.001 | 0.114±0.0003 | 0.112±0.0004 | 0.09±0.0013 | 0.087±0.0009 | 0.089±0.0009 | 0.106±0.0013 | | 5 | Country/Purpose/State | scaled\_crps | 0.165±0.0009 | 0.249±0.0004 | 0.247±0.0004 | 0.18±0.0018 | 0.169±0.0009 | 0.169±0.0008 | 0.231±0.0021 | | 7 | Country/Purpose/State/CityNonCity | scaled\_crps | 0.218±0.0013 | 0.289±0.0004 | 0.286±0.0004 | 0.228±0.0018 | 0.217±0.0013 | 0.218±0.0011 | 0.302±0.0033 | | 9 | Overall | scaled\_crps | 0.193±0.0011 | 0.266±0.0004 | 0.263±0.0004 | 0.205±0.0017 | 0.194±0.0011 | 0.195±0.0009 | 0.268±0.0027 | ## References * [Syama Sundar Rangapuram, Lucien D Werner, Konstantinos Benidis, Pedro Mercado, Jan Gasthaus, Tim Januschowski. (2021). "End-to-End Learning of Coherent Probabilistic Forecasts for Hierarchical Time Series". Proceedings of the 38th International Conference on Machine Learning (ICML).](https://proceedings.mlr.press/v139/rangapuram21a.html) * [Kin G. Olivares, O. Nganba Meetei, Ruijun Ma, Rohan Reddy, Mengfei Cao, Lee Dicker (2022). “Probabilistic Hierarchical Forecasting with Deep Poisson Mixtures”. Submitted to the International Journal Forecasting, Working paper available at arxiv.](https://arxiv.org/pdf/2110.13179.pdf) # Quick Start | HierarchicalForecast Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/tourismsmall.html > Minimal Example of Hierarchical Reconciliation Large collections of time series organized into structures at different aggregation levels often require their forecasts to follow their aggregation constraints, which poses the challenge of creating novel algorithms capable of coherent forecasts. The `HierarchicalForecast` package provides a wide collection of Python implementations of hierarchical forecasting algorithms that follow classic hierarchical reconciliation. In this notebook we will show how to use the `StatsForecast` library to produce base forecasts, and use `HierarchicalForecast` package to perform hierarchical reconciliation. You can run these experiments using CPU or GPU with Google Colab. Open In Colab ## 1. Libraries ```python theme={null} !pip install hierarchicalforecast statsforecast datasetsforecast ``` ## 2. Load Data In this example we will use the `TourismSmall` dataset. The following cell gets the time series for the different levels in the hierarchy, the summing matrix `S` which recovers the full dataset from the bottom level hierarchy and the indices of each hierarchy denoted by `tags`. ```python theme={null} import pandas as pd from datasetsforecast.hierarchical import HierarchicalData, HierarchicalInfo ``` ```python theme={null} group_name = 'TourismSmall' group = HierarchicalInfo.get_group(group_name) Y_df, S_df, tags = HierarchicalData.load('./data', group_name) S_df = S_df.reset_index(names="unique_id") Y_df['ds'] = pd.to_datetime(Y_df['ds']) ``` ```python theme={null} S_df.iloc[:6, :6] ``` | | unique\_id | nsw-hol-city | nsw-hol-noncity | vic-hol-city | vic-hol-noncity | qld-hol-city | | - | ---------- | ------------ | --------------- | ------------ | --------------- | ------------ | | 0 | total | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | | 1 | hol | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | | 2 | vfr | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 3 | bus | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 4 | oth | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 5 | nsw-hol | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | ```python theme={null} tags ``` ```text theme={null} {'Country': array(['total'], dtype=object), 'Country/Purpose': array(['hol', 'vfr', 'bus', 'oth'], dtype=object), 'Country/Purpose/State': array(['nsw-hol', 'vic-hol', 'qld-hol', 'sa-hol', 'wa-hol', 'tas-hol', 'nt-hol', 'nsw-vfr', 'vic-vfr', 'qld-vfr', 'sa-vfr', 'wa-vfr', 'tas-vfr', 'nt-vfr', 'nsw-bus', 'vic-bus', 'qld-bus', 'sa-bus', 'wa-bus', 'tas-bus', 'nt-bus', 'nsw-oth', 'vic-oth', 'qld-oth', 'sa-oth', 'wa-oth', 'tas-oth', 'nt-oth'], dtype=object), 'Country/Purpose/State/CityNonCity': array(['nsw-hol-city', 'nsw-hol-noncity', 'vic-hol-city', 'vic-hol-noncity', 'qld-hol-city', 'qld-hol-noncity', 'sa-hol-city', 'sa-hol-noncity', 'wa-hol-city', 'wa-hol-noncity', 'tas-hol-city', 'tas-hol-noncity', 'nt-hol-city', 'nt-hol-noncity', 'nsw-vfr-city', 'nsw-vfr-noncity', 'vic-vfr-city', 'vic-vfr-noncity', 'qld-vfr-city', 'qld-vfr-noncity', 'sa-vfr-city', 'sa-vfr-noncity', 'wa-vfr-city', 'wa-vfr-noncity', 'tas-vfr-city', 'tas-vfr-noncity', 'nt-vfr-city', 'nt-vfr-noncity', 'nsw-bus-city', 'nsw-bus-noncity', 'vic-bus-city', 'vic-bus-noncity', 'qld-bus-city', 'qld-bus-noncity', 'sa-bus-city', 'sa-bus-noncity', 'wa-bus-city', 'wa-bus-noncity', 'tas-bus-city', 'tas-bus-noncity', 'nt-bus-city', 'nt-bus-noncity', 'nsw-oth-city', 'nsw-oth-noncity', 'vic-oth-city', 'vic-oth-noncity', 'qld-oth-city', 'qld-oth-noncity', 'sa-oth-city', 'sa-oth-noncity', 'wa-oth-city', 'wa-oth-noncity', 'tas-oth-city', 'tas-oth-noncity', 'nt-oth-city', 'nt-oth-noncity'], dtype=object)} ``` We split the dataframe in train/test splits. ```python theme={null} Y_test_df = Y_df.groupby('unique_id').tail(group.horizon) Y_train_df = Y_df.drop(Y_test_df.index) ``` ## 3. Base forecasts The following cell computes the *base forecast* for each time series using the `auto_arima` and `naive` models. Observe that `Y_hat_df` contains the forecasts but they are not coherent. ```python theme={null} from statsforecast.core import StatsForecast from statsforecast.models import AutoARIMA, Naive ``` ```text theme={null} /home/osprangers/Repositories/hierarchicalforecast/.venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm ``` ```python theme={null} fcst = StatsForecast( models=[AutoARIMA(season_length=group.seasonality), Naive()], freq="QE", n_jobs=-1 ) Y_hat_df = fcst.forecast(df=Y_train_df, h=group.horizon) ``` ## 4. Hierarchical reconciliation The following cell makes the previous forecasts coherent using the `HierarchicalReconciliation` class. The used methods to make the forecasts coherent are: * `BottomUp`: The reconciliation of the method is a simple addition to the upper levels. * `TopDown`: The second method constrains the base-level predictions to the top-most aggregate-level serie and then distributes it to the disaggregate series through the use of proportions. * `MiddleOut`: Anchors the base predictions in a middle level. ```python theme={null} from hierarchicalforecast.core import HierarchicalReconciliation from hierarchicalforecast.methods import BottomUp, TopDown, MiddleOut ``` ```python theme={null} reconcilers = [ BottomUp(), TopDown(method='forecast_proportions'), TopDown(method='proportion_averages'), MiddleOut(middle_level="Country/Purpose/State", top_down_method="proportion_averages"), ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_train_df, S_df=S_df, tags=tags) ``` ## 4.1 Coherence Diagnostics The `reconcile` method supports an optional `diagnostics=True` parameter that computes a detailed report showing how reconciliation changed the forecasts. This is useful for: * Verifying that base forecasts were incoherent and reconciliation fixed them * Understanding which hierarchy levels were adjusted the most * Detecting if reconciliation introduced negative values * Confirming numerical coherence within tolerance ```python theme={null} # Run reconciliation with diagnostics enabled hrec_diag = HierarchicalReconciliation(reconcilers=[BottomUp(), TopDown(method='forecast_proportions')]) Y_rec_diag_df = hrec_diag.reconcile( Y_hat_df=Y_hat_df, Y_df=Y_train_df, S_df=S_df, tags=tags, diagnostics=True # Enable coherence diagnostics ) ``` The diagnostics are stored in `hrec.diagnostics` as a DataFrame with metrics per hierarchical level: ```python theme={null} # View the full diagnostics report hrec_diag.diagnostics ``` | | level | metric | AutoARIMA/BottomUp | Naive/BottomUp | AutoARIMA/TopDown\_method-forecast\_proportions | Naive/TopDown\_method-forecast\_proportions | | --- | ------- | --------------------------------- | ------------------ | -------------- | ----------------------------------------------- | ------------------------------------------- | | 0 | Country | coherence\_residual\_mae\_before | 1551.154858 | 0.0 | 1.551155e+03 | 0.0 | | 1 | Country | coherence\_residual\_rmse\_before | 1823.566338 | 0.0 | 1.823566e+03 | 0.0 | | 2 | Country | coherence\_residual\_mae\_after | 0.000000 | 0.0 | 7.275958e-12 | 0.0 | | 3 | Country | coherence\_residual\_rmse\_after | 0.000000 | 0.0 | 1.455192e-11 | 0.0 | | 4 | Country | adjustment\_mae | 1551.154858 | 0.0 | 0.000000e+00 | 0.0 | | ... | ... | ... | ... | ... | ... | ... | | 57 | Overall | negative\_count\_after | 0.000000 | 0.0 | 0.000000e+00 | 0.0 | | 58 | Overall | negative\_introduced | 0.000000 | 0.0 | 0.000000e+00 | 0.0 | | 59 | Overall | negative\_removed | 0.000000 | 0.0 | 0.000000e+00 | 0.0 | | 60 | Overall | is\_coherent | 1.000000 | 1.0 | 1.000000e+00 | 1.0 | | 61 | Overall | coherence\_max\_violation | 0.000000 | 0.0 | 2.910383e-11 | 0.0 | **Key metrics explained:** * `coherence_residual_mae_before`: Mean absolute incoherence in base forecasts (should be > 0 if base forecasts are incoherent) * `coherence_residual_mae_after`: Mean absolute incoherence after reconciliation (should be \~0) * `adjustment_mae/rmse/max`: How much forecasts were adjusted by reconciliation * `negative_count_before/after`: Count of negative forecast values * `is_coherent`: Whether the reconciled forecasts satisfy the hierarchical constraints (1.0 = yes) Let’s filter to see just the coherence verification: ```python theme={null} # Check coherence metrics at the Overall level coherence_check = hrec_diag.diagnostics.query( "level == 'Overall' and metric in ['coherence_residual_mae_before', 'coherence_residual_mae_after', 'is_coherent', 'coherence_max_violation']" ) coherence_check ``` | | level | metric | AutoARIMA/BottomUp | Naive/BottomUp | AutoARIMA/TopDown\_method-forecast\_proportions | Naive/TopDown\_method-forecast\_proportions | | -- | ------- | -------------------------------- | ------------------ | -------------- | ----------------------------------------------- | ------------------------------------------- | | 48 | Overall | coherence\_residual\_mae\_before | 91.123692 | 0.0 | 9.112369e+01 | 0.0 | | 50 | Overall | coherence\_residual\_mae\_after | 0.000000 | 0.0 | 2.119653e-13 | 0.0 | | 60 | Overall | is\_coherent | 1.000000 | 1.0 | 1.000000e+00 | 1.0 | | 61 | Overall | coherence\_max\_violation | 0.000000 | 0.0 | 2.910383e-11 | 0.0 | We can also see which levels required the largest adjustments: ```python theme={null} # Compare adjustment magnitude across levels adjustment_by_level = hrec_diag.diagnostics.query("metric == 'adjustment_mae'") adjustment_by_level ``` | | level | metric | AutoARIMA/BottomUp | Naive/BottomUp | AutoARIMA/TopDown\_method-forecast\_proportions | Naive/TopDown\_method-forecast\_proportions | | -- | --------------------------------- | --------------- | ------------------ | -------------- | ----------------------------------------------- | ------------------------------------------- | | 4 | Country | adjustment\_mae | 1551.154858 | 0.0 | 0.000000 | 0.0 | | 16 | Country/Purpose | adjustment\_mae | 996.859118 | 0.0 | 1106.796143 | 0.0 | | 28 | Country/Purpose/State | adjustment\_mae | 91.836329 | 0.0 | 151.248239 | 0.0 | | 40 | Country/Purpose/State/CityNonCity | adjustment\_mae | 0.000000 | 0.0 | 87.497279 | 0.0 | | 52 | Overall | adjustment\_mae | 91.123692 | 0.0 | 152.381830 | 0.0 | ## 5. Evaluation The `HierarchicalForecast` package includes the `evaluate` function to evaluate the different hierarchies and we can use utilsforecast to compute the mean absolute error relative to a baseline model. ```python theme={null} from hierarchicalforecast.evaluation import evaluate from utilsforecast.losses import mse ``` ```python theme={null} df = Y_rec_df.merge(Y_test_df, on=['unique_id', 'ds']) evaluation = evaluate(df = df, tags = tags, train_df = Y_train_df, metrics = [mse], benchmark="Naive") evaluation.set_index(["level", "metric"]).filter(like="ARIMA", axis=1) ``` | | | AutoARIMA | AutoARIMA/BottomUp | AutoARIMA/TopDown\_method-forecast\_proportions | AutoARIMA/TopDown\_method-proportion\_averages | AutoARIMA/MiddleOut\_middle\_level-Country/Purpose/State\_top\_down\_method-proportion\_averages | | --------------------------------- | ---------- | --------- | ------------------ | ----------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------ | | level | metric | | | | | | | Country | mse-scaled | 0.123161 | 0.055264 | 0.123161 | 0.123161 | 0.079278 | | Country/Purpose | mse-scaled | 0.171063 | 0.077688 | 0.101570 | 0.128151 | 0.104186 | | Country/Purpose/State | mse-scaled | 0.194383 | 0.149163 | 0.201738 | 0.327854 | 0.194383 | | Country/Purpose/State/CityNonCity | mse-scaled | 0.170373 | 0.170373 | 0.210060 | 0.341365 | 0.225656 | | Overall | mse-scaled | 0.154912 | 0.085342 | 0.131308 | 0.168269 | 0.115569 | ### References * [Orcutt, G.H., Watts, H.W., & Edwards, J.B.(1968). Data aggregation and information loss. The American Economic Review, 58 , 773(787)](http://www.jstor.org/stable/1815532). * [Disaggregation methods to expedite product line forecasting. Journal of Forecasting, 9 , 233–254. doi:10.1002/for.3980090304](https://onlinelibrary.wiley.com/doi/abs/10.1002/for.3980090304).
* [An investigation of aggregate variable time series forecast strategies with specific subaggregate time series statistical correlation. Computers and Operations Research, 26 , 1133–1149. doi:10.1016/S0305-0548(99)00017-9](https://doi.org/10.1016/S0305-0548\(99\)00017-9). * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) # Quick Start (Polars) Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/tourismsmallpolars.html > Minimal Example of Hierarchical Reconciliation using Polars Large collections of time series organized into structures at different aggregation levels often require their forecasts to follow their aggregation constraints, which poses the challenge of creating novel algorithms capable of coherent forecasts. The `HierarchicalForecast` package provides a wide collection of Python implementations of hierarchical forecasting algorithms that follow classic hierarchical reconciliation. In this notebook we will show how to use the `StatsForecast` library to produce base forecasts, and use `HierarchicalForecast` package to perform hierarchical reconciliation. You can run these experiments using CPU or GPU with Google Colab. Open In Colab ## 1. Libraries ```python theme={null} !pip install hierarchicalforecast statsforecast datasetsforecast ``` ## 2. Load Data In this example we will use the `TourismSmall` dataset. The following cell gets the time series for the different levels in the hierarchy, the summing matrix `S` which recovers the full dataset from the bottom level hierarchy and the indices of each hierarchy denoted by `tags`. ```python theme={null} import numpy as np import polars as pl from datasetsforecast.hierarchical import HierarchicalData, HierarchicalInfo ``` ```python theme={null} group_name = 'TourismSmall' group = HierarchicalInfo.get_group(group_name) Y_df, S_df, tags = HierarchicalData.load('./data', group_name) Y_df = pl.from_pandas(Y_df) S_df = pl.from_pandas(S_df.reset_index(names="unique_id")) Y_df = Y_df.with_columns(pl.col('ds').cast(pl.Date)) ``` ```python theme={null} S_df[:6, :6] ``` | unique\_id | nsw-hol-city | nsw-hol-noncity | vic-hol-city | vic-hol-noncity | qld-hol-city | | ---------- | ------------ | --------------- | ------------ | --------------- | ------------ | | str | f64 | f64 | f64 | f64 | f64 | | "total" | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | | "hol" | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | | "vfr" | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | "bus" | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | "oth" | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | "nsw-hol" | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | ```python theme={null} tags ``` ```text theme={null} {'Country': array(['total'], dtype=object), 'Country/Purpose': array(['hol', 'vfr', 'bus', 'oth'], dtype=object), 'Country/Purpose/State': array(['nsw-hol', 'vic-hol', 'qld-hol', 'sa-hol', 'wa-hol', 'tas-hol', 'nt-hol', 'nsw-vfr', 'vic-vfr', 'qld-vfr', 'sa-vfr', 'wa-vfr', 'tas-vfr', 'nt-vfr', 'nsw-bus', 'vic-bus', 'qld-bus', 'sa-bus', 'wa-bus', 'tas-bus', 'nt-bus', 'nsw-oth', 'vic-oth', 'qld-oth', 'sa-oth', 'wa-oth', 'tas-oth', 'nt-oth'], dtype=object), 'Country/Purpose/State/CityNonCity': array(['nsw-hol-city', 'nsw-hol-noncity', 'vic-hol-city', 'vic-hol-noncity', 'qld-hol-city', 'qld-hol-noncity', 'sa-hol-city', 'sa-hol-noncity', 'wa-hol-city', 'wa-hol-noncity', 'tas-hol-city', 'tas-hol-noncity', 'nt-hol-city', 'nt-hol-noncity', 'nsw-vfr-city', 'nsw-vfr-noncity', 'vic-vfr-city', 'vic-vfr-noncity', 'qld-vfr-city', 'qld-vfr-noncity', 'sa-vfr-city', 'sa-vfr-noncity', 'wa-vfr-city', 'wa-vfr-noncity', 'tas-vfr-city', 'tas-vfr-noncity', 'nt-vfr-city', 'nt-vfr-noncity', 'nsw-bus-city', 'nsw-bus-noncity', 'vic-bus-city', 'vic-bus-noncity', 'qld-bus-city', 'qld-bus-noncity', 'sa-bus-city', 'sa-bus-noncity', 'wa-bus-city', 'wa-bus-noncity', 'tas-bus-city', 'tas-bus-noncity', 'nt-bus-city', 'nt-bus-noncity', 'nsw-oth-city', 'nsw-oth-noncity', 'vic-oth-city', 'vic-oth-noncity', 'qld-oth-city', 'qld-oth-noncity', 'sa-oth-city', 'sa-oth-noncity', 'wa-oth-city', 'wa-oth-noncity', 'tas-oth-city', 'tas-oth-noncity', 'nt-oth-city', 'nt-oth-noncity'], dtype=object)} ``` We split the dataframe in train/test splits. ```python theme={null} Y_test_df = Y_df.group_by('unique_id').tail(group.horizon) Y_train_df = Y_df.filter(pl.col('ds') < Y_test_df['ds'].min()) ``` ## 3. Base forecasts The following cell computes the *base forecast* for each time series using the `auto_arima` and `naive` models. Observe that `Y_hat_df` contains the forecasts but they are not coherent. ```python theme={null} from statsforecast.core import StatsForecast from statsforecast.models import AutoARIMA, Naive ``` ```python theme={null} fcst = StatsForecast( models=[AutoARIMA(season_length=group.seasonality), Naive()], freq="1q", n_jobs=-1 ) Y_hat_df = fcst.forecast(df=Y_train_df, h=group.horizon) ``` ## 4. Hierarchical reconciliation The following cell makes the previous forecasts coherent using the `HierarchicalReconciliation` class. The used methods to make the forecasts coherent are: * `BottomUp`: The reconciliation of the method is a simple addition to the upper levels. * `TopDown`: The second method constrains the base-level predictions to the top-most aggregate-level serie and then distributes it to the disaggregate series through the use of proportions. * `MiddleOut`: Anchors the base predictions in a middle level. ```python theme={null} from hierarchicalforecast.core import HierarchicalReconciliation from hierarchicalforecast.methods import BottomUp, TopDown, MiddleOut ``` ```python theme={null} reconcilers = [ BottomUp(), TopDown(method='forecast_proportions'), MiddleOut(middle_level='Country/Purpose/State', top_down_method='forecast_proportions') ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_train_df, S_df=S_df, tags=tags) ``` ## 5. Evaluation The `HierarchicalForecast` package includes the `evaluate` function to evaluate the different hierarchies and we can use utilsforecast to compute the mean absolute error relative to a baseline model. ```python theme={null} from hierarchicalforecast.evaluation import evaluate from utilsforecast.losses import mse ``` ```python theme={null} df = Y_rec_df.join(Y_test_df, on=['unique_id', 'ds']) evaluation = evaluate(df = df, tags = tags, train_df = Y_train_df, metrics = [mse], benchmark="Naive") evaluation[["level", "metric", "AutoARIMA", "AutoARIMA/BottomUp", "AutoARIMA/TopDown_method-forecast_proportions"]] ``` | level | metric | AutoARIMA | AutoARIMA/BottomUp | AutoARIMA/TopDown\_method-forecast\_proportions | | -------------------------------- | ------------ | --------- | ------------------ | ----------------------------------------------- | | str | str | f64 | f64 | f64 | | "Country" | "mse-scaled" | 0.317897 | 0.226999 | 0.317897 | | "Country/Purpose" | "mse-scaled" | 0.323207 | 0.199359 | 0.251368 | | "Country/Purpose/State" | "mse-scaled" | 0.266118 | 0.305711 | 0.308241 | | "Country/Purpose/State/CityNonC… | "mse-scaled" | 0.305173 | 0.305173 | 0.305913 | | "Overall" | "mse-scaled" | 0.311707 | 0.234934 | 0.289406 | ### References * [Orcutt, G.H., Watts, H.W., & Edwards, J.B.(1968). Data aggregation and information loss. The American Economic Review, 58 , 773(787)](http://www.jstor.org/stable/1815532). * [Disaggregation methods to expedite product line forecasting. Journal of Forecasting, 9 , 233–254. doi:10.1002/for.3980090304](https://onlinelibrary.wiley.com/doi/abs/10.1002/for.3980090304).
* [An investigation of aggregate variable time series forecast strategies with specific subaggregate time series statistical correlation. Computers and Operations Research, 26 , 1133–1149. doi:10.1016/S0305-0548(99)00017-9](https://doi.org/10.1016/S0305-0548\(99\)00017-9). * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) # Hierarchical Forecast 👑 Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/index.html Probabilistic hierarchical forecasting with statistical and econometric methods ## 📚 Intro A vast amount of time series datasets are organized into structures with different levels or hierarchies of aggregation. Examples include cross-sectional aggregations such as categories, brands, or geographical groupings, or temporal aggregations such as weeks, months or years. Coherent forecasts across levels are necessary for consistent decision-making and planning. Hierachical Forecast offers different reconciliation methods that render coherent forecasts across cross-sectional and temporal hierachies. ## 🎊 Features * Classic reconciliation methods: * `BottomUp`: Simple addition to the upper levels. * `TopDown`: Distributes the top levels forecasts trough the hierarchies. * Alternative reconciliation methods: * `MiddleOut`: It anchors the base predictions in a middle level. The levels above the base predictions use the bottom-up approach, while the levels below use a top-down. * `MinTrace`: Minimizes the total forecast variance of the space of coherent forecasts, with the Minimum Trace reconciliation. * `ERM`: Optimizes the reconciliation matrix minimizing an L1 regularized objective. * Probabilistic coherent methods: * `Normality`: Uses MinTrace variance-covariance closed form matrix under a normality assumption. * `Bootstrap`: Generates distribution of hierarchically reconciled predictions using Gamakumara's bootstrap approach. * `PERMBU`: Reconciles independent sample predictions by reinjecting multivariate dependence with estimated rank permutation copulas, and performing a Bottom-Up aggregation. * `Conformal`: Distribution-free prediction intervals using conformal prediction. Provides valid coverage under exchangeability assumptions. * Temporal reconciliation methods: * All reconciliation methods (except for the insample methods) are available to use with temporal hierarchies too. Missing something? Please open an issue here or write us in [![Slack](https://img.shields.io/badge/Slack-4A154B?\&logo=slack\&logoColor=white)](https://join.slack.com/t/nixtlaworkspace/shared_invite/zt-135dssye9-fWTzMpv2WBthq8NK0Yvu6A) ## 📖 Why? **Short**: We want to contribute to the ML field by providing reliable baselines and benchmarks for hierarchical forecasting task in industry and academia. Here's the complete [paper](https://arxiv.org/abs/2207.03517). **Verbose**: `HierarchicalForecast` integrates publicly available processed datasets, evaluation metrics, and a curated set of standard statistical baselines. In this library we provide usage examples and references to extensive experiments where we showcase the baseline's use and evaluate the accuracy of their predictions. With this work, we hope to contribute to Machine Learning forecasting by bridging the gap to statistical and econometric modeling, as well as providing tools for the development of novel hierarchical forecasting algorithms rooted in a thorough comparison of these well-established models. We intend to continue maintaining and increasing the repository, promoting collaboration across the forecasting community. ## 💻 Installation We recommend using `uv` as Python package manager, for which you can find installation instructions [here](https://docs.astral.sh/uv/getting-started/installation/). You can then install `HierarchicalForecast` with: ```python theme={null} uv pip install hierarchicalforecast ``` Alternatively, you can use the Python package index [pip](https://pypi.org) directly: ```python theme={null} pip install hierarchicalforecast ``` ## 🧬 How to use The following example needs `statsforecast` and `datasetsforecast` as additional packages. If not installed, install it via your preferred method, e.g. `pip install statsforecast datasetsforecast`. The `datasetsforecast` library allows us to download hierarhical datasets and we will use `statsforecast` to compute the base forecasts to be reconciled. You can open a complete example in Colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/nixtla/hierarchicalforecast/blob/main/nbs/examples/TourismSmall.ipynb) Minimal Example: ```python theme={null} # !pip install -U numba statsforecast datasetsforecast import numpy as np import pandas as pd #obtain hierarchical dataset from datasetsforecast.hierarchical import HierarchicalData # compute base forecast no coherent from statsforecast.core import StatsForecast from statsforecast.models import AutoARIMA, Naive #obtain hierarchical reconciliation methods and evaluation from hierarchicalforecast.core import HierarchicalReconciliation from hierarchicalforecast.evaluation import evaluate from hierarchicalforecast.methods import BottomUp, TopDown, MiddleOut from utilsforecast.losses import mse # Load TourismSmall dataset Y_df, S_df, tags = HierarchicalData.load('./data', 'TourismSmall') Y_df['ds'] = pd.to_datetime(Y_df['ds']) S_df = S_df.reset_index(names="unique_id") #split train/test sets Y_test_df = Y_df.groupby('unique_id').tail(4) Y_train_df = Y_df.drop(Y_test_df.index) # Compute base auto-ARIMA predictions fcst = StatsForecast(models=[AutoARIMA(season_length=4), Naive()], freq='QE', n_jobs=-1) Y_hat_df = fcst.forecast(df=Y_train_df, h=4) # Reconcile the base predictions reconcilers = [ BottomUp(), TopDown(method='forecast_proportions'), MiddleOut(middle_level='Country/Purpose/State', top_down_method='forecast_proportions') ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df, Y_df=Y_train_df, S_df=S_df, tags=tags) ``` ### Evaluation Assumes you have a test dataframe. ```python theme={null} df = Y_rec_df.merge(Y_test_df, on=['unique_id', 'ds']) evaluation = evaluate(df = df, tags = tags, metrics = [mse], benchmark = "Naive") ``` ## 📖 Documentation Here is a link to the [documentation](https://nixtlaverse.nixtla.io/hierarchicalforecast/index.html). ## 📃 License This project is licensed under the Apache 2.0 License - see the [LICENSE](https://github.com/Nixtla/hierarchicalforecast/blob/main/LICENSE) file for details. ## 🏟 HTS projects In the R ecosystem, we recommend checking out [fable](http://fable.tidyverts.org/), and the now-retired [hts](https://github.com/earowang/hts). In Python we want to acknowledge the following libraries [hiere2e](https://github.com/rshyamsundar/gluonts-hierarchical-ICML-2021), [hierts](https://github.com/elephaint/hierts), [sktime](https://github.com/sktime/sktime-tutorial-pydata-berlin-2022), [darts](https://github.com/unit8co/darts), [pyhts](https://github.com/AngelPone/pyhts), [scikit-hts](https://github.com/carlomazzaferro/scikit-hts). ## 📚 References and Acknowledgements This work is highly influenced by the fantastic work of previous contributors and other scholars who previously proposed the reconciliation methods presented here. We want to highlight the work of Rob Hyndman, George Athanasopoulos, Shanika L. Wickramasuriya, Souhaib Ben Taieb, and Bonsoo Koo. For a full reference link, please visit the Reference section of this [paper](https://arxiv.org/pdf/2207.03517.pdf). We encourage users to explore this [literature review](https://otexts.com/fpp3/hierarchical-reading.html). ## 🙏 How to cite If you enjoy or benefit from using these Python implementations, a citation to this [hierarchical forecasting reference paper](https://arxiv.org/abs/2207.03517) will be greatly appreciated. ```bibtex theme={null} @article{olivares2024hierarchicalforecastreferenceframeworkhierarchical, title={HierarchicalForecast: A Reference Framework for Hierarchical Forecasting in Python}, author={Kin G. Olivares and Azul Garza and David Luo and Cristian Challú and Max Mergenthaler and Souhaib Ben Taieb and Shanika L. Wickramasuriya and Artur Dubrawski}, year={2024}, eprint={2207.03517}, archivePrefix={arXiv}, primaryClass={stat.ML}, url={https://arxiv.org/abs/2207.03517}, } ``` # Reconciliation Methods Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/methods.html In hierarchical forecasting, we aim to create forecasts for many time series concurrently, whilst adhering to pre-specified hierarchical relationships that exist between the time series. We can enforce this coherence by performing a post-processing reconciliation step on the forecasts. The `HierarchicalForecast` package provides the most comprehensive collection of Python implementations of hierarchical forecasting algorithms that follow classic hierarchical reconciliation. All the methods have a `reconcile` function capable of reconciling base forecasts using `numpy` arrays. ## Cross-sectional hierarchies Traditionally, hierarchical forecasting methods reconcile *cross-sectional* aggregations. For example, we may have forecasts for individual product demand, but also for the overall product group, department and store, and we are interested in making sure these forecasts are coherent with each other. This can be formalized as: $\tilde{\textbf{Y}} = SP\hat{\textbf{Y}} \;, $ where $\hat{\textbf{Y}} \in \mathbb{R}^{m \times p}$ denotes the matrix of forecasts for all $m$ time series for all $p$ time steps in the hierarchy, $S \in \lbrace 0, 1 \rbrace^{m \times n}$ is a matrix that defines the hierarchical relationship between the $n$ bottom-level time series and the $m^* = m - n$ aggregations, $P \in \mathbb{R}^{n \times m}$ is a matrix that encapsulates the contribution of each forecast to the final estimate, and $\tilde{\textbf{Y}} \in \mathbb{R}^{m \times p}$ is the matrix of reconciled forecasts. We can use the matrix $P$ to define various forecast contribution scenarios. Cross-sectional reconciliation methods aim to find the optimal $P$ matrix. ## Temporal hierarchies We can also perform *temporal* reconciliation. For example, we may have forecasts for daily demand, weekly, and monthly, and we are interested in making sure these forecasts are coherent with each other. We formalize the temporal hierarchical forecasting problem as: $\tilde{\textbf{Y}} = \left( S_{te} P_{te} \hat{\textbf{Y}}^{\intercal} \right)^{\intercal} \;, $ where $S_{te} \in \lbrace 0, 1 \rbrace^{p \times k}$ is a matrix that defines the hierarchical relationship between the $k$ bottom-level time steps and the $p^* = p - k$ aggregations and $P_{te} \in \mathbb{R}^{k \times p}$ is a matrix that encapsulates the contribution of each forecast to the final estimate. We can use the matrix $P_{te}$ to define various forecast contribution scenarios. Temporal reconciliation methods aim to find the optimal $P_{te}$ matrix. ## Cross-temporal reconciliation We can combine cross-sectional and temporal hierarchical forecasting by performing cross-sectional reconciliation and temporal reconciliation in a two-step procedure. ## References -[Hyndman, Rob. Notation for forecast reconciliation.](https://robjhyndman.com/hyndsight/reconciliation-notation.html) ## 1. Bottom-Up *** ### `BottomUp` ```python theme={null} BottomUp() ``` Bases: [HReconciler](#hierarchicalforecast.methods.HReconciler) Bottom Up Reconciliation Class. The most basic hierarchical reconciliation is performed using an Bottom-Up strategy. It was proposed for the first time by Orcutt in 1968. The corresponding hierarchical "projection" matrix is defined as: ```math theme={null} \mathbf{P}_{\\text{BU}} = [\mathbf{0}_{\mathrm{[b],[a]}}\;|\;\mathbf{I}_{\mathrm{[b][b]}}] ``` References: * [Orcutt, G.H., Watts, H.W., & Edwards, J.B.(1968). "Data aggregation and information loss". The American Economic Review, 58 , 773(787)](http://www.jstor.org/stable/1815532). #### `BottomUp.fit` ```python theme={null} fit(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` Bottom Up Fit Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | In-sample values of size (`base`, `horizon`). Default is None. | None | | `y_hat_insample` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | In-sample forecast values of size (`base`, `horizon`). Default is None. | None | | `sigmah` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | Estimated standard deviation of the conditional marginal distribution. Default is None. | None | | `intervals_method` | [Optional](#Optional)\[[str](#str)] | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. Default is None. | None | | `num_samples` | [Optional](#Optional)\[[int](#int)] | Number of samples for probabilistic coherent distribution. Default is None. | None | | `seed` | [Optional](#Optional)\[[int](#int)] | Seed for reproducibility. Default is None. | None | | `tags` | [Optional](#Optional)\[[dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)]] | Tags for hierarchical structure. Default is None. | None | **Returns:** | Name | Type | Description | | ---------- | ------------------------------ | ------------------ | | `BottomUp` | [object](#object) | fitted reconciler. | #### `BottomUp.predict` ```python theme={null} predict(S, y_hat, level=None) ``` Predict using reconciler. Predict using fitted mean and probabilistic reconcilers. **Parameters:** | Name | Type | Description | Default | | ------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------- | -------------------------- | | `y_tilde` | [dict](#dict) | Reconciliated predictions. | #### `BottomUp.fit_predict` ```python theme={null} fit_predict(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, level=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` BottomUp Reconciliation Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | In-sample values of size (`base`, `insample_size`). Default is None. | None | | `y_hat_insample` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | In-sample forecast values of size (`base`, `insample_size`). Default is None. | None | | `sigmah` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | Estimated standard deviation of the conditional marginal distribution. Default is None. | None | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | | `intervals_method` | [Optional](#Optional)\[[str](#str)] | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. Default is None. | None | | `num_samples` | [Optional](#Optional)\[[int](#int)] | Number of samples for probabilistic coherent distribution. Default is None. | None | | `seed` | [Optional](#Optional)\[[int](#int)] | Seed for reproducibility. Default is None. | None | | `tags` | [Optional](#Optional)\[[dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)]] | Tags for hierarchical structure. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------- | -------------------------------------------------- | | `y_tilde` | [dict](#dict) | Reconciliated y\_hat using the Bottom Up approach. | #### `BottomUp.sample` ```python theme={null} sample(num_samples) ``` Sample probabilistic coherent distribution. Generates n samples from a probabilistic coherent distribution. The method uses fitted mean and probabilistic reconcilers, defined by the `intervals_method` selected during the reconciler's instantiation. Currently available: `normality`, `bootstrap`, `permbu`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`num_series`, `horizon`, `num_samples`). | ### `BottomUpSparse` ```python theme={null} BottomUpSparse() ``` Bases: [BottomUp](#hierarchicalforecast.methods.BottomUp) BottomUpSparse Reconciliation Class. This is the implementation of a Bottom Up reconciliation using the sparse matrix approach. It works much more efficient on datasets with many time series. \[makoren: At least I hope so, I only checked up until \~20k time series, and there's no real improvement, it would be great to check for smth like 1M time series, where the dense S matrix really stops fitting in memory] See the parent class for more details. #### `BottomUpSparse.fit` ```python theme={null} fit(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` Bottom Up Fit Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | In-sample values of size (`base`, `horizon`). Default is None. | None | | `y_hat_insample` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | In-sample forecast values of size (`base`, `horizon`). Default is None. | None | | `sigmah` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | Estimated standard deviation of the conditional marginal distribution. Default is None. | None | | `intervals_method` | [Optional](#Optional)\[[str](#str)] | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. Default is None. | None | | `num_samples` | [Optional](#Optional)\[[int](#int)] | Number of samples for probabilistic coherent distribution. Default is None. | None | | `seed` | [Optional](#Optional)\[[int](#int)] | Seed for reproducibility. Default is None. | None | | `tags` | [Optional](#Optional)\[[dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)]] | Tags for hierarchical structure. Default is None. | None | **Returns:** | Name | Type | Description | | ---------- | ------------------------------ | ------------------ | | `BottomUp` | [object](#object) | fitted reconciler. | #### `BottomUpSparse.predict` ```python theme={null} predict(S, y_hat, level=None) ``` Predict using reconciler. Predict using fitted mean and probabilistic reconcilers. **Parameters:** | Name | Type | Description | Default | | ------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------- | -------------------------- | | `y_tilde` | [dict](#dict) | Reconciliated predictions. | #### `BottomUpSparse.fit_predict` ```python theme={null} fit_predict(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, level=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` BottomUp Reconciliation Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | In-sample values of size (`base`, `insample_size`). Default is None. | None | | `y_hat_insample` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | In-sample forecast values of size (`base`, `insample_size`). Default is None. | None | | `sigmah` | [Optional](#Optional)\[[ndarray](#numpy.ndarray)] | Estimated standard deviation of the conditional marginal distribution. Default is None. | None | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | | `intervals_method` | [Optional](#Optional)\[[str](#str)] | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. Default is None. | None | | `num_samples` | [Optional](#Optional)\[[int](#int)] | Number of samples for probabilistic coherent distribution. Default is None. | None | | `seed` | [Optional](#Optional)\[[int](#int)] | Seed for reproducibility. Default is None. | None | | `tags` | [Optional](#Optional)\[[dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)]] | Tags for hierarchical structure. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------- | -------------------------------------------------- | | `y_tilde` | [dict](#dict) | Reconciliated y\_hat using the Bottom Up approach. | #### `BottomUpSparse.sample` ```python theme={null} sample(num_samples) ``` Sample probabilistic coherent distribution. Generates n samples from a probabilistic coherent distribution. The method uses fitted mean and probabilistic reconcilers, defined by the `intervals_method` selected during the reconciler's instantiation. Currently available: `normality`, `bootstrap`, `permbu`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`num_series`, `horizon`, `num_samples`). | ## 2. Top-Down ### `TopDown` ```python theme={null} TopDown(method) ``` Bases: [HReconciler](#hierarchicalforecast.methods.HReconciler) Top Down Reconciliation Class. The Top Down hierarchical reconciliation method, distributes the total aggregate predictions and decomposes it down the hierarchy using proportions $\mathbf{p}_{\mathrm{[b]}}$ that can be actual historical values or estimated. ```math theme={null} \mathbf{P}=[\mathbf{p}_{\mathrm{[b]}}\;|\;\mathbf{0}_{\mathrm{[b][a,b\;-1]}}] ``` **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------------------------------------------------- | ---------- | | `method` | [str](#str) | One of `forecast_proportions`, `average_proportions` and `proportion_averages`. | *required* | References: * [CW. Gross (1990). "Disaggregation methods to expedite product line forecasting". Journal of Forecasting, 9 , 233-254. doi:10.1002/for.3980090304](https://onlinelibrary.wiley.com/doi/abs/10.1002/for.3980090304). * [G. Fliedner (1999). "An investigation of aggregate variable time series forecast strategies with specific subaggregate time series statistical correlation". Computers and Operations Research, 26 , 1133-1149. doi:10.1016/S0305-0548(99)00017-9](https://doi.org/10.1016/S0305-0548\(99\)00017-9). #### `TopDown.fit` ```python theme={null} fit(S, y_hat, y_insample, y_hat_insample=None, sigmah=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` TopDown Fit Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [ndarray](#numpy.ndarray) | Insample values of size (`base`, `insample_size`). Optional for `forecast_proportions` method. | *required* | | `y_hat_insample` | [ndarray](#numpy.ndarray) | Insample forecast values of size (`base`, `insample_size`). Optional for `forecast_proportions` method. | None | | `sigmah` | [ndarray](#numpy.ndarray) | Estimated standard deviation of the conditional marginal distribution. | None | | `intervals_method` | [str](#str) | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. | None | | `num_samples` | [int](#int) | Number of samples for probabilistic coherent distribution. | None | | `seed` | [int](#int) | Seed for reproducibility. | None | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | Each key is a level and each value its `S` indices. | None | **Returns:** | Name | Type | Description | | --------- | ------------------------------ | ------------------ | | `TopDown` | [object](#object) | fitted reconciler. | #### `TopDown.predict` ```python theme={null} predict(S, y_hat, level=None) ``` Predict using reconciler. Predict using fitted mean and probabilistic reconcilers. **Parameters:** | Name | Type | Description | Default | | ------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------- | -------------------------- | | `y_tilde` | [dict](#dict) | Reconciliated predictions. | #### `TopDown.fit_predict` ```python theme={null} fit_predict(S, y_hat, tags, y_insample=None, y_hat_insample=None, sigmah=None, level=None, intervals_method=None, num_samples=None, seed=None) ``` Top Down Reconciliation Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | Each key is a level and each value its `S` indices. | *required* | | `y_insample` | [ndarray](#numpy.ndarray) | Insample values of size (`base`, `insample_size`). Optional for `forecast_proportions` method. Default is None. | None | | `y_hat_insample` | [ndarray](#numpy.ndarray) | Insample forecast values of size (`base`, `insample_size`). Optional for `forecast_proportions` method. Default is None. | None | | `sigmah` | [ndarray](#numpy.ndarray) | Estimated standard deviation of the conditional marginal distribution. Default is None. | None | | `level` | [list](#list)\[[int](#int)] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | | `intervals_method` | [str](#str) | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. Default is None. | None | | `num_samples` | [int](#int) | Number of samples for probabilistic coherent distribution. Default is None. | None | | `seed` | [int](#int) | Seed for reproducibility. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------- | | `y_tilde` | [ndarray](#numpy.ndarray) | Reconciliated y\_hat using the Top Down approach. | #### `TopDown.sample` ```python theme={null} sample(num_samples) ``` Sample probabilistic coherent distribution. Generates n samples from a probabilistic coherent distribution. The method uses fitted mean and probabilistic reconcilers, defined by the `intervals_method` selected during the reconciler's instantiation. Currently available: `normality`, `bootstrap`, `permbu`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`num_series`, `horizon`, `num_samples`). | ### `TopDownSparse` Bases: [TopDown](#hierarchicalforecast.methods.TopDown) TopDownSparse Reconciliation Class. This is an implementation of top-down reconciliation using the sparse matrix approach. It works much more efficiently on data sets with many time series. See the parent class for more details. #### `TopDownSparse.fit` ```python theme={null} fit(S, y_hat, y_insample, y_hat_insample=None, sigmah=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` TopDown Fit Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [ndarray](#numpy.ndarray) | Insample values of size (`base`, `insample_size`). Optional for `forecast_proportions` method. | *required* | | `y_hat_insample` | [ndarray](#numpy.ndarray) | Insample forecast values of size (`base`, `insample_size`). Optional for `forecast_proportions` method. | None | | `sigmah` | [ndarray](#numpy.ndarray) | Estimated standard deviation of the conditional marginal distribution. | None | | `intervals_method` | [str](#str) | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. | None | | `num_samples` | [int](#int) | Number of samples for probabilistic coherent distribution. | None | | `seed` | [int](#int) | Seed for reproducibility. | None | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | Each key is a level and each value its `S` indices. | None | **Returns:** | Name | Type | Description | | --------- | ------------------------------ | ------------------ | | `TopDown` | [object](#object) | fitted reconciler. | #### `TopDownSparse.predict` ```python theme={null} predict(S, y_hat, level=None) ``` Predict using reconciler. Predict using fitted mean and probabilistic reconcilers. **Parameters:** | Name | Type | Description | Default | | ------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------- | -------------------------- | | `y_tilde` | [dict](#dict) | Reconciliated predictions. | #### `TopDownSparse.fit_predict` ```python theme={null} fit_predict(S, y_hat, tags, y_insample=None, y_hat_insample=None, sigmah=None, level=None, intervals_method=None, num_samples=None, seed=None) ``` #### `TopDownSparse.sample` ```python theme={null} sample(num_samples) ``` Sample probabilistic coherent distribution. Generates n samples from a probabilistic coherent distribution. The method uses fitted mean and probabilistic reconcilers, defined by the `intervals_method` selected during the reconciler's instantiation. Currently available: `normality`, `bootstrap`, `permbu`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`num_series`, `horizon`, `num_samples`). | ```python theme={null} cls_top_down( S=S, y_hat=S @ y_hat_bottom, y_insample=S @ y_bottom, tags=tags )["mean"] ``` ```python theme={null} cls_top_down = TopDownSparse(method="average_proportions") test_fail( cls_top_down, contains="Top-down reconciliation requires strictly hierarchical structures.", args=(sparse.csr_matrix(S_non_hier), None, tags_non_hier), ) ``` ## 3. Middle-Out ### `MiddleOut` ```python theme={null} MiddleOut(middle_level, top_down_method) ``` Bases: [HReconciler](#hierarchicalforecast.methods.HReconciler) Middle Out Reconciliation Class. This method is only available for **strictly hierarchical structures**. It anchors the base predictions in a middle level. The levels above the base predictions use the Bottom-Up approach, while the levels below use a Top-Down. **Parameters:** | Name | Type | Description | Default | | ----------------- | ------------------------ | ------------------------------------------------------------------------------- | ---------- | | `middle_level` | [str](#str) | Middle level. | *required* | | `top_down_method` | [str](#str) | One of `forecast_proportions`, `average_proportions` and `proportion_averages`. | *required* | References: * [Hyndman, R.J., & Athanasopoulos, G. (2021). "Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series". OTexts: Melbourne, Australia. OTexts.com/fpp3. Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) #### `MiddleOut.fit` ```python theme={null} fit(**kwargs) ``` #### `MiddleOut.predict` ```python theme={null} predict(**kwargs) ``` #### `MiddleOut.fit_predict` ```python theme={null} fit_predict(S, y_hat, tags, y_insample=None, y_hat_insample=None, sigmah=None, level=None, intervals_method=None, num_samples=None, seed=None) ``` Middle Out Reconciliation Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | Each key is a level and each value its `S` indices. | *required* | | `y_insample` | [ndarray](#numpy.ndarray) | Insample values of size (`base`, `insample_size`). Only used for `forecast_proportions`. Default is None. | None | | `y_hat_insample` | [ndarray](#numpy.ndarray) | In-sample forecast values of size (`base`, `insample_size`). Only used for `forecast_proportions`. Default is None. | None | | `sigmah` | [ndarray](#numpy.ndarray) | Estimated standard deviation of the conditional marginal distribution. Default is None. | None | | `level` | [list](#list)\[[int](#int)] | Confidence levels for prediction intervals. Default is None. | None | | `intervals_method` | [str](#str) | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. Default is None. | None | | `num_samples` | [int](#int) | Number of samples for probabilistic coherent distribution. Default is None. | None | | `seed` | [int](#int) | Seed for reproducibility. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | ---- | --------------------------------------------------- | | `y_tilde` | | Reconciliated y\_hat using the Middle Out approach. | #### `MiddleOut.sample` ```python theme={null} sample(num_samples) ``` Sample probabilistic coherent distribution. Generates n samples from a probabilistic coherent distribution. The method uses fitted mean and probabilistic reconcilers, defined by the `intervals_method` selected during the reconciler's instantiation. Currently available: `normality`, `bootstrap`, `permbu`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`num_series`, `horizon`, `num_samples`). | ### `MiddleOutSparse` Bases: [MiddleOut](#hierarchicalforecast.methods.MiddleOut) MiddleOutSparse Reconciliation Class. This is an implementation of middle-out reconciliation using the sparse matrix approach. It works much more efficiently on data sets with many time series. See the parent class for more details. #### `MiddleOutSparse.fit` ```python theme={null} fit(**kwargs) ``` #### `MiddleOutSparse.predict` ```python theme={null} predict(**kwargs) ``` #### `MiddleOutSparse.fit_predict` ```python theme={null} fit_predict(S, y_hat, tags, y_insample=None, y_hat_insample=None, sigmah=None, level=None, intervals_method=None, num_samples=None, seed=None) ``` #### `MiddleOutSparse.sample` ```python theme={null} sample(num_samples) ``` Sample probabilistic coherent distribution. Generates n samples from a probabilistic coherent distribution. The method uses fitted mean and probabilistic reconcilers, defined by the `intervals_method` selected during the reconciler's instantiation. Currently available: `normality`, `bootstrap`, `permbu`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`num_series`, `horizon`, `num_samples`). | ## 4. Min-Trace ### `MinTrace` ```python theme={null} MinTrace(method, nonnegative=False, mint_shr_ridge=2e-08, num_threads=1) ``` Bases: [HReconciler](#hierarchicalforecast.methods.HReconciler) MinTrace Reconciliation Class. This reconciliation algorithm proposed by Wickramasuriya et al. depends on a generalized least squares estimator and an estimator of the covariance matrix of the coherency errors $\mathbf{W}_{h}$. The Min Trace algorithm minimizes the squared errors for the coherent forecasts under an unbiasedness assumption; the solution has a closed form. ```math theme={null} \mathbf{P}_{\text{MinT}}=\left(\mathbf{S}^{\intercal}\mathbf{W}_{h}\mathbf{S}\right)^{-1}\mathbf{S}^{\intercal}\mathbf{W}^{-1}_{h} ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `method` | [str](#str) | One of `ols`, `wls_struct`, `wls_var`, `mint_shrink`, `mint_cov`, `emint`. | *required* | | `nonnegative` | [bool](#bool) | Reconciled forecasts should be nonnegative? | False | | `mint_shr_ridge` | [float](#float) | Ridge numeric protection to MinTrace-shr covariance estimator. | 2e-08 | | `num_threads` | [int](#int) | Number of threads for the C++ covariance backend (OpenMP) and for solving the optimization problems (when nonnegative=True). | 1 | References: * [Wickramasuriya, S. L., Athanasopoulos, G., & Hyndman, R. J. (2019). "Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization". Journal of the American Statistical Association, 114 , 804-819. doi:10.1080/01621459.2018.1448825.](https://robjhyndman.com/publications/mint/). * [Wickramasuriya, S.L., Turlach, B.A. & Hyndman, R.J. (2020). "Optimal non-negative forecast reconciliation". Stat Comput 30, 1167-1182. https://doi.org/10.1007/s11222-020-09930-0](https://robjhyndman.com/publications/nnmint/). * [Wickramasuriya, S.L. (2021). Properties of point forecast reconciliation approaches. arXiv:2103.11129](https://arxiv.org/abs/2103.11129). * [Wang, X., Hyndman, R.J., & Wickramasuriya, S.L. (2025). Optimal forecast reconciliation with time series selection. European Journal of Operational Research, 323, 455-470.](https://doi.org/10.1016/j.ejor.2024.12.004) #### `MinTrace.fit` ```python theme={null} fit(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` MinTrace Fit Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------- | | `S` | | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [ndarray](#numpy.ndarray) \| None | Insample values of size (`base`, `insample_size`). Only used with "wls\_var", "mint\_cov", "mint\_shrink". | None | | `y_hat_insample` | [ndarray](#numpy.ndarray) \| None | Insample forecast values of size (`base`, `insample_size`). Only used with "wls\_var", "mint\_cov", "mint\_shrink" | None | | `sigmah` | [ndarray](#numpy.ndarray) \| None | Estimated standard deviation of the conditional marginal distribution. | None | | `intervals_method` | [str](#str) \| None | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. | None | | `num_samples` | [int](#int) \| None | Number of samples for probabilistic coherent distribution. | None | | `seed` | [int](#int) \| None | Seed for reproducibility. | None | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] \| None | Each key is a level and each value its `S` indices. | None | **Returns:** | Name | Type | Description | | ------ | ---- | -------------------------- | | `self` | | object, fitted reconciler. | #### `MinTrace.predict` ```python theme={null} predict(S, y_hat, level=None) ``` Predict using reconciler. Predict using fitted mean and probabilistic reconcilers. **Parameters:** | Name | Type | Description | Default | | ------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------- | -------------------------- | | `y_tilde` | [dict](#dict) | Reconciliated predictions. | #### `MinTrace.fit_predict` ```python theme={null} fit_predict(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, level=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` MinTrace Reconciliation Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [ndarray](#numpy.ndarray) \| None | Insample values of size (`base`, `insample_size`). Only used by `wls_var`, `mint_cov`, `mint_shrink` | None | | `y_hat_insample` | [ndarray](#numpy.ndarray) \| None | Insample fitted values of size (`base`, `insample_size`). Only used by `wls_var`, `mint_cov`, `mint_shrink` | None | | `sigmah` | [ndarray](#numpy.ndarray) \| None | Estimated standard deviation of the conditional marginal distribution. | None | | `level` | [list](#list)\[[int](#int)] \| None | float list 0-100, confidence levels for prediction intervals. | None | | `intervals_method` | [str](#str) \| None | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. | None | | `num_samples` | [int](#int) \| None | Number of samples for probabilistic coherent distribution. | None | | `seed` | [int](#int) \| None | Seed for reproducibility. | None | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] \| None | Each key is a level and each value its `S` indices. | None | **Returns:** | Name | Type | Description | | --------- | ---- | ------------------------------------------------- | | `y_tilde` | | Reconciliated y\_hat using the MinTrace approach. | #### `MinTrace.sample` ```python theme={null} sample(num_samples) ``` Sample probabilistic coherent distribution. Generates n samples from a probabilistic coherent distribution. The method uses fitted mean and probabilistic reconcilers, defined by the `intervals_method` selected during the reconciler's instantiation. Currently available: `normality`, `bootstrap`, `permbu`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`num_series`, `horizon`, `num_samples`). | ### `MinTraceSparse` ```python theme={null} MinTraceSparse(method, nonnegative=False, num_threads=1, qp=True) ``` Bases: [MinTrace](#hierarchicalforecast.methods.MinTrace) MinTraceSparse Reconciliation Class. This is the implementation of OLS and WLS estimators using sparse matrices. It is not guaranteed to give identical results to the non-sparse version, but works much more efficiently on data sets with many time series. See the parent class for more details. **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------- | ---------------------------------------------------------------------------------- | ------------------ | | `method` | [str](#str) | One of `ols`, `wls_struct`, or `wls_var`. | *required* | | `nonnegative` | [bool](#bool) | Return non-negative reconciled forecasts. | False | | `num_threads` | [int](#int) | Number of threads for non-negative quadratic programming calls. | 1 | | `qp` | [bool](#bool) | Implement non-negativity constraint with a quadratic programming approach. Setting | True | #### `MinTraceSparse.fit` ```python theme={null} fit(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` MinTraceSparse Fit Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------- | | `S` | [csr\_matrix](#scipy.sparse.csr_matrix) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [ndarray](#numpy.ndarray) \| None | Insample values of size (`base`, `insample_size`). Only used with "wls\_var". | None | | `y_hat_insample` | [ndarray](#numpy.ndarray) \| None | Insample forecast values of size (`base`, `insample_size`). Only used with "wls\_var" | None | | `sigmah` | [ndarray](#numpy.ndarray) \| None | Estimated standard deviation of the conditional marginal distribution. | None | | `intervals_method` | [str](#str) \| None | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. | None | | `num_samples` | [int](#int) \| None | Number of samples for probabilistic coherent distribution. | None | | `seed` | [int](#int) \| None | Seed for reproducibility. | None | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] \| None | Each key is a level and each value its `S` indices. | None | **Returns:** | Name | Type | Description | | ------ | --------------------------------------------------------------------------- | -------------------------- | | `self` | [MinTraceSparse](#hierarchicalforecast.methods.MinTraceSparse) | object, fitted reconciler. | #### `MinTraceSparse.predict` ```python theme={null} predict(S, y_hat, level=None) ``` Predict using reconciler. Predict using fitted mean and probabilistic reconcilers. **Parameters:** | Name | Type | Description | Default | | ------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------- | -------------------------- | | `y_tilde` | [dict](#dict) | Reconciliated predictions. | #### `MinTraceSparse.fit_predict` ```python theme={null} fit_predict(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, level=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` MinTrace Reconciliation Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [ndarray](#numpy.ndarray) \| None | Insample values of size (`base`, `insample_size`). Only used by `wls_var`, `mint_cov`, `mint_shrink` | None | | `y_hat_insample` | [ndarray](#numpy.ndarray) \| None | Insample fitted values of size (`base`, `insample_size`). Only used by `wls_var`, `mint_cov`, `mint_shrink` | None | | `sigmah` | [ndarray](#numpy.ndarray) \| None | Estimated standard deviation of the conditional marginal distribution. | None | | `level` | [list](#list)\[[int](#int)] \| None | float list 0-100, confidence levels for prediction intervals. | None | | `intervals_method` | [str](#str) \| None | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. | None | | `num_samples` | [int](#int) \| None | Number of samples for probabilistic coherent distribution. | None | | `seed` | [int](#int) \| None | Seed for reproducibility. | None | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] \| None | Each key is a level and each value its `S` indices. | None | **Returns:** | Name | Type | Description | | --------- | ---- | ------------------------------------------------- | | `y_tilde` | | Reconciliated y\_hat using the MinTrace approach. | #### `MinTraceSparse.sample` ```python theme={null} sample(num_samples) ``` Sample probabilistic coherent distribution. Generates n samples from a probabilistic coherent distribution. The method uses fitted mean and probabilistic reconcilers, defined by the `intervals_method` selected during the reconciler's instantiation. Currently available: `normality`, `bootstrap`, `permbu`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`num_series`, `horizon`, `num_samples`). | ## 5. Optimal Combination ### `OptimalCombination` ```python theme={null} OptimalCombination(method, nonnegative=False, num_threads=1) ``` Bases: [MinTrace](#hierarchicalforecast.methods.MinTrace) Optimal Combination Reconciliation Class. This reconciliation algorithm was proposed by Hyndman et al. 2011, the method uses generalized least squares estimator using the coherency errors covariance matrix. Consider the covariance of the base forecast $\textrm{Var}(\epsilon_{h}) = \Sigma_{h}$, the $\mathbf{P}$ matrix of this method is defined by: ```math theme={null} \mathbf{P} = \left(\mathbf{S}^{\intercal}\Sigma_{h}^{\dagger}\mathbf{S}\right)^{-1}\mathbf{S}^{\intercal}\Sigma^{\dagger}_{h} ``` where $\Sigma_{h}^{\dagger}$ denotes the variance pseudo-inverse. The method was later proven equivalent to `MinTrace` variants. **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------- | --------------------------------------------------------------- | ------------------ | | `method` | [str](#str) | str, allowed optimal combination methods: 'ols', 'wls\_struct'. | *required* | | `nonnegative` | [bool](#bool) | bool, reconciled forecasts should be nonnegative? | False |
References * [Rob J. Hyndman, Roman A. Ahmed, George Athanasopoulos, Han Lin Shang (2010). "Optimal Combination Forecasts for Hierarchical Time Series".](https://robjhyndman.com/papers/Hierarchical6.pdf). * [Shanika L. Wickramasuriya, George Athanasopoulos and Rob J. Hyndman (2010). "Optimal Combination Forecasts for Hierarchical Time Series".](https://robjhyndman.com/papers/MinT.pdf). * [Wickramasuriya, S.L., Turlach, B.A. & Hyndman, R.J. (2020). "Optimal non-negative forecast reconciliation". Stat Comput 30, 1167-1182. https://doi.org/10.1007/s11222-020-09930-0](https://robjhyndman.com/publications/nnmint/).
#### `OptimalCombination.fit` ```python theme={null} fit(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` MinTrace Fit Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------- | | `S` | | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [ndarray](#numpy.ndarray) \| None | Insample values of size (`base`, `insample_size`). Only used with "wls\_var", "mint\_cov", "mint\_shrink". | None | | `y_hat_insample` | [ndarray](#numpy.ndarray) \| None | Insample forecast values of size (`base`, `insample_size`). Only used with "wls\_var", "mint\_cov", "mint\_shrink" | None | | `sigmah` | [ndarray](#numpy.ndarray) \| None | Estimated standard deviation of the conditional marginal distribution. | None | | `intervals_method` | [str](#str) \| None | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. | None | | `num_samples` | [int](#int) \| None | Number of samples for probabilistic coherent distribution. | None | | `seed` | [int](#int) \| None | Seed for reproducibility. | None | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] \| None | Each key is a level and each value its `S` indices. | None | **Returns:** | Name | Type | Description | | ------ | ---- | -------------------------- | | `self` | | object, fitted reconciler. | #### `OptimalCombination.predict` ```python theme={null} predict(S, y_hat, level=None) ``` Predict using reconciler. Predict using fitted mean and probabilistic reconcilers. **Parameters:** | Name | Type | Description | Default | | ------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------- | -------------------------- | | `y_tilde` | [dict](#dict) | Reconciliated predictions. | #### `OptimalCombination.fit_predict` ```python theme={null} fit_predict(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, level=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` MinTrace Reconciliation Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [ndarray](#numpy.ndarray) \| None | Insample values of size (`base`, `insample_size`). Only used by `wls_var`, `mint_cov`, `mint_shrink` | None | | `y_hat_insample` | [ndarray](#numpy.ndarray) \| None | Insample fitted values of size (`base`, `insample_size`). Only used by `wls_var`, `mint_cov`, `mint_shrink` | None | | `sigmah` | [ndarray](#numpy.ndarray) \| None | Estimated standard deviation of the conditional marginal distribution. | None | | `level` | [list](#list)\[[int](#int)] \| None | float list 0-100, confidence levels for prediction intervals. | None | | `intervals_method` | [str](#str) \| None | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. | None | | `num_samples` | [int](#int) \| None | Number of samples for probabilistic coherent distribution. | None | | `seed` | [int](#int) \| None | Seed for reproducibility. | None | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] \| None | Each key is a level and each value its `S` indices. | None | **Returns:** | Name | Type | Description | | --------- | ---- | ------------------------------------------------- | | `y_tilde` | | Reconciliated y\_hat using the MinTrace approach. | #### `OptimalCombination.sample` ```python theme={null} sample(num_samples) ``` Sample probabilistic coherent distribution. Generates n samples from a probabilistic coherent distribution. The method uses fitted mean and probabilistic reconcilers, defined by the `intervals_method` selected during the reconciler's instantiation. Currently available: `normality`, `bootstrap`, `permbu`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`num_series`, `horizon`, `num_samples`). | ## 6. Emp. Risk Minimization ### `ERM` ```python theme={null} ERM(method, lambda_reg=0.01) ``` Bases: [HReconciler](#hierarchicalforecast.methods.HReconciler) Empirical Risk Minimization Reconciliation Class. The Empirical Risk Minimization reconciliation strategy relaxes the unbiasedness assumptions from previous reconciliation methods like MinT and optimizes square errors between the reconciled predictions and the validation data to obtain an optimal reconciliation matrix P. The exact solution for $\mathbf{P}$ (`method='closed'`) follows the expression: ```math theme={null} \mathbf{P}^{*} = \left(\mathbf{S}^{\intercal}\mathbf{S}\right)^{-1}\mathbf{Y}^{\intercal}\hat{\mathbf{Y}}\left(\hat{\mathbf{Y}}\hat{\mathbf{Y}}\right)^{-1} ``` The alternative Lasso regularized $\mathbf{P}$ solution (`method='reg_bu'`) is useful when the observations of validation data is limited or the exact solution has low numerical stability. ```math theme={null} \mathbf{P}^{*} = \text{argmin}_{\mathbf{P}} ||\mathbf{Y}-\mathbf{S} \mathbf{P} \hat{Y} ||^{2}_{2} + \lambda ||\mathbf{P}-\mathbf{P}_{\text{BU}}||_{1} ``` **Parameters:** | Name | Type | Description | Default | | ------------ | ---------------------------- | --------------------------------------------- | ----------------- | | `method` | [str](#str) | str, one of `closed`, `reg` and `reg_bu`. | *required* | | `lambda_reg` | [float](#float) | float, l1 regularizer for `reg` and `reg_bu`. | 0.01 |
References * [Ben Taieb, S., & Koo, B. (2019). Regularized regression for hierarchical forecasting without unbiasedness conditions. In Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining KDD '19 (p. 1337-1347). New York, NY, USA: Association for Computing Machinery.](https://doi.org/10.1145/3292500.3330976).
#### `ERM.fit` ```python theme={null} fit(S, y_hat, y_insample, y_hat_insample, sigmah=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` ERM Fit Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------- | | `S` | | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | | Train values of size (`base`, `insample_size`). | *required* | | `y_hat_insample` | | Insample train predictions of size (`base`, `insample_size`). | *required* | | `sigmah` | [ndarray](#numpy.ndarray) \| None | Estimated standard deviation of the conditional marginal distribution. | None | | `intervals_method` | [str](#str) \| None | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. | None | | `num_samples` | [int](#int) \| None | Number of samples for probabilistic coherent distribution. | None | | `seed` | [int](#int) \| None | Seed for reproducibility. | None | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] \| None | Each key is a level and each value its `S` indices. | None | **Returns:** | Name | Type | Description | | ------ | ---- | -------------------------- | | `self` | | object, fitted reconciler. | #### `ERM.predict` ```python theme={null} predict(S, y_hat, level=None) ``` Predict using reconciler. Predict using fitted mean and probabilistic reconcilers. **Parameters:** | Name | Type | Description | Default | | ------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | float list 0-100, confidence levels for prediction intervals. Default is None. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------- | -------------------------- | | `y_tilde` | [dict](#dict) | Reconciliated predictions. | #### `ERM.fit_predict` ```python theme={null} fit_predict(S, y_hat, y_insample=None, y_hat_insample=None, sigmah=None, level=None, intervals_method=None, num_samples=None, seed=None, tags=None) ``` ERM Reconciliation Method. **Parameters:** | Name | Type | Description | Default | | ------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Forecast values of size (`base`, `horizon`). | *required* | | `y_insample` | [ndarray](#numpy.ndarray) \| None | Train values of size (`base`, `insample_size`). | None | | `y_hat_insample` | [ndarray](#numpy.ndarray) \| None | Insample train predictions of size (`base`, `insample_size`). | None | | `sigmah` | [ndarray](#numpy.ndarray) \| None | Estimated standard deviation of the conditional marginal distribution. | None | | `level` | [list](#list)\[[int](#int)] \| None | float list 0-100, confidence levels for prediction intervals. | None | | `intervals_method` | [str](#str) \| None | Sampler for prediction intervals, one of `normality`, `bootstrap`, `permbu`, `conformal`. | None | | `num_samples` | [int](#int) \| None | Number of samples for probabilistic coherent distribution. | None | | `seed` | [int](#int) \| None | Seed for reproducibility. | None | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] \| None | Each key is a level and each value its `S` indices. | None | **Returns:** | Name | Type | Description | | --------- | ---- | -------------------------------------------- | | `y_tilde` | | Reconciliated y\_hat using the ERM approach. | #### `ERM.sample` ```python theme={null} sample(num_samples) ``` Sample probabilistic coherent distribution. Generates n samples from a probabilistic coherent distribution. The method uses fitted mean and probabilistic reconcilers, defined by the `intervals_method` selected during the reconciler's instantiation. Currently available: `normality`, `bootstrap`, `permbu`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`num_series`, `horizon`, `num_samples`). | ## References ### General Reconciliation * [Orcutt, G.H., Watts, H.W., & Edwards, J.B.(1968). Data aggregation and information loss. The American Economic Review, 58 , 773(787).](http://www.jstor.org/stable/1815532) * [Disaggregation methods to expedite product line forecasting. Journal of Forecasting, 9 , 233–254. doi:10.1002/for.3980090304](https://onlinelibrary.wiley.com/doi/abs/10.1002/for.3980090304). * [An investigation of aggregate variable time series forecast strategies with specific subaggregate time series statistical correlation. Computers and Operations Research, 26 , 1133–1149. doi:10.1016/S0305-0548(99)00017-9.](https://doi.org/10.1016/S0305-0548\(99\)00017-9) * [Hyndman, R.J., & Athanasopoulos, G. (2021). “Forecasting: principles and practice, 3rd edition: Chapter 11: Forecasting hierarchical and grouped series.”. OTexts: Melbourne, Australia. OTexts.com/fpp3 Accessed on July 2022.](https://otexts.com/fpp3/hierarchical.html) * [Rob J. Hyndman, Roman A. Ahmed, George Athanasopoulos, Han Lin Shang. “Optimal Combination Forecasts for Hierarchical Time Series” (2010).](https://robjhyndman.com/papers/Hierarchical6.pdf) * [Shanika L. Wickramasuriya, George Athanasopoulos and Rob J. Hyndman. “Optimal Combination Forecasts for Hierarchical Time Series” (2010).](https://robjhyndman.com/papers/MinT.pdf) * [Ben Taieb, S., & Koo, B. (2019). Regularized regression for hierarchical forecasting without unbiasedness conditions. In Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining KDD ’19 (p. 1337-1347). New York, NY, USA: Association for Computing Machinery.](https://doi.org/10.1145/3292500.3330976) ### Hierarchical Probabilistic Coherent Predictions * [Puwasala Gamakumara Ph. D. dissertation. Monash University, Econometrics and Business Statistics. “Probabilistic Forecast Reconciliation”.](https://bridges.monash.edu/articles/thesis/Probabilistic_Forecast_Reconciliation_Theory_and_Applications/11869533) * [Taieb, Souhaib Ben and Taylor, James W and Hyndman, Rob J. (2017). Coherent probabilistic forecasts for hierarchical time series. International conference on machine learning ICML.](https://proceedings.mlr.press/v70/taieb17a.html) # Probabilistic Methods Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/probabilistic_methods.html Here we provide a collection of methods designed to provide hierarchically coherent probabilistic distributions, which means that they generate samples of multivariate time series with hierarchical linear constraints. We designed these methods to extend the `core.HierarchicalForecast` capabilities class. Check their [usage example here](https://nixtlaverse.nixtla.io/hierarchicalforecast/examples/introduction.html). ## 1. Normality ### `Normality` ```python theme={null} Normality(S, P, y_hat, sigmah, W=None, seed=0, covariance_type='diagonal', residuals=None, shrinkage_ridge=_DEFAULT_SHRINKAGE_RIDGE) ``` Normality Probabilistic Reconciliation Class. The Normality method leverages the Gaussian Distribution linearity, to generate hierarchically coherent prediction distributions. This class is meant to be used as the `sampler` input as other `HierarchicalForecast` [reconciliation classes](./methods.html). Given base forecasts under a normal distribution: ```math theme={null} \hat{y}_{h} \sim \mathrm{N}(\hat{\boldsymbol{\mu}}, \hat{\mathbf{W}}_{h}) ``` The reconciled forecasts are also normally distributed: ```math theme={null} \tilde{y}_{h} \sim \mathrm{N}(\mathbf{S}\mathbf{P}\hat{\boldsymbol{\mu}}, \mathbf{S}\mathbf{P}\hat{\mathbf{W}}_{h} \mathbf{P}^{\intercal} \mathbf{S}^{\intercal}) ``` **Parameters:** | Name | Type | Description | Default | | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `S` | [Union](#Union)\[[ndarray](#numpy.ndarray), [spmatrix](#scipy.sparse.spmatrix)] | Summing matrix of size (`base`, `bottom`). | *required* | | `P` | [Union](#Union)\[[ndarray](#numpy.ndarray), [spmatrix](#scipy.sparse.spmatrix)] | Reconciliation matrix of size (`bottom`, `base`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Point forecasts values of size (`base`, `horizon`). | *required* | | `sigmah` | [ndarray](#numpy.ndarray) | Forecast standard dev. of size (`base`, `horizon`). | *required* | | `W` | [Union](#Union)\[[ndarray](#numpy.ndarray), [spmatrix](#scipy.sparse.spmatrix)] | Hierarchical covariance matrix of size (`base`, `base`). Required when `covariance_type='diagonal'` (default). **Ignored** when `covariance_type` is `'full'` or `'shrink'` (covariance is computed from residuals instead). Default is None. | None | | `seed` | [int](#int) | Random seed for numpy generator's replicability. Default is 0. | 0 | | `covariance_type` | [Union](#Union)\[[str](#str), [CovarianceType](#hierarchicalforecast.probabilistic_methods.CovarianceType)] | Type of covariance estimator. Can be a string or CovarianceType enum. Options are:
- `'diagonal'` / `CovarianceType.DIAGONAL`: Uses the W matrix diagonal with correlation scaling (default, backward compatible). W is required. - `'full'` / `CovarianceType.FULL`: Uses full empirical covariance from residuals. W is ignored. Warning: may be non-positive-definite if n\_series > n\_observations. - `'shrink'` / `CovarianceType.SHRINK`: Uses Schäfer-Strimmer shrinkage estimator. W is ignored. Recommended for numerical stability with many series.
Default is `'diagonal'`. | 'diagonal' | | `residuals` | [ndarray](#numpy.ndarray) | Insample residuals of size (`base`, `obs`). Required when `covariance_type` is `'full'` or `'shrink'`. Default is None. | None | | `shrinkage_ridge` | [float](#float) | Ridge parameter for shrinkage covariance estimator. Only used when `covariance_type='shrink'`. A warning is issued if provided with other covariance types. Default is 2e-8. | [\_DEFAULT\_SHRINKAGE\_RIDGE](#hierarchicalforecast.probabilistic_methods.Normality._DEFAULT_SHRINKAGE_RIDGE) | **Raises:** | Type | Description | | -------------------------------------- | ----------------------------------------------------------------------- | | [ValueError](#ValueError) | If `covariance_type` is invalid. | | [ValueError](#ValueError) | If `covariance_type='diagonal'` and `W` is None. | | [ValueError](#ValueError) | If `covariance_type` is `'full'` or `'shrink'` and `residuals` is None. | | [ValueError](#ValueError) | If `residuals` shape doesn't match expected (`base`, `obs`). | | [ValueError](#ValueError) | If `residuals` has fewer than 2 observations. | | [ValueError](#ValueError) | If `residuals` is empty. | | [ValueError](#ValueError) | If any series in `residuals` has all NaN values. | **Warns:** | Type | Description | | ------------------------ | -------------------------------------------------------------------------------- | | UserWarning | If `shrinkage_ridge` is provided but `covariance_type` is not `'shrink'`. | | UserWarning | If `W` is provided but `covariance_type` is not `'diagonal'` (W is ignored). | | UserWarning | If any series has zero or near-zero variance (may affect correlation estimates). | | UserWarning | If `covariance_type='full'` and n\_series > n\_observations (non-PSD risk). |
References * [Panagiotelis A., Gamakumara P. Athanasopoulos G., and Hyndman R. J. (2022). "Probabilistic forecast reconciliation: Properties, evaluation and score optimisation". European Journal of Operational Research.](https://www.sciencedirect.com/science/article/pii/S0377221722006087) * [Schäfer, Juliane, and Korbinian Strimmer. "A Shrinkage Approach to Large-Scale Covariance Matrix Estimation". Statistical Applications in Genetics and Molecular Biology 4, no. 1 (2005).](https://doi.org/10.2202/1544-6115.1175)
**Examples:** ```pycon theme={null} >>> # Using diagonal covariance (default, backward compatible) >>> normality = Normality(S=S, P=P, y_hat=y_hat, sigmah=sigmah, W=W) >>> samples = normality.get_samples(num_samples=100) ``` ```pycon theme={null} >>> # Using full empirical covariance from residuals >>> normality = Normality( ... S=S, P=P, y_hat=y_hat, sigmah=sigmah, ... covariance_type="full", residuals=residuals ... ) ``` ```pycon theme={null} >>> # Using shrinkage covariance (recommended for stability) >>> normality = Normality( ... S=S, P=P, y_hat=y_hat, sigmah=sigmah, ... covariance_type=CovarianceType.SHRINK, residuals=residuals ... ) ``` #### `Normality.get_samples` ```python theme={null} get_samples(num_samples) ``` Normality Coherent Samples. Obtains coherent samples under the Normality assumptions. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`base`, `horizon`, `num_samples`). | ## 2. Bootstrap ### `Bootstrap` ```python theme={null} Bootstrap(S, P, y_hat, y_insample, y_hat_insample, num_samples=100, seed=0, W=None) ``` Bootstrap Probabilistic Reconciliation Class. This method goes beyond the normality assumption for the base forecasts, the technique simulates future sample paths and uses them to generate base sample paths that are latered reconciled. This clever idea and its simplicity allows to generate coherent bootstraped prediction intervals for any reconciliation strategy. This class is meant to be used as the `sampler` input as other `HierarchicalForecast` [reconciliation classes](./methods.html). Given a boostraped set of simulated sample paths: ```math theme={null} \hat{\mathbf{y}}^{[1]}_{\\tau}, \dots ,\hat{\mathbf{y}}^{[B]}_{\\tau}) ``` The reconciled sample paths allow for reconciled distributional forecasts: ```math theme={null} (\mathbf{S}\mathbf{P}\hat{\mathbf{y}}^{[1]}_{\\tau}, \dots ,\mathbf{S}\mathbf{P}\hat{\mathbf{y}}^{[B]}_{\\tau}) ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------- | ---------------- | | `S` | [ndarray](#numpy.ndarray) \| [spmatrix](#scipy.sparse.spmatrix) | np.array, summing matrix of size (`base`, `bottom`). | *required* | | `P` | [ndarray](#numpy.ndarray) \| [spmatrix](#scipy.sparse.spmatrix) | np.array, reconciliation matrix of size (`bottom`, `base`). | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Point forecasts values of size (`base`, `horizon`). | *required* | | `y_insample` | [ndarray](#numpy.ndarray) | Insample values of size (`base`, `insample_size`). | *required* | | `y_hat_insample` | [ndarray](#numpy.ndarray) | Insample point forecasts of size (`base`, `insample_size`). | *required* | | `num_samples` | [int](#int) | int, number of bootstraped samples generated. | 100 | | `seed` | [int](#int) | int, random seed for numpy generator's replicability. | 0 |
References * [Puwasala Gamakumara Ph. D. dissertation. Monash University, Econometrics and Business Statistics (2020). "Probabilistic Forecast Reconciliation"](https://bridges.monash.edu/articles/thesis/Probabilistic_Forecast_Reconciliation_Theory_and_Applications/11869533) * [Panagiotelis A., Gamakumara P. Athanasopoulos G., and Hyndman R. J. (2022). "Probabilistic forecast reconciliation: Properties, evaluation and score optimisation". European Journal of Operational Research.](https://www.sciencedirect.com/science/article/pii/S0377221722006087)
#### `Bootstrap.get_samples` ```python theme={null} get_samples(num_samples) ``` Bootstrap Sample Reconciliation Method. Applies Bootstrap sample reconciliation method as defined by Gamakumara 2020. Generating independent sample paths and reconciling them with Bootstrap. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------------ | ---------- | | `num_samples` | [int](#int) | int, number of samples generated from coherent distribution. | *required* | **Returns:** | Name | Type | Description | | --------- | ---- | ------------------------------------------------------------ | | `samples` | | Coherent samples of size (`base`, `horizon`, `num_samples`). | ## 3. PERMBU ### `PERMBU` ```python theme={null} PERMBU(S, tags, y_hat, y_insample, y_hat_insample, sigmah, num_samples=None, seed=0, P=None) ``` PERMBU Probabilistic Reconciliation Class. The PERMBU method leverages empirical bottom-level marginal distributions with empirical copula functions (describing bottom-level dependencies) to generate the distribution of aggregate-level distributions using BottomUp reconciliation. The sample reordering technique in the PERMBU method reinjects multivariate dependencies into independent bottom-level samples. ```math theme={null} residuals = \hat{\epsilon}_{i,t} ``` Algorithm: 1. For all series compute conditional marginals distributions. 2. Compute `residuals` and obtain rank permutations. 3. Obtain K-sample from the bottom-level series predictions. 4. Apply recursively through the hierarchical structure: 1. For a given aggregate series $i$ and its children series: 2. Obtain children's empirical joint using sample reordering copula. 3. From the children's joint obtain the aggregate series's samples. **Parameters:** | Name | Type | Description | Default | | ---------------- | ------------------------------------------------------------------- | -------------------------------------------------------------- | ----------------- | | `S` | [array](#numpy.array) | summing matrix of size (`base`, `bottom`). | *required* | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | Each key is a level and each value its `S` indices. | *required* | | `y_insample` | [array](#numpy.array) | Insample values of size (`base`, `insample_size`). | *required* | | `y_hat_insample` | [array](#numpy.array) | Insample point forecasts of size (`base`, `insample_size`). | *required* | | `sigmah` | [array](#numpy.array) | forecast standard dev. of size (`base`, `horizon`). | *required* | | `num_samples` | [int](#int) | number of normal prediction samples generated. Default is None | None | | `seed` | [int](#int) | random seed for numpy generator's replicability. Default is 0. | 0 |
References * [Taieb, Souhaib Ben and Taylor, James W and Hyndman, Rob J. (2017). "Coherent probabilistic forecasts for hierarchical time series. International conference on machine learning ICML."](https://proceedings.mlr.press/v70/taieb17a.html)
#### `PERMBU.get_samples` ```python theme={null} get_samples(num_samples=None) ``` PERMBU Sample Reconciliation Method. Applies PERMBU reconciliation method as defined by Taieb et. al 2017. Generating independent base prediction samples, restoring its multivariate dependence using estimated copula with reordering and applying the BottomUp aggregation to the new samples. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ----------------- | | `num_samples` | [int](#int) | number of samples generated from coherent distribution. | None | **Returns:** | Name | Type | Description | | --------- | -------------------------------------- | ------------------------------------------------------------ | | `samples` | [ndarray](#numpy.ndarray) | Coherent samples of size (`base`, `horizon`, `num_samples`). | ## References * [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, Reconciled distributional forecasts”.](https://otexts.com/fpp3/rec-prob.html) * [Puwasala Gamakumara Ph. D. dissertation. Monash University, Econometrics and Business Statistics (2020). “Probabilistic Forecast Reconciliation”](https://bridges.monash.edu/articles/thesis/Probabilistic_Forecast_Reconciliation_Theory_and_Applications/11869533) * [Panagiotelis A., Gamakumara P. Athanasopoulos G., and Hyndman R. J. (2022). “Probabilistic forecast reconciliation: Properties, evaluation and score optimisation”. European Journal of Operational Research.](https://www.sciencedirect.com/science/article/pii/S0377221722006087) * [Taieb, Souhaib Ben and Taylor, James W and Hyndman, Rob J. (2017). Coherent probabilistic forecasts for hierarchical time series. International conference on machine learning ICML.](https://proceedings.mlr.press/v70/taieb17a.html) # Aggregation/Visualization Utils Source: https://nixtlaverse.nixtla.io/hierarchicalforecast/utils.html The `HierarchicalForecast` package contains utility functions to wrangle and visualize hierarchical series datasets. The [`aggregate`](https://nixtlaverse.nixtla.io/hierarchicalforecast/utils.html#aggregate) function of the module allows you to create a hierarchy from categorical variables representing the structure levels, returning also the aggregation contraints matrix $\mathbf{S}$. In addition, `HierarchicalForecast` ensures compatibility of its reconciliation methods with other popular machine-learning libraries via its external forecast adapters that transform output base forecasts from external libraries into a compatible data frame format. ## Aggregate Function ### `aggregate` ```python theme={null} aggregate(df, spec, exog_vars=None, sparse_s=False, id_col='unique_id', time_col='ds', id_time_col=None, target_cols=('y',)) ``` Utils Aggregation Function. Aggregates bottom level series contained in the DataFrame `df` according to levels defined in the `spec` list. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | [Frame](#narwhals.typing.Frame) | Dataframe with columns `[time_col, *target_cols]`, columns to aggregate and optionally exog\_vars. | *required* | | `spec` | [list](#list)\[[list](#list)\[[str](#str)]] | list of levels. Each element of the list should contain a list of columns of `df` to aggregate. | *required* | | `exog_vars` | [Optional](#Optional)\[[dict](#dict)\[[str](#str), [Union](#Union)\[[str](#str), [list](#list)\[[str](#str)]]]] | dictionary of string keys & values that can either be a list of strings or a single string keys correspond to column names and the values represent the aggregation(s) that will be applied to each column. Accepted values are those from Pandas or Polars aggregation Functions, check the respective docs for guidance. Default is None. | None | | `sparse_s` | [bool](#bool) | Return `S_df` as an `SMatrix` (sparse summing matrix wrapper) instead of a dense DataFrame. Works with both Pandas and Polars inputs. Default is False. | False | | `id_col` | [str](#str) | Column that will identify each serie after aggregation. Default is "unique\_id". | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Default is "ds". | 'ds' | | `id_time_col` | [Optional](#Optional)\[[str](#str)] | Column that will identify each timestep after temporal aggregation. If provided, aggregate will operate temporally. Default is None. | None | | `target_cols` | [Sequence](#collections.abc.Sequence)\[[str](#str)] | list of columns that contains the targets to aggregate. Default is ("y",). | ('y',) | **Returns:** | Type | Description | | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [tuple](#tuple)\[[FrameT](#narwhals.typing.FrameT), [FrameT](#narwhals.typing.FrameT) \| [SMatrix](#hierarchicalforecast.utils.SMatrix), [dict](#dict)] | tuple\[FrameT, FrameT | SMatrix, dict]: Y\_df, S\_df, tags Y\_df: Hierarchically structured series. S\_df: Summing dataframe. When `sparse_s=True`, returns an :class:`SMatrix` instead of a DataFrame. tags: Aggregation indices. | ### `aggregate_temporal` ```python theme={null} aggregate_temporal(df, spec, exog_vars=None, sparse_s=False, id_col='unique_id', time_col='ds', id_time_col='temporal_id', target_cols=('y',), aggregation_type='local') ``` Utils Aggregation Function for Temporal aggregations. Aggregates bottom level timesteps contained in the DataFrame `df` according to temporal levels defined in the `spec` list. **Parameters:** | Name | Type | Description | Default | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | `df` | [Frame](#narwhals.typing.Frame) | Dataframe with columns `[time_col, target_cols]` and columns to aggregate. | *required* | | `spec` | [dict](#dict)\[[str](#str), [int](#int)] | Dictionary of temporal levels. Each key should be a string with the value representing the number of bottom-level timesteps contained in the aggregation. | *required* | | `exog_vars` | [Optional](#Optional)\[[dict](#dict)\[[str](#str), [Union](#Union)\[[str](#str), [list](#list)\[[str](#str)]]]] | dictionary of string keys & values that can either be a list of strings or a single string keys correspond to column names and the values represent the aggregation(s) that will be applied to each column. Accepted values are those from Pandas or Polars aggregation Functions, check the respective docs for guidance. Default is None. | None | | `sparse_s` | [bool](#bool) | Return `S_df` as an `SMatrix` (sparse summing matrix wrapper) instead of a dense DataFrame. Works with both Pandas and Polars inputs. Default is False. | False | | `id_col` | [str](#str) | Column that will identify each serie after aggregation. Default is 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Default is 'ds'. | 'ds' | | `id_time_col` | [str](#str) | Column that will identify each timestep after aggregation. Default is 'temporal\_id'. | 'temporal\_id' | | `target_cols` | [Sequence](#collections.abc.Sequence)\[[str](#str)] | List of columns that contain the targets to aggregate. Default is ('y',). | ('y',) | | `aggregation_type` | [str](#str) | If 'local' the aggregation will be performed on the timestamps of each timeseries independently. If 'global' the aggregation will be performed on the unique timestamps of all timeseries. Default is 'local'. | 'local' | **Returns:** | Type | Description | | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [tuple](#tuple)\[[FrameT](#narwhals.typing.FrameT), [FrameT](#narwhals.typing.FrameT), [dict](#dict)] | tuple\[FrameT, FrameT, dict]: Y\_df, S\_df, tags Y\_df: Temporally hierarchically structured series. S\_df: Temporal summing dataframe. tags: Temporal aggregation indices. | ### `make_future_dataframe` ```python theme={null} make_future_dataframe(df, freq, h, id_col='unique_id', time_col='ds') ``` Create future dataframe for forecasting. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | [Frame](#narwhals.typing.Frame) | Dataframe with ids, times and values for the exogenous regressors. | *required* | | `freq` | [Union](#Union)\[[str](#str), [int](#int)] | Frequency of the data. Must be a valid pandas or polars offset alias, or an integer. | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Default is 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Default is 'ds'. | 'ds' | **Returns:** | Name | Type | Description | | -------- | ---------------------------------------------- | ----------------------------- | | `FrameT` | [FrameT](#narwhals.typing.FrameT) | DataFrame with future values. | ### `get_cross_temporal_tags` ```python theme={null} get_cross_temporal_tags(df, tags_cs, tags_te, sep='//', id_col='unique_id', id_time_col='temporal_id', cross_temporal_id_col='cross_temporal_id') ``` Get cross-temporal tags. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------- | | `df` | [Frame](#narwhals.typing.Frame) | DataFrame with temporal ids. | *required* | | `tags_cs` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | Tags for the cross-sectional hierarchies. | *required* | | `tags_te` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | Tags for the temporal hierarchies. | *required* | | `sep` | [str](#str) | Separator for the cross-temporal tags. Default is "//". | '//' | | `id_col` | [str](#str) | Column that identifies each series. Default is 'unique\_id'. | 'unique\_id' | | `id_time_col` | [str](#str) | Column that identifies each (aggregated) timestep. Default is 'temporal\_id'. | 'temporal\_id' | | `cross_temporal_id_col` | [str](#str) | Column that will identify each cross-temporal aggregation. Default is 'cross\_temporal\_id'. | 'cross\_temporal\_id' | **Returns:** | Type | Description | | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | [tuple](#tuple)\[[FrameT](#narwhals.typing.FrameT), [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)]] | tuple\[FrameT, dict\[str, np.ndarray]]: df, tags\_ct df: DataFrame with cross-temporal ids. tags\_ct: Tags for the cross-temporal hierarchies. | ## Hierarchical Visualization ### `HierarchicalPlot` ```python theme={null} HierarchicalPlot(S, tags, S_id_col='unique_id') ``` Hierarchical Plot This class contains a collection of matplotlib visualization methods, suited for small to medium sized hierarchical series. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------- | | `S` | [Frame](#narwhals.typing.Frame) | DataFrame with summing matrix of size `(base, bottom)`, see [aggregate function](./utils.html#aggregate). | *required* | | `tags` | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | hierarchical aggregation indexes, where each key is a level and its value contains tags associated to that level. | *required* | | `S_id_col` | [str](#str) | column that identifies each aggregation. Default is 'unique\_id'. | 'unique\_id' | #### `HierarchicalPlot.plot_summing_matrix` ```python theme={null} plot_summing_matrix() ``` Summation Constraints plot This method simply plots the hierarchical aggregation constraints matrix $\mathbf{S}$. **Returns:** | Name | Type | Description | | ----- | ------------------------------------------------ | -------------------------------------------------------- | | `fig` | [Figure](#matplotlib.figure.Figure) | figure object containing the plot of the summing matrix. | #### `HierarchicalPlot.plot_series` ```python theme={null} plot_series(series, Y_df, models=None, level=None, id_col='unique_id', time_col='ds', target_col='y') ``` Single Series plot **Parameters:** | Name | Type | Description | Default | | ------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- | | `series` | [str](#str) | string identifying the `'unique_id'` any-level series to plot. | *required* | | `Y_df` | [Frame](#narwhals.typing.Frame) | hierarchically structured series ($\mathbf{y}_{[a,b]}$). It contains columns `['unique_id', 'ds', 'y']`, it may have `'models'`. | *required* | | `models` | [Optional](#Optional)\[[list](#list)\[[str](#str)]] | string identifying filtering model columns. Default is None. | None | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | confidence levels for prediction intervals available in `Y_df`. Default is None. | None | | `id_col` | [str](#str) | column that identifies each series. Default is 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | column that identifies each timestep, its values can be timestamps or integers. Default is 'ds'. | 'ds' | | `target_col` | [str](#str) | column that contains the target. Default is 'y'. | 'y' | **Returns:** | Name | Type | Description | | ----- | ------------------------------------------------ | ------------------------------------------------------- | | `fig` | [Figure](#matplotlib.figure.Figure) | figure object containing the plot of the single series. | #### `HierarchicalPlot.plot_hierarchically_linked_series` ```python theme={null} plot_hierarchically_linked_series(bottom_series, Y_df, models=None, level=None, id_col='unique_id', time_col='ds', target_col='y') ``` Hierarchically Linked Series plot **Parameters:** | Name | Type | Description | Default | | --------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `bottom_series` | [str](#str) | string identifying the `'unique_id'` bottom-level series to plot. | *required* | | `Y_df` | [Frame](#narwhals.typing.Frame) | hierarchically structured series ($\mathbf{y}_{[a,b]}$). It contains columns \['unique\_id', 'ds', 'y'] and models. | *required* | | `models` | [Optional](#Optional)\[[list](#list)\[[str](#str)]] | string identifying filtering model columns. Default is None. | None | | `level` | [Optional](#Optional)\[[list](#list)\[[int](#int)]] | confidence levels for prediction intervals available in `Y_df`. Default is None. | None | | `id_col` | [str](#str) | column that identifies each series. Default is 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | column that identifies each timestep, its values can be timestamps or integers. Default is 'ds'. | 'ds' | | `target_col` | [str](#str) | column that contains the target. Default is 'y'. | 'y' | **Returns:** | Name | Type | Description | | ----- | ------------------------------------------------ | ----------------------------------------------------------------------- | | `fig` | [Figure](#matplotlib.figure.Figure) | figure object containing the plots of the hierarchically linked series. | #### `HierarchicalPlot.plot_hierarchical_predictions_gap` ```python theme={null} plot_hierarchical_predictions_gap(Y_df, models=None, xlabel=None, ylabel=None, id_col='unique_id', time_col='ds', target_col='y') ``` Hierarchically Predictions Gap plot **Parameters:** | Name | Type | Description | Default | | ------------ | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `Y_df` | [Frame](#narwhals.typing.Frame) | hierarchically structured series ($\mathbf{y}_{[a,b]}$). It contains columns \['unique\_id', 'ds', 'y'] and models. | *required* | | `models` | [Optional](#Optional)\[[list](#list)\[[str](#str)]] | string identifying filtering model columns. Default is None. | None | | `xlabel` | [Optional](#Optional)\[[str](#str)] | string for the plot's x axis label. Default is None. | None | | `ylabel` | [Optional](#Optional)\[[str](#str)] | string for the plot's y axis label. Default is None. | None | | `id_col` | [str](#str) | column that identifies each series. Default is 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | column that identifies each timestep, its values can be timestamps or integers. Default is 'ds'. | 'ds' | | `target_col` | [str](#str) | column that contains the target. Default is 'y'. | 'y' | **Returns:** | Name | Type | Description | | ----- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | `fig` | [Figure](#matplotlib.figure.Figure) | figure object containing the plot of the aggregated predictions at different levels of the hierarchical structure. | ### Example ```python theme={null} from statsforecast.core import StatsForecast from statsforecast.models import AutoETS from datasetsforecast.hierarchical import HierarchicalData Y_df, S, tags = HierarchicalData.load('./data', 'Labour') Y_df['ds'] = pd.to_datetime(Y_df['ds']) S = S.reset_index(names="unique_id") Y_test_df = Y_df.groupby('unique_id').tail(24) Y_train_df = Y_df.drop(Y_test_df.index) fcst = StatsForecast( models=[AutoETS(season_length=12, model='AAZ')], freq='MS', n_jobs=-1 ) Y_hat_df = fcst.forecast(df=Y_train_df, h=24).reset_index() # Plot prediction difference of different aggregation # Levels Country, Country/Region, Country/Gender/Region ... hplots = HierarchicalPlot(S=S, tags=tags) hplots.plot_hierarchical_predictions_gap( Y_df=Y_hat_df, models='AutoETS', xlabel='Month', ylabel='Predictions', ) ``` # Nixtlaverse Source: https://nixtlaverse.nixtla.io/index The Nixtlaverse is composed of our open-source libraries, designed to provide a comprehensive, cutting-edge toolkit for time series forecasting. The Nixtla ecosystem is primarily built around five main libraries, each specializing in different aspects of time series forecasting: Checkout our end-to-end Agentic AI pipeline! Checkout our Enterprise offering for time series forecasting and anomaly detection! Lightning fast forecasting with statistical and econometric models. Scalable machine learning for time series forecasting. Scalable and user friendly neural forecasting algorithms for time series data. Probabilistic Hierarchical forecasting with statistical and econometric methods. Datasets for time series forecasting. Forecasting utilities for plotting and robust evaluation. Fast implementations of common forecasting routines. # Auto Source: https://nixtlaverse.nixtla.io/mlforecast/auto.html ## ### `AutoRandomForest` ```python theme={null} AutoRandomForest(config=None) ``` Bases: [AutoModel](#mlforecast.auto.AutoModel) Structure to hold a model and its search space **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ---------------------------------------------------------------- | ---------- | | `model` | BaseEstimator | scikit-learn compatible regressor | *required* | | `config` | callable | function that takes an optuna trial and produces a configuration | *required* | ### `AutoElasticNet` ```python theme={null} AutoElasticNet(config=None) ``` Bases: [AutoModel](#mlforecast.auto.AutoModel) Structure to hold a model and its search space **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ---------------------------------------------------------------- | ---------- | | `model` | BaseEstimator | scikit-learn compatible regressor | *required* | | `config` | callable | function that takes an optuna trial and produces a configuration | *required* | ### `AutoLasso` ```python theme={null} AutoLasso(config=None) ``` Bases: [AutoModel](#mlforecast.auto.AutoModel) Structure to hold a model and its search space **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ---------------------------------------------------------------- | ---------- | | `model` | BaseEstimator | scikit-learn compatible regressor | *required* | | `config` | callable | function that takes an optuna trial and produces a configuration | *required* | ### `AutoRidge` ```python theme={null} AutoRidge(config=None) ``` Bases: [AutoModel](#mlforecast.auto.AutoModel) Structure to hold a model and its search space **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ---------------------------------------------------------------- | ---------- | | `model` | BaseEstimator | scikit-learn compatible regressor | *required* | | `config` | callable | function that takes an optuna trial and produces a configuration | *required* | ### `AutoLinearRegression` ```python theme={null} AutoLinearRegression(config=None) ``` Bases: [AutoModel](#mlforecast.auto.AutoModel) Structure to hold a model and its search space **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ---------------------------------------------------------------- | ---------- | | `model` | BaseEstimator | scikit-learn compatible regressor | *required* | | `config` | callable | function that takes an optuna trial and produces a configuration | *required* | ### `AutoCatboost` ```python theme={null} AutoCatboost(config=None) ``` Bases: [AutoModel](#mlforecast.auto.AutoModel) Structure to hold a model and its search space **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ---------------------------------------------------------------- | ---------- | | `model` | BaseEstimator | scikit-learn compatible regressor | *required* | | `config` | callable | function that takes an optuna trial and produces a configuration | *required* | ### `AutoXGBoost` ```python theme={null} AutoXGBoost(config=None) ``` Bases: [AutoModel](#mlforecast.auto.AutoModel) Structure to hold a model and its search space **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ---------------------------------------------------------------- | ---------- | | `model` | BaseEstimator | scikit-learn compatible regressor | *required* | | `config` | callable | function that takes an optuna trial and produces a configuration | *required* | ### `AutoLightGBM` ```python theme={null} AutoLightGBM(config=None) ``` Bases: [AutoModel](#mlforecast.auto.AutoModel) Structure to hold a model and its search space **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ---------------------------------------------------------------- | ---------- | | `model` | BaseEstimator | scikit-learn compatible regressor | *required* | | `config` | callable | function that takes an optuna trial and produces a configuration | *required* | ### `random_forest_space` ```python theme={null} random_forest_space(trial) ``` ### `elastic_net_space` ```python theme={null} elastic_net_space(trial) ``` ### `lasso_space` ```python theme={null} lasso_space(trial) ``` ### `ridge_space` ```python theme={null} ridge_space(trial) ``` ### `linear_regression_space` ```python theme={null} linear_regression_space(trial) ``` ### `catboost_space` ```python theme={null} catboost_space(trial) ``` ### `xgboost_space` ```python theme={null} xgboost_space(trial) ``` ### `lightgbm_space` ```python theme={null} lightgbm_space(trial) ``` ### `AutoModel` ```python theme={null} AutoModel(model, config) ``` Structure to hold a model and its search space **Parameters:** | Name | Type | Description | Default | | -------- | --------------------------------------------------------- | ---------------------------------------------------------------- | ---------- | | `model` | [BaseEstimator](#sklearn.base.BaseEstimator) | scikit-learn compatible regressor | *required* | | `config` | [callable](#callable) | function that takes an optuna trial and produces a configuration | *required* | ### `AutoMLForecast` ```python theme={null} AutoMLForecast(models, freq, season_length=None, init_config=None, fit_config=None, num_threads=1, reuse_cv_splits=False) ``` Hyperparameter optimization helper **Parameters:** | Name | Type | Description | Default | | ----------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `models` | [list](#list) or [dict](#dict) | Auto models to be optimized. | *required* | | `freq` | [str](#str) or [int](#int) | pandas' or polars' offset alias or integer denoting the frequency of the series. | *required* | | `season_length` | [int](#int) | Length of the seasonal period. This is used for producing the feature space. Only required if `init_config` is None. Defaults to None. | None | | `init_config` | [callable](#callable) | Function that takes an optuna trial and produces a configuration passed to the MLForecast constructor. Defaults to None. | None | | `fit_config` | [callable](#callable) | Function that takes an optuna trial and produces a configuration passed to the MLForecast fit method. Defaults to None. | None | | `num_threads` | [int](#int) | Number of threads to use when computing the features. Use -1 to use all available CPU cores. Defaults to 1. | 1 | | `reuse_cv_splits` | [bool](#bool) | Creates splits for cv once and re-uses them for tuning instead of generating the splits in each tuning round. Default is set to False. | False | #### `AutoMLForecast.fit` ```python theme={null} fit(df, n_windows, h, num_samples, step_size=None, input_size=None, refit=False, loss=None, id_col='unique_id', time_col='ds', target_col='y', study_kwargs=None, optimize_kwargs=None, fitted=False, prediction_intervals=None, weight_col=None) ``` Carry out the optimization process. Each model is optimized independently and the best one is trained on all data **Parameters:** | Name | Type | Description | Default | | ---------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | Series data in long format. | *required* | | `n_windows` | [int](#int) | Number of windows to evaluate. | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `num_samples` | [int](#int) | Number of trials to run | *required* | | `step_size` | [int](#int) | Step size between each cross validation window. If None it will be equal to `h`. Defaults to None. | None | | `input_size` | [int](#int) | Maximum training samples per serie in each window. If None, will use an expanding window. Defaults to None. | None | | `refit` | [bool](#bool) or [int](#int) | Retrain model for each cross validation window. If False, the models are trained at the beginning and then used to predict each window. If positive int, the models are retrained every `refit` windows. Defaults to False. | False | | `loss` | [callable](#callable) | Function that takes the validation and train dataframes and produces a float. If `None` will use the average SMAPE across series. Defaults to None. | None | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `study_kwargs` | [dict](#dict) | Keyword arguments to be passed to the optuna.Study constructor. Defaults to None. | None | | `optimize_kwargs` | [dict](#dict) | Keyword arguments to be passed to the optuna.Study.optimize method. Defaults to None. | None | | `fitted` | [bool](#bool) | Whether to compute the fitted values when retraining the best model. Defaults to False. | False | | `prediction_intervals` | [Optional](#typing.Optional)\[[PredictionIntervals](#mlforecast.utils.PredictionIntervals)] | Configuration to calibrate prediction intervals when retraining the best model. | None | **Returns:** | Type | Description | | -------------------------------------------------------------- | ------------------------------------------------ | | [AutoMLForecast](#mlforecast.auto.AutoMLForecast) | object with best models and optimization results | #### `AutoMLForecast.predict` ```python theme={null} predict(h, X_df=None, level=None) ``` "Compute forecasts **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Number of periods to predict. | *required* | | `X_df` | pandas or polars DataFrame | Dataframe with the future exogenous features. Should have the id column and the time column. Defaults to None. | None | | `level` | list of ints or floats | Confidence levels between 0 and 100 for prediction intervals. Defaults to None. | None | **Returns:** | Type | Description | | --------------------------------------- | ------------------------------------------------------------------- | | pandas or polars DataFrame | Predictions for each serie and timestep, with one column per model. | #### `AutoMLForecast.save` ```python theme={null} save(path) ``` Save AutoMLForecast objects **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------------------- | ----------------------------------------- | ---------- | | `path` | [str](#str) or [Path](#pathlib.Path) | Directory where artifacts will be stored. | *required* | #### `AutoMLForecast.forecast_fitted_values` ```python theme={null} forecast_fitted_values(level=None, *, h=1, train_df=None) ``` Access in-sample predictions. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------- | | `level` | list of ints or floats | Confidence levels between 0 and 100 for prediction intervals. Defaults to None. | None | | `h` | [int](#int) | Forecast horizon for fitted values. Defaults to 1. | 1 | | `train_df` | pandas or polars DataFrame | Training data to use when computing recursive fitted values for `h>1` on demand. Defaults to None. | None | **Returns:** | Type | Description | | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [DataFrame](#utilsforecast.compat.DataFrame) | pandas or polars DataFrame: DataFrame with the following columns: | | [DataFrame](#utilsforecast.compat.DataFrame) | - `id_col`: series identifier. | | [DataFrame](#utilsforecast.compat.DataFrame) | - `time_col`: timestamp of the predicted observation. | | [DataFrame](#utilsforecast.compat.DataFrame) | - `target_col`: actual observed value. | | [DataFrame](#utilsforecast.compat.DataFrame) | - `h`: number of steps ahead the prediction was made. For recursive models this equals the `h` argument. For direct models (`max_horizon`) it reflects the specific horizon step (1-indexed) at which each row was predicted, ranging from 1 to `max_horizon`. | | [DataFrame](#utilsforecast.compat.DataFrame) | - One column per model with the fitted (in-sample) predictions. | | [DataFrame](#utilsforecast.compat.DataFrame) | - If `level` is provided, additional columns with the lower and upper bounds of the prediction intervals for each model and confidence level. | ```python theme={null} import time import pandas as pd from datasetsforecast.m4 import M4, M4Evaluation, M4Info from sklearn.linear_model import Ridge from sklearn.compose import ColumnTransformer from sklearn.pipeline import make_pipeline from sklearn.preprocessing import OneHotEncoder ``` ```python theme={null} def train_valid_split(group): df, *_ = M4.load(directory='data', group=group) df['ds'] = df['ds'].astype('int') horizon = M4Info[group].horizon valid = df.groupby('unique_id').tail(horizon).copy() train = df.drop(valid.index).reset_index(drop=True) return train, valid ``` ```python theme={null} ridge_pipeline = make_pipeline( ColumnTransformer( [('encoder', OneHotEncoder(), ['unique_id'])], remainder='passthrough', ), Ridge() ) auto_ridge = AutoModel(ridge_pipeline, lambda trial: {f'ridge__{k}': v for k, v in ridge_space(trial).items()}) ``` ```python theme={null} optuna.logging.set_verbosity(optuna.logging.ERROR) group = 'Weekly' train, valid = train_valid_split(group) train['unique_id'] = train['unique_id'].astype('category') valid['unique_id'] = valid['unique_id'].astype(train['unique_id'].dtype) info = M4Info[group] h = info.horizon season_length = info.seasonality auto_mlf = AutoMLForecast( freq=1, season_length=season_length, models={ 'lgb': AutoLightGBM(), 'ridge': auto_ridge, }, fit_config=lambda trial: {'static_features': ['unique_id']}, num_threads=2, ) auto_mlf.fit( df=train, n_windows=2, h=h, num_samples=2, optimize_kwargs={'timeout': 60}, fitted=True, prediction_intervals=PredictionIntervals(n_windows=2, h=h), ) auto_mlf.predict(h, level=[80]) ``` | | unique\_id | ds | lgb | lgb-lo-80 | lgb-hi-80 | ridge | ridge-lo-80 | ridge-hi-80 | | ---- | ---------- | ---- | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | | 0 | W1 | 2180 | 35529.435224 | 35061.835362 | 35997.035086 | 36110.921202 | 35880.445097 | 36341.397307 | | 1 | W1 | 2181 | 35521.764894 | 34973.035617 | 36070.494171 | 36195.175757 | 36051.013811 | 36339.337702 | | 2 | W1 | 2182 | 35537.417268 | 34960.050939 | 36114.783596 | 36107.528852 | 35784.062169 | 36430.995536 | | 3 | W1 | 2183 | 35538.058206 | 34823.640706 | 36252.475705 | 36027.139248 | 35612.635725 | 36441.642771 | | 4 | W1 | 2184 | 35614.611211 | 34627.023739 | 36602.198683 | 36092.858489 | 35389.690977 | 36796.026000 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 4662 | W99 | 2292 | 15071.536978 | 14484.617399 | 15658.456557 | 15319.146221 | 14869.410567 | 15768.881875 | | 4663 | W99 | 2293 | 15058.145278 | 14229.686322 | 15886.604234 | 15299.549555 | 14584.269352 | 16014.829758 | | 4664 | W99 | 2294 | 15042.493434 | 14096.380636 | 15988.606232 | 15271.744712 | 14365.349338 | 16178.140086 | | 4665 | W99 | 2295 | 15042.144846 | 14037.053904 | 16047.235787 | 15250.070504 | 14403.428791 | 16096.712216 | | 4666 | W99 | 2296 | 15038.729044 | 13944.821480 | 16132.636609 | 15232.127800 | 14325.059776 | 16139.195824 | ```python theme={null} auto_mlf.forecast_fitted_values(level=[95]) ``` | | unique\_id | ds | y | lgb | lgb-lo-95 | lgb-hi-95 | ridge | ridge-lo-95 | ridge-hi-95 | | ------ | ---------- | ---- | -------- | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | | 0 | W1 | 15 | 1071.06 | 1060.584344 | 599.618355 | 1521.550334 | 1076.990151 | 556.535492 | 1597.444810 | | 1 | W1 | 16 | 1073.73 | 1072.669242 | 611.703252 | 1533.635232 | 1083.633276 | 563.178617 | 1604.087936 | | 2 | W1 | 17 | 1066.97 | 1072.452128 | 611.486139 | 1533.418118 | 1084.724311 | 564.269652 | 1605.178970 | | 3 | W1 | 18 | 1066.17 | 1065.837828 | 604.871838 | 1526.803818 | 1080.127197 | 559.672538 | 1600.581856 | | 4 | W1 | 19 | 1064.43 | 1065.214681 | 604.248691 | 1526.180671 | 1080.636826 | 560.182167 | 1601.091485 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 361881 | W99 | 2279 | 15738.54 | 15887.661228 | 15721.237195 | 16054.085261 | 15927.918181 | 15723.222760 | 16132.613603 | | 361882 | W99 | 2280 | 15388.13 | 15755.943789 | 15589.519756 | 15922.367823 | 15841.599064 | 15636.903642 | 16046.294485 | | 361883 | W99 | 2281 | 15187.62 | 15432.224701 | 15265.800668 | 15598.648735 | 15584.462232 | 15379.766811 | 15789.157654 | | 361884 | W99 | 2282 | 15172.27 | 15177.040831 | 15010.616797 | 15343.464864 | 15396.243223 | 15191.547801 | 15600.938644 | | 361885 | W99 | 2283 | 15101.03 | 15162.090803 | 14995.666770 | 15328.514836 | 15335.982465 | 15131.287044 | 15540.677887 | ```python theme={null} import polars as pl ``` ```python theme={null} train_pl = pl.from_pandas(train.astype({'unique_id': 'str'})) auto_mlf = AutoMLForecast( freq=1, season_length=season_length, models={'ridge': AutoRidge()}, num_threads=2, ) auto_mlf.fit( df=train_pl, n_windows=2, h=h, num_samples=2, optimize_kwargs={'timeout': 60}, fitted=True, prediction_intervals=PredictionIntervals(n_windows=2, h=h), ) auto_mlf.predict(h, level=[80]) ``` | unique\_id | ds | ridge | ridge-lo-80 | ridge-hi-80 | | ---------- | ---- | ------------ | ------------ | ------------ | | str | i64 | f64 | f64 | f64 | | "W1" | 2180 | 35046.096663 | 34046.69521 | 36045.498116 | | "W1" | 2181 | 34743.269216 | 33325.847975 | 36160.690457 | | "W1" | 2182 | 34489.591086 | 32591.254559 | 36387.927614 | | "W1" | 2183 | 34270.768179 | 32076.507727 | 36465.02863 | | "W1" | 2184 | 34124.021857 | 31352.454121 | 36895.589593 | | … | … | … | … | … | | "W99" | 2292 | 14719.457096 | 13983.308582 | 15455.605609 | | "W99" | 2293 | 14631.552077 | 13928.874336 | 15334.229818 | | "W99" | 2294 | 14532.905239 | 13642.840118 | 15422.97036 | | "W99" | 2295 | 14446.065443 | 13665.088667 | 15227.04222 | | "W99" | 2296 | 14363.049604 | 13654.220051 | 15071.879157 | ```python theme={null} auto_mlf.forecast_fitted_values(level=[95]) ``` | unique\_id | ds | y | ridge | ridge-lo-95 | ridge-hi-95 | | ---------- | ---- | -------- | ------------ | ------------ | ------------ | | str | i64 | f64 | f64 | f64 | f64 | | "W1" | 14 | 1061.96 | 1249.326428 | 488.765249 | 2009.887607 | | "W1" | 15 | 1071.06 | 1246.067836 | 485.506657 | 2006.629015 | | "W1" | 16 | 1073.73 | 1254.027897 | 493.466718 | 2014.589076 | | "W1" | 17 | 1066.97 | 1254.475948 | 493.914769 | 2015.037126 | | "W1" | 18 | 1066.17 | 1248.306754 | 487.745575 | 2008.867933 | | … | … | … | … | … | … | | "W99" | 2279 | 15738.54 | 15754.558812 | 15411.968645 | 16097.148979 | | "W99" | 2280 | 15388.13 | 15655.780865 | 15313.190698 | 15998.371032 | | "W99" | 2281 | 15187.62 | 15367.498468 | 15024.908301 | 15710.088635 | | "W99" | 2282 | 15172.27 | 15172.591423 | 14830.001256 | 15515.18159 | | "W99" | 2283 | 15101.03 | 15141.032886 | 14798.44272 | 15483.623053 | # Callbacks Source: https://nixtlaverse.nixtla.io/mlforecast/callbacks.html Utility functions use in the predict step. ## ### `SaveFeatures` ```python theme={null} SaveFeatures() ``` Saves the features in every timestamp. #### `SaveFeatures.get_features` ```python theme={null} get_features(with_step=False) ``` Retrieves the input features for every timestep **Parameters:** | Name | Type | Description | Default | | ----------- | -------------------------- | ---------------------------------------------------- | ------------------ | | `with_step` | [bool](#bool) | Add a column indicating the step. Defaults to False. | False | **Returns:** | Type | Description | | --------------------------------------- | ----------------------------- | | pandas or polars DataFrame | DataFrame with input features | # Conformal Prediction Source: https://nixtlaverse.nixtla.io/mlforecast/conformal_prediction.html Conformal prediction intervals and transfer conformal methods ## ### `PredictionIntervals` ```python theme={null} PredictionIntervals(n_windows=2, h=1, method='conformal_distribution', scale_estimator=None) ``` Class for storing prediction intervals metadata information. ### `TransferConformal` ```python theme={null} TransferConformal(method='recalibrate', dre_estimator='logistic', weights=None, n_windows=None, step_size=None, cv=5, clip_quantile=0.99) ``` Predict-time configuration for transfer conformal prediction. Pass to `MLForecast.predict(transfer_conformal=...)` instead of the removed flat kwargs `transfer_conformal_method`, `covariate_shift_weights`, and `dre_estimator`. A plain string is shorthand for `TransferConformal(method=)`. #### `TransferConformal.validate` ```python theme={null} validate(pi) ``` Cross-validate against the fitted PredictionIntervals config. ### `estimate_density_ratio` ```python theme={null} estimate_density_ratio(source_features, target_features, estimator='logistic', cv=5, clip_quantile=0.99, return_target_weights=False) ``` Estimate w(x) = p\_target(x) / p\_source(x) for source domain points. Trains a binary classifier (source=0, target=1) on StandardScaler- normalised features and returns the odds ratio p(1|x) / p(0|x) for each source point. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | `source_features` | [ndarray](#numpy.ndarray) | Feature matrix for source-domain calibration points, shape (n\_source, n\_features). | *required* | | `target_features` | [ndarray](#numpy.ndarray) | Feature matrix for target-domain points, shape (n\_target, n\_features). | *required* | | `estimator` | [str](#str) | `"logistic"` (default) or `"gradient_boosting"`. | 'logistic' | | `cv` | [int](#int) | Number of stratified K-fold splits for cross-fitting (`cv >= 2`). Source weights are computed from out-of-fold predictions, reducing overfitting from in-sample scoring. `cv=0` or `cv=1` uses the original in-sample behavior. Defaults to 5. | 5 | | `clip_quantile` | [Optional](#typing.Optional)\[[float](#float)] | Clip source weights above this quantile of the computed weights to prevent extreme values. `None` disables clipping. Defaults to 0.99. | 0.99 | | `return_target_weights` | [bool](#bool) | If `True`, also return per-target-row weights (averaged across fold models when `cv >= 2`). Defaults to `False`. | False | **Returns:** | Type | Description | | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | [Union](#typing.Union)\[[ndarray](#numpy.ndarray), [Tuple](#typing.Tuple)\[[ndarray](#numpy.ndarray), [ndarray](#numpy.ndarray)]] | np.ndarray of shape (n\_source,) if `return_target_weights=False`, else | | [Union](#typing.Union)\[[ndarray](#numpy.ndarray), [Tuple](#typing.Tuple)\[[ndarray](#numpy.ndarray), [ndarray](#numpy.ndarray)]] | a tuple `(source_weights, target_weights)` where target\_weights has | | [Union](#typing.Union)\[[ndarray](#numpy.ndarray), [Tuple](#typing.Tuple)\[[ndarray](#numpy.ndarray), [ndarray](#numpy.ndarray)]] | shape (n\_target,). | # Core | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/core.html ```python theme={null} import datetime import tempfile from nbdev import show_doc from fastcore.test import test_eq, test_fail, test_warns from mlforecast.callbacks import SaveFeatures from mlforecast.lag_transforms import ExpandingMean, RollingMean from mlforecast.target_transforms import Differences, LocalStandardScaler from mlforecast.utils import generate_daily_series, generate_prices_for_series ``` ## Data format The required input format is a dataframe with at least the following columns: * `unique_id` with a unique identifier for each time serie * `ds` with the datestamp and a column * `y` with the values of theserie Every other column is considered a static feature unless stated otherwise in `TimeSeries.fit` ```python theme={null} series = generate_daily_series(20, n_static_features=2) series ``` | | unique\_id | ds | y | static\_0 | static\_1 | | ---- | ---------- | ---------- | ---------- | --------- | --------- | | 0 | id\_00 | 2000-01-01 | 7.404529 | 27 | 53 | | 1 | id\_00 | 2000-01-02 | 35.952624 | 27 | 53 | | 2 | id\_00 | 2000-01-03 | 68.958353 | 27 | 53 | | 3 | id\_00 | 2000-01-04 | 84.994505 | 27 | 53 | | 4 | id\_00 | 2000-01-05 | 113.219810 | 27 | 53 | | ... | ... | ... | ... | ... | ... | | 4869 | id\_19 | 2000-03-25 | 400.606807 | 97 | 45 | | 4870 | id\_19 | 2000-03-26 | 538.794824 | 97 | 45 | | 4871 | id\_19 | 2000-03-27 | 620.202104 | 97 | 45 | | 4872 | id\_19 | 2000-03-28 | 20.625426 | 97 | 45 | | 4873 | id\_19 | 2000-03-29 | 141.513169 | 97 | 45 | For simplicity we’ll just take one time serie here. ```python theme={null} uids = series['unique_id'].unique() serie = series[series['unique_id'].eq(uids[0])] serie ``` | | unique\_id | ds | y | static\_0 | static\_1 | | --- | ---------- | ---------- | ---------- | --------- | --------- | | 0 | id\_00 | 2000-01-01 | 7.404529 | 27 | 53 | | 1 | id\_00 | 2000-01-02 | 35.952624 | 27 | 53 | | 2 | id\_00 | 2000-01-03 | 68.958353 | 27 | 53 | | 3 | id\_00 | 2000-01-04 | 84.994505 | 27 | 53 | | 4 | id\_00 | 2000-01-05 | 113.219810 | 27 | 53 | | ... | ... | ... | ... | ... | ... | | 217 | id\_00 | 2000-08-05 | 13.263188 | 27 | 53 | | 218 | id\_00 | 2000-08-06 | 38.231981 | 27 | 53 | | 219 | id\_00 | 2000-08-07 | 59.555183 | 27 | 53 | | 220 | id\_00 | 2000-08-08 | 86.986368 | 27 | 53 | | 221 | id\_00 | 2000-08-09 | 119.254810 | 27 | 53 | *** ### `TimeSeries` ```python theme={null} TimeSeries(freq, lags=None, lag_transforms=None, date_features=None, num_threads=1, target_transforms=None, lag_transforms_namer=None, date_features_as_dummies=False, drop_auxiliary_columns=True) ``` Utility class for storing and transforming time series data. #### `TimeSeries.fit_transform` ```python theme={null} fit_transform(data, id_col, time_col, target_col, static_features=None, dropna=True, keep_last_n=None, max_horizon=None, horizons=None, return_X_y=False, as_numpy=False, weight_col=None) ``` Add the features to `data` and save the required information for the predictions step. If not all features are static, specify which ones are in `static_features`. If you don't want to drop rows with null values after the transformations set `dropna=False` If `keep_last_n` is not None then that number of observations is kept across all series for updates. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ----------------- | | `max_horizon` | [Optional](#typing.Optional)\[[int](#int)] | Train models for all horizons 1 to max\_horizon. | None | | `horizons` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Train models only for specific horizons (1-indexed). Mutually exclusive with max\_horizon. | None | #### `TimeSeries.predict` ```python theme={null} predict(models, horizon, before_predict_callback=None, after_predict_callback=None, X_df=None, ids=None) ``` #### `TimeSeries.update` ```python theme={null} update(df, validate_new_data=False) ``` Update the values of the stored series. **Parameters:** | Name | Type | Description | Default | | ------------------- | --------------------------------------------------------- | --------------------------------------------------------- | ------------------ | | `df` | [DataFrame](#utilsforecast.compat.DataFrame) | New observations to append. | *required* | | `validate_new_data` | [bool](#bool) | If True, validate continuity, start dates, and frequency. | False | ```python theme={null} def month_start_or_end(dates): return dates.is_month_start | dates.is_month_end flow_config = dict( freq='W-THU', lags=[7], lag_transforms={ 1: [ExpandingMean(), RollingMean(7)] }, date_features=['dayofweek', 'week', month_start_or_end] ) ts = TimeSeries(**flow_config) ts ``` ```text theme={null} TimeSeries(freq=W-THU, transforms=['lag7', 'expanding_mean_lag1', 'rolling_mean_lag1_window_size7'], date_features=['dayofweek', 'week', 'month_start_or_end'], num_threads=1) ``` The frequency is converted to an offset. ```python theme={null} test_eq(ts.freq, pd.tseries.frequencies.to_offset(flow_config['freq'])) ``` The date features are stored as they were passed to the constructor. ```python theme={null} test_eq(ts.date_features, flow_config['date_features']) ``` The transformations are stored as a dictionary where the key is the name of the transformation (name of the column in the dataframe with the computed features), which is built using `build_transform_name` and the value is a tuple where the first element is the lag it is applied to, then the function and then the function arguments. ```python theme={null} test_eq( ts.transforms.keys(), ['lag7', 'expanding_mean_lag1', 'rolling_mean_lag1_window_size7'], ) ``` Note that for `lags` we define the transformation as the identity function applied to its corresponding lag. This is because [`_transform_series`](https://Nixtla.github.io/mlforecast/grouped_array.html#_transform_series) takes the lag as an argument and shifts the array before computing the transformation. ```python theme={null} flow_config = dict( freq='D', lags=[7, 14], lag_transforms={ 2: [ RollingMean(7), RollingMean(14), ] }, date_features=['dayofweek', 'month', 'year'], num_threads=2 ) ts = TimeSeries(**flow_config) _ = ts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y') ``` The series values are stored as a GroupedArray in an attribute `ga`. If the data type of the series values is an int then it is converted to `np.float32`, this is because lags generate `np.nan`s so we need a float data type for them. ```python theme={null} np.testing.assert_equal( ts.ga.data, series.groupby('unique_id', observed=True).tail(ts.keep_last_n)['y'], ) ``` The series ids are stored in an `uids` attribute. ```python theme={null} test_eq(ts.uids, series['unique_id'].unique()) ``` For each time serie, the last observed date is stored so that predictions start from the last date + the frequency. ```python theme={null} test_eq(ts.last_dates, series.groupby('unique_id', observed=True)['ds'].max().values) ``` The last row of every serie without the `y` and `ds` columns are taken as static features. ```python theme={null} pd.testing.assert_frame_equal( ts.static_features_, series.groupby('unique_id', observed=True).tail(1).drop(columns=['ds', 'y']).reset_index(drop=True), ) ``` If you pass `static_features` to [`TimeSeries.fit_transform`](https://Nixtla.github.io/mlforecast/core.html#timeseries.fit_transform) then only these are kept. ```python theme={null} ts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y', static_features=['static_0']) pd.testing.assert_frame_equal( ts.static_features_, series.groupby('unique_id', observed=True).tail(1)[['unique_id', 'static_0']].reset_index(drop=True), ) ``` You can also specify keep\_last\_n in TimeSeries.fit\_transform, which means that after computing the features for training we want to keep only the last n samples of each time serie for computing the updates. This saves both memory and time, since the updates are performed by running the transformation functions on all time series again and keeping only the last value (the update). If you have very long time series and your updates only require a small sample it’s recommended that you set keep\_last\_n to the minimum number of samples required to compute the updates, which in this case is 15 since we have a rolling mean of size 14 over the lag 2 and in the first update the lag 2 becomes the lag 1. This is because in the first update the lag 1 is the last value of the series (or the lag 0), the lag 2 is the lag 1 and so on. ```python theme={null} keep_last_n = 15 ts = TimeSeries(**flow_config) df = ts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y', keep_last_n=keep_last_n) ts._predict_setup() expected_lags = ['lag7', 'lag14'] expected_transforms = ['rolling_mean_lag2_window_size7', 'rolling_mean_lag2_window_size14'] expected_date_features = ['dayofweek', 'month', 'year'] test_eq(ts.features, expected_lags + expected_transforms + expected_date_features) test_eq(ts.static_features_.columns.tolist() + ts.features, df.columns.drop(['ds', 'y']).tolist()) # we dropped 2 rows because of the lag 2 and 13 more to have the window of size 14 test_eq(df.shape[0], series.shape[0] - (2 + 13) * ts.ga.n_groups) test_eq(ts.ga.data.size, ts.ga.n_groups * keep_last_n) ``` [`TimeSeries.fit_transform`](https://Nixtla.github.io/mlforecast/core.html#timeseries.fit_transform) requires that the *y* column doesn’t have any null values. This is because the transformations could propagate them forward, so if you have null values in the *y* column you’ll get an error. ```python theme={null} series_with_nulls = series.copy() series_with_nulls.loc[1, 'y'] = np.nan test_fail( lambda: ts.fit_transform(series_with_nulls, id_col='unique_id', time_col='ds', target_col='y'), contains='y column contains null values' ) ``` Once we have a trained model we can use [`TimeSeries.predict`](https://Nixtla.github.io/mlforecast/core.html#timeseries.predict) passing the model and the horizon to get the predictions back. ```python theme={null} class DummyModel: def predict(self, X: pd.DataFrame) -> np.ndarray: return X['lag7'].values horizon = 7 model = DummyModel() ts = TimeSeries(**flow_config) ts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y') predictions = ts.predict({'DummyModel': model}, horizon) grouped_series = series.groupby('unique_id', observed=True) expected_preds = grouped_series['y'].tail(7) # the model predicts the lag-7 last_dates = grouped_series['ds'].max() expected_dsmin = last_dates + pd.offsets.Day() expected_dsmax = last_dates + horizon * pd.offsets.Day() grouped_preds = predictions.groupby('unique_id', observed=True) np.testing.assert_allclose(predictions['DummyModel'], expected_preds) pd.testing.assert_series_equal(grouped_preds['ds'].min(), expected_dsmin) pd.testing.assert_series_equal(grouped_preds['ds'].max(), expected_dsmax) ``` If we have dynamic features we can pass them to `X_df`. ```python theme={null} class PredictPrice: def predict(self, X): return X['price'] series = generate_daily_series(20, n_static_features=2, equal_ends=True) dynamic_series = series.rename(columns={'static_1': 'product_id'}) prices_catalog = generate_prices_for_series(dynamic_series) series_with_prices = dynamic_series.merge(prices_catalog, how='left') model = PredictPrice() ts = TimeSeries(**flow_config) ts.fit_transform( series_with_prices, id_col='unique_id', time_col='ds', target_col='y', static_features=['static_0', 'product_id'], ) predictions = ts.predict({'PredictPrice': model}, horizon=1, X_df=prices_catalog) pd.testing.assert_frame_equal( predictions.rename(columns={'PredictPrice': 'price'}), prices_catalog.merge(predictions[['unique_id', 'ds']])[['unique_id', 'ds', 'price']] ) ``` # Distributed Forecast Source: https://nixtlaverse.nixtla.io/mlforecast/distributed.forecast.html Distributed pipeline encapsulation **This interface is only tested on Linux** ## ### `DistributedMLForecast` ```python theme={null} DistributedMLForecast(models, freq, lags=None, lag_transforms=None, date_features=None, num_threads=1, target_transforms=None, engine=None, num_partitions=None, lag_transforms_namer=None, date_features_as_dummies=False) ``` Multi backend distributed pipeline Create distributed forecast object **Parameters:** | Name | Type | Description | Default | | -------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `models` | regressor or list of regressors | Models that will be trained and used to compute the forecasts. | *required* | | `freq` | [str](#str) or [int](#int) | Pandas offset alias, e.g. 'D', 'W-THU' or integer denoting the frequency of the series. Defaults to None. | *required* | | `lags` | list of int | Lags of the target to use as features. Defaults to None. | None | | `lag_transforms` | dict of int to list of functions | Mapping of target lags to their transformations. Defaults to None. | None | | `date_features` | list of str or callable | Features computed from the dates. Can be pandas date attributes or functions that will take the dates as input. Defaults to None. | None | | `num_threads` | [int](#int) | Number of threads to use when computing the features. Use -1 to use all available CPU cores. Defaults to 1. | 1 | | `target_transforms` | list of transformers | Transformations that will be applied to the target before computing the features and restored after the forecasting step. Defaults to None. | None | | `engine` | fugue execution engine | Dask Client, Spark Session, etc to use for the distributed computation. If None will infer depending on the input type. Defaults to None. | None | | `num_partitions` | number of data partitions to use | If None, the default partitions provided by the AnyDataFrame used by the `fit` and `cross_validation` methods will be used. If a Ray Dataset is provided and `num_partitions` is None, the partitioning will be done by the `id_col`. Defaults to None. | None | | `lag_transforms_namer` | [callable](#callable) | Function that takes a transformation (either function or class), a lag and extra arguments and produces a name. Defaults to None. | None | | `date_features_as_dummies` | [bool](#bool) | If True, string date features with a known finite range (e.g. 'dayofweek', 'month') are expanded into binary indicator columns named '\_' instead of being kept as ordinal integers. Defaults to False. | False | #### `DistributedMLForecast.fit` ```python theme={null} fit(df, id_col='unique_id', time_col='ds', target_col='y', static_features=None, dropna=True, keep_last_n=None, weight_col=None) ``` Apply the feature engineering and train the models. **Parameters:** | Name | Type | Description | Default | | ----------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | dask, spark or ray DataFrame | Series data in long format. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `static_features` | list of str | Names of the features that are static and will be repeated when forecasting. Defaults to None. | None | | `dropna` | [bool](#bool) | Drop rows with missing values produced by the transformations. Defaults to True. | True | | `keep_last_n` | [int](#int) | Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it. Pooled lag transforms (global\_/groupby/partition\_by) with a window wider than this keep that wider window instead, since their shared aggregates have no per-series buffer to trim below it. Defaults to None. | None | | `weight_col` | [str](#str) | Column that contains the sample weights. Defaults to None. | None | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | [DistributedMLForecast](#mlforecast.distributed.forecast.DistributedMLForecast) | Forecast object with series values and trained models. | #### `DistributedMLForecast.predict` ```python theme={null} predict(h, before_predict_callback=None, after_predict_callback=None, X_df=None, new_df=None, ids=None) ``` Compute the predictions for the next `horizon` steps. **Parameters:** | Name | Type | Description | Default | | ------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `before_predict_callback` | [callable](#callable) | Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index. Defaults to None. | None | | `after_predict_callback` | [callable](#callable) | Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index. Defaults to None. | None | | `X_df` | pandas, Dask, Spark or Ray DataFrame | Dataframe with the future exogenous features. Should have the id column and the time column. Distributed DataFrames (Dask, Spark, Ray) are processed per-partition so that no single node needs to hold the entire X\_df in memory at once. Defaults to None. | None | | `new_df` | dask or spark DataFrame | Series data of new observations for which forecasts are to be generated. This dataframe should have the same structure as the one used to fit the model, including any features and time series data. If `new_df` is not None, the method will generate forecasts for the new observations. Defaults to None. | None | | `ids` | list of str | List with subset of ids seen during training for which the forecasts should be computed. Defaults to None. | None | **Returns:** | Type | Description | | ----------------------------------------- | ------------------------------------------------------------------- | | dask, spark or ray DataFrame | Predictions for each serie and timestep, with one column per model. | #### `DistributedMLForecast.save` ```python theme={null} save(path) ``` Save forecast object **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------ | ----------------------------------------- | ---------- | | `path` | [str](#str) | Directory where artifacts will be stored. | *required* | #### `DistributedMLForecast.load` ```python theme={null} load(path, engine) ``` Load forecast object **Parameters:** | Name | Type | Description | Default | | -------- | ----------------------------------- | ----------------------------------------------------------------------- | ---------- | | `path` | [str](#str) | Directory with saved artifacts. | *required* | | `engine` | fugue execution engine | Dask Client, Spark Session, etc to use for the distributed computation. | *required* | #### `DistributedMLForecast.update` ```python theme={null} update(df) ``` Update the values of the stored series. **Parameters:** | Name | Type | Description | Default | | ---- | ----------------------------- | -------------------------------- | ---------- | | `df` | pandas DataFrame | Dataframe with new observations. | *required* | #### `DistributedMLForecast.to_local` ```python theme={null} to_local() ``` Convert this distributed forecast object into a local one This pulls all the data from the remote machines, so you have to be sure that it fits in the scheduler/driver. If you're not sure use the save method instead. **Returns:** | Type | Description | | ---------------------------------------------------------- | ---------------------- | | [MLForecast](#mlforecast.forecast.MLForecast) | Local forecast object. | #### `DistributedMLForecast.preprocess` ```python theme={null} preprocess(df, id_col='unique_id', time_col='ds', target_col='y', static_features=None, dropna=True, keep_last_n=None) ``` Add the features to `data`. **Parameters:** | Name | Type | Description | Default | | ----------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | dask, spark or ray DataFrame | Series data in long format. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `static_features` | list of str | Names of the features that are static and will be repeated when forecasting. Defaults to None. | None | | `dropna` | [bool](#bool) | Drop rows with missing values produced by the transformations. Defaults to True. | True | | `keep_last_n` | [int](#int) | Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it. Pooled lag transforms (global\_/groupby/partition\_by) with a window wider than this keep that wider window instead, since their shared aggregates have no per-series buffer to trim below it. Defaults to None. | None | **Returns:** | Type | Description | | ---------------------------- | ------------------------- | | same type as df | `df` with added features. | #### `DistributedMLForecast.cross_validation` ```python theme={null} cross_validation(df, n_windows, h, id_col='unique_id', time_col='ds', target_col='y', step_size=None, static_features=None, dropna=True, keep_last_n=None, refit=True, before_predict_callback=None, after_predict_callback=None, input_size=None, weight_col=None) ``` Perform time series cross validation. Creates `n_windows` splits where each window has `h` test periods, trains the models, computes the predictions and merges the actuals. **Parameters:** | Name | Type | Description | Default | | ------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | dask, spark or ray DataFrame | Series data in long format. | *required* | | `n_windows` | [int](#int) | Number of windows to evaluate. | *required* | | `h` | [int](#int) | Number of test periods in each window. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `step_size` | [int](#int) | Step size between each cross validation window. If None it will be equal to `h`. Defaults to None. | None | | `static_features` | list of str | Names of the features that are static and will be repeated when forecasting. Defaults to None. | None | | `dropna` | [bool](#bool) | Drop rows with missing values produced by the transformations. Defaults to True. | True | | `keep_last_n` | [int](#int) | Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it. Pooled lag transforms (global\_/groupby/partition\_by) with a window wider than this keep that wider window instead, since their shared aggregates have no per-series buffer to trim below it. Defaults to None. | None | | `refit` | [bool](#bool) | Retrain model for each cross validation window. If False, the models are trained at the beginning and then used to predict each window. Defaults to True. | True | | `before_predict_callback` | [callable](#callable) | Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index. Defaults to None. | None | | `after_predict_callback` | [callable](#callable) | Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index. Defaults to None. | None | | `input_size` | [int](#int) | Maximum training samples per serie in each window. If None, will use an expanding window. Defaults to None. | None | | `weight_col` | [str](#str) | Column that contains the sample weights. Defaults to None. | None | **Returns:** | Type | Description | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------- | | dask, spark or ray DataFrame | Predictions for each window with the series id, timestamp, target value and predictions from each model. | # DaskLGBMForecast Source: https://nixtlaverse.nixtla.io/mlforecast/distributed.models.dask.lgb.html dask LightGBM forecaster Wrapper of `lightgbm.dask.DaskLGBMRegressor` that adds a `model_` property that contains the fitted booster and is sent to the workers to in the forecasting step. ### `DaskLGBMForecast` Bases: [DaskLGBMRegressor](#lightgbm.dask.DaskLGBMRegressor) #### `DaskLGBMForecast.model_` ```python theme={null} model_ ``` # DaskXGBForecast Source: https://nixtlaverse.nixtla.io/mlforecast/distributed.models.dask.xgb.html dask XGBoost forecaster Wrapper of `xgboost.dask.DaskXGBRegressor` that adds a `model_` property that contains the fitted model and is sent to the workers in the forecasting step. ### `DaskXGBForecast` Bases: [DaskXGBRegressor](#xgboost.dask.DaskXGBRegressor) #### `DaskXGBForecast.model_` ```python theme={null} model_ ``` # RayLGBMForecast Source: https://nixtlaverse.nixtla.io/mlforecast/distributed.models.ray.lgb.html ray LightGBM forecaster Wrapper of `lightgbm.ray.RayLGBMRegressor` that adds a `model_` property that contains the fitted booster and is sent to the workers to in the forecasting step. ### `RayLGBMForecast` Bases: [RayLGBMRegressor](#lightgbm_ray.RayLGBMRegressor) #### `RayLGBMForecast.model_` ```python theme={null} model_ ``` # RayXGBForecast Source: https://nixtlaverse.nixtla.io/mlforecast/distributed.models.ray.xgb.html ray XGBoost forecaster Wrapper of `xgboost.ray.RayXGBRegressor` that adds a `model_` property that contains the fitted model and is sent to the workers in the forecasting step. ### `RayXGBForecast` Bases: [RayXGBRegressor](#xgboost_ray.RayXGBRegressor) #### `RayXGBForecast.model_` ```python theme={null} model_ ``` # SparkLGBMForecast Source: https://nixtlaverse.nixtla.io/mlforecast/distributed.models.spark.lgb.html spark LightGBM forecaster Wrapper of `synapse.ml.lightgbm.LightGBMRegressor` that adds an `extract_local_model` method to get a local version of the trained model and broadcast it to the workers. ### `SparkLGBMForecast` Bases: [LightGBMRegressor](#synapse.ml.lightgbm.LightGBMRegressor) #### `SparkLGBMForecast.extract_local_model` ```python theme={null} extract_local_model(trained_model) ``` # SparkXGBForecast Source: https://nixtlaverse.nixtla.io/mlforecast/distributed.models.spark.xgb.html spark XGBoost forecaster Wrapper of `xgboost.spark.SparkXGBRegressor` that adds an `extract_local_model` method to get a local version of the trained model and broadcast it to the workers. ### `SparkXGBForecast` Bases: [SparkXGBRegressor](#xgboost.spark.SparkXGBRegressor) #### `SparkXGBForecast.extract_local_model` ```python theme={null} extract_local_model(trained_model) ``` # End to end walkthrough | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/docs/getting-started/end_to_end_walkthrough.html > Detailed description of all the functionalities that MLForecast > provides. ## Data setup For this example we’ll use a subset of the M4 hourly dataset. You can find the a notebook with the full dataset [here](https://www.kaggle.com/code/lemuz90/m4-competition). ```python theme={null} import random import tempfile from pathlib import Path import pandas as pd from datasetsforecast.m4 import M4 from utilsforecast.plotting import plot_series ``` ```python theme={null} await M4.async_download('data', group='Hourly') df, *_ = M4.load('data', 'Hourly') uids = df['unique_id'].unique() random.seed(0) sample_uids = random.choices(uids, k=4) df = df[df['unique_id'].isin(sample_uids)].reset_index(drop=True) df['ds'] = df['ds'].astype('int64') df ``` | | unique\_id | ds | y | | ---- | ---------- | ---- | ---- | | 0 | H196 | 1 | 11.8 | | 1 | H196 | 2 | 11.4 | | 2 | H196 | 3 | 11.1 | | 3 | H196 | 4 | 10.8 | | 4 | H196 | 5 | 10.6 | | ... | ... | ... | ... | | 4027 | H413 | 1004 | 99.0 | | 4028 | H413 | 1005 | 88.0 | | 4029 | H413 | 1006 | 47.0 | | 4030 | H413 | 1007 | 41.0 | | 4031 | H413 | 1008 | 34.0 | ## EDA We’ll take a look at our series to get ideas for transformations and features. ```python theme={null} fig = plot_series(df, max_insample_length=24 * 14) ``` We can use the `MLForecast.preprocess` method to explore different transformations. It looks like these series have a strong seasonality on the hour of the day, so we can subtract the value from the same hour in the previous day to remove it. This can be done with the `mlforecast.target_transforms.Differences` transformer, which we pass through `target_transforms`. ```python theme={null} from mlforecast import MLForecast from mlforecast.target_transforms import Differences ``` ```python theme={null} fcst = MLForecast( models=[], # we're not interested in modeling yet freq=1, # our series have integer timestamps, so we'll just add 1 in every timestep target_transforms=[Differences([24])], ) prep = fcst.preprocess(df) prep ``` | | unique\_id | ds | y | | ---- | ---------- | ---- | ---- | | 24 | H196 | 25 | 0.3 | | 25 | H196 | 26 | 0.3 | | 26 | H196 | 27 | 0.1 | | 27 | H196 | 28 | 0.2 | | 28 | H196 | 29 | 0.2 | | ... | ... | ... | ... | | 4027 | H413 | 1004 | 39.0 | | 4028 | H413 | 1005 | 55.0 | | 4029 | H413 | 1006 | 14.0 | | 4030 | H413 | 1007 | 3.0 | | 4031 | H413 | 1008 | 4.0 | This has subtracted the lag 24 from each value, we can see what our series look like now. ```python theme={null} fig = plot_series(prep) ``` ## Adding features ### Lags Looks like the seasonality is gone, we can now try adding some lag features. ```python theme={null} fcst = MLForecast( models=[], freq=1, lags=[1, 24], target_transforms=[Differences([24])], ) prep = fcst.preprocess(df) prep ``` | | unique\_id | ds | y | lag1 | lag24 | | ---- | ---------- | ---- | ---- | ---- | ----- | | 48 | H196 | 49 | 0.1 | 0.1 | 0.3 | | 49 | H196 | 50 | 0.1 | 0.1 | 0.3 | | 50 | H196 | 51 | 0.2 | 0.1 | 0.1 | | 51 | H196 | 52 | 0.1 | 0.2 | 0.2 | | 52 | H196 | 53 | 0.1 | 0.1 | 0.2 | | ... | ... | ... | ... | ... | ... | | 4027 | H413 | 1004 | 39.0 | 29.0 | 1.0 | | 4028 | H413 | 1005 | 55.0 | 39.0 | -25.0 | | 4029 | H413 | 1006 | 14.0 | 55.0 | -20.0 | | 4030 | H413 | 1007 | 3.0 | 14.0 | 0.0 | | 4031 | H413 | 1008 | 4.0 | 3.0 | -16.0 | ```python theme={null} prep.drop(columns=['unique_id', 'ds']).corr()['y'] ``` ```text theme={null} y 1.000000 lag1 0.622531 lag24 -0.234268 Name: y, dtype: float64 ``` ### Lag transforms Lag transforms are defined as a dictionary where the keys are the lags and the values are the transformations that we want to apply to that lag. The lag transformations can be either objects from the `mlforecast.lag_transforms` module or [numba](http://numba.pydata.org/) jitted functions (so that computing the features doesn’t become a bottleneck and we can bypass the GIL when using multithreading), we have some implemented in the [window-ops package](https://github.com/jmoralez/window_ops) but you can also implement your own. ```python theme={null} from mlforecast.lag_transforms import ExpandingMean, RollingMean ``` ```python theme={null} from numba import njit from window_ops.rolling import rolling_mean ``` ```python theme={null} @njit def rolling_mean_48(x): return rolling_mean(x, window_size=48) fcst = MLForecast( models=[], freq=1, target_transforms=[Differences([24])], lag_transforms={ 1: [ExpandingMean()], 24: [RollingMean(window_size=48), rolling_mean_48], }, ) prep = fcst.preprocess(df) prep ``` | | unique\_id | ds | y | expanding\_mean\_lag1 | rolling\_mean\_lag24\_window\_size48 | rolling\_mean\_48\_lag24 | | ---- | ---------- | ---- | ---- | --------------------- | ------------------------------------ | ------------------------ | | 95 | H196 | 96 | 0.1 | 0.174648 | 0.150000 | 0.150000 | | 96 | H196 | 97 | 0.3 | 0.173611 | 0.145833 | 0.145833 | | 97 | H196 | 98 | 0.3 | 0.175342 | 0.141667 | 0.141667 | | 98 | H196 | 99 | 0.3 | 0.177027 | 0.141667 | 0.141667 | | 99 | H196 | 100 | 0.3 | 0.178667 | 0.141667 | 0.141667 | | ... | ... | ... | ... | ... | ... | ... | | 4027 | H413 | 1004 | 39.0 | 0.242084 | 3.437500 | 3.437500 | | 4028 | H413 | 1005 | 55.0 | 0.281633 | 2.708333 | 2.708333 | | 4029 | H413 | 1006 | 14.0 | 0.337411 | 2.125000 | 2.125000 | | 4030 | H413 | 1007 | 3.0 | 0.351324 | 1.770833 | 1.770833 | | 4031 | H413 | 1008 | 4.0 | 0.354018 | 1.208333 | 1.208333 | You can see that both approaches get to the same result, you can use whichever one you feel most comfortable with. ### Date features If your time column is made of timestamps then it might make sense to extract features like week, dayofweek, quarter, etc. You can do that by passing a list of strings with [pandas time/date components](https://pandas.pydata.org/docs/user_guide/timeseries.html#time-date-components). You can also pass functions that will take the time column as input, as we’ll show here. ```python theme={null} def hour_index(times): return times % 24 fcst = MLForecast( models=[], freq=1, target_transforms=[Differences([24])], date_features=[hour_index], ) fcst.preprocess(df) ``` | | unique\_id | ds | y | hour\_index | | ---- | ---------- | ---- | ---- | ----------- | | 24 | H196 | 25 | 0.3 | 1 | | 25 | H196 | 26 | 0.3 | 2 | | 26 | H196 | 27 | 0.1 | 3 | | 27 | H196 | 28 | 0.2 | 4 | | 28 | H196 | 29 | 0.2 | 5 | | ... | ... | ... | ... | ... | | 4027 | H413 | 1004 | 39.0 | 20 | | 4028 | H413 | 1005 | 55.0 | 21 | | 4029 | H413 | 1006 | 14.0 | 22 | | 4030 | H413 | 1007 | 3.0 | 23 | | 4031 | H413 | 1008 | 4.0 | 0 | ### Target transformations If you want to do some transformation to your target before computing the features and then re-apply it after predicting you can use the `target_transforms` argument, which takes a list of transformations. You can find the implemented ones in `mlforecast.target_transforms` or you can implement your own as described in the [target transformations guide](../how-to-guides/target_transforms_guide.html#custom-transformations). ```python theme={null} from mlforecast.target_transforms import LocalStandardScaler ``` ```python theme={null} fcst = MLForecast( models=[], freq=1, lags=[1], target_transforms=[LocalStandardScaler()] ) fcst.preprocess(df) ``` | | unique\_id | ds | y | lag1 | | ---- | ---------- | ---- | --------- | --------- | | 1 | H196 | 2 | -1.493026 | -1.383286 | | 2 | H196 | 3 | -1.575331 | -1.493026 | | 3 | H196 | 4 | -1.657635 | -1.575331 | | 4 | H196 | 5 | -1.712505 | -1.657635 | | 5 | H196 | 6 | -1.794810 | -1.712505 | | ... | ... | ... | ... | ... | | 4027 | H413 | 1004 | 3.062766 | 2.425012 | | 4028 | H413 | 1005 | 2.523128 | 3.062766 | | 4029 | H413 | 1006 | 0.511751 | 2.523128 | | 4030 | H413 | 1007 | 0.217403 | 0.511751 | | 4031 | H413 | 1008 | -0.126003 | 0.217403 | We can define a naive model to test this ```python theme={null} from sklearn.base import BaseEstimator class Naive(BaseEstimator): def fit(self, X, y): return self def predict(self, X): return X['lag1'] ``` ```python theme={null} fcst = MLForecast( models=[Naive()], freq=1, lags=[1], target_transforms=[LocalStandardScaler()] ) fcst.fit(df) preds = fcst.predict(1) preds ``` | | unique\_id | ds | Naive | | - | ---------- | ---- | ----- | | 0 | H196 | 1009 | 16.8 | | 1 | H256 | 1009 | 13.4 | | 2 | H381 | 1009 | 207.0 | | 3 | H413 | 1009 | 34.0 | We compare this with the last values of our series ```python theme={null} last_vals = df.groupby('unique_id').tail(1) last_vals ``` | | unique\_id | ds | y | | ---- | ---------- | ---- | ----- | | 1007 | H196 | 1008 | 16.8 | | 2015 | H256 | 1008 | 13.4 | | 3023 | H381 | 1008 | 207.0 | | 4031 | H413 | 1008 | 34.0 | ```python theme={null} import numpy as np ``` ```python theme={null} np.testing.assert_allclose(preds['Naive'], last_vals['y']) ``` ## Training Once you’ve decided the features, transformations and models that you want to use you can use the `MLForecast.fit` method instead, which will do the preprocessing and then train the models. The models can be specified as a list (which will name them by using their class name and an index if there are repeated classes) or as a dictionary where the keys are the names you want to give to the models, i.e. the name of the column that will hold their predictions, and the values are the models themselves. ```python theme={null} import lightgbm as lgb ``` ```python theme={null} lgb_params = { 'verbosity': -1, 'num_leaves': 512, } fcst = MLForecast( models={ 'avg': lgb.LGBMRegressor(**lgb_params), 'q75': lgb.LGBMRegressor(**lgb_params, objective='quantile', alpha=0.75), 'q25': lgb.LGBMRegressor(**lgb_params, objective='quantile', alpha=0.25), }, freq=1, target_transforms=[Differences([24])], lags=[1, 24], lag_transforms={ 1: [ExpandingMean()], 24: [RollingMean(window_size=48)], }, date_features=[hour_index], ) fcst.fit(df) ``` ```text theme={null} MLForecast(models=[avg, q75, q25], freq=1, lag_features=['lag1', 'lag24', 'expanding_mean_lag1', 'rolling_mean_lag24_window_size48'], date_features=[], num_threads=1) ``` This computed the features and trained three different models using them. We can now compute our forecasts. ## Forecasting ```python theme={null} preds = fcst.predict(48) preds ``` | | unique\_id | ds | avg | q75 | q25 | | --- | ---------- | ---- | ---------- | ---------- | --------- | | 0 | H196 | 1009 | 16.295257 | 16.357148 | 16.315731 | | 1 | H196 | 1010 | 15.910282 | 16.007322 | 15.862261 | | 2 | H196 | 1011 | 15.728367 | 15.780183 | 15.658180 | | 3 | H196 | 1012 | 15.468414 | 15.513598 | 15.399717 | | 4 | H196 | 1013 | 15.081279 | 15.133848 | 15.007694 | | ... | ... | ... | ... | ... | ... | | 187 | H413 | 1052 | 100.450617 | 124.211150 | 47.025017 | | 188 | H413 | 1053 | 88.426800 | 108.303409 | 44.715380 | | 189 | H413 | 1054 | 59.675737 | 81.859964 | 19.239462 | | 190 | H413 | 1055 | 57.580356 | 72.703301 | 21.486674 | | 191 | H413 | 1056 | 42.669879 | 46.018271 | 24.392357 | ```python theme={null} fig = plot_series(df, preds, max_insample_length=24 * 7) ``` ## Saving and loading The MLForecast class has the `MLForecast.save` and `MLForecast.load` to store and then load the forecast object. ```python theme={null} with tempfile.TemporaryDirectory() as tmpdir: save_dir = Path(tmpdir) / 'mlforecast' fcst.save(save_dir) fcst2 = MLForecast.load(save_dir) preds2 = fcst2.predict(48) pd.testing.assert_frame_equal(preds, preds2) ``` ## Updating series’ values After you’ve trained a forecast object you can save and load it with the previous methods. If by the time you want to use it you already know the following values of the target you can use the `MLForecast.update` method to incorporate these, which will allow you to use these new values when computing predictions. * If no new values are provided for a series that’s currently stored, only the previous ones are kept. * If new series are included they are added to the existing ones. ```python theme={null} fcst = MLForecast( models=[Naive()], freq=1, lags=[1, 2, 3], ) fcst.fit(df) fcst.predict(1) ``` | | unique\_id | ds | Naive | | - | ---------- | ---- | ----- | | 0 | H196 | 1009 | 16.8 | | 1 | H256 | 1009 | 13.4 | | 2 | H381 | 1009 | 207.0 | | 3 | H413 | 1009 | 34.0 | ```python theme={null} new_values = pd.DataFrame({ 'unique_id': ['H196', 'H256'], 'ds': [1009, 1009], 'y': [17.0, 14.0], }) fcst.update(new_values) preds = fcst.predict(1) preds ``` | | unique\_id | ds | Naive | | - | ---------- | ---- | ----- | | 0 | H196 | 1010 | 17.0 | | 1 | H256 | 1010 | 14.0 | | 2 | H381 | 1009 | 207.0 | | 3 | H413 | 1009 | 34.0 | ## Estimating model performance ### Cross validation In order to get an estimate of how well our model will be when predicting future data we can perform cross validation, which consists of training a few models independently on different subsets of the data, using them to predict a validation set and measuring their performance. Since our data depends on time, we make our splits by removing the last portions of the series and using them as validation sets. This process is implemented in `MLForecast.cross_validation`. ```python theme={null} fcst = MLForecast( models=lgb.LGBMRegressor(**lgb_params), freq=1, target_transforms=[Differences([24])], lags=[1, 24], lag_transforms={ 1: [ExpandingMean()], 24: [RollingMean(window_size=48)], }, date_features=[hour_index], ) cv_result = fcst.cross_validation( df, n_windows=4, # number of models to train/splits to perform h=48, # length of the validation set in each window ) cv_result ``` | | unique\_id | ds | cutoff | y | LGBMRegressor | | --- | ---------- | ---- | ------ | ---- | ------------- | | 0 | H196 | 817 | 816 | 15.3 | 15.383165 | | 1 | H196 | 818 | 816 | 14.9 | 14.923219 | | 2 | H196 | 819 | 816 | 14.6 | 14.667834 | | 3 | H196 | 820 | 816 | 14.2 | 14.275964 | | 4 | H196 | 821 | 816 | 13.9 | 13.973491 | | ... | ... | ... | ... | ... | ... | | 763 | H413 | 1004 | 960 | 99.0 | 65.644823 | | 764 | H413 | 1005 | 960 | 88.0 | 71.717097 | | 765 | H413 | 1006 | 960 | 47.0 | 76.704377 | | 766 | H413 | 1007 | 960 | 41.0 | 53.446638 | | 767 | H413 | 1008 | 960 | 34.0 | 54.902634 | ```python theme={null} fig = plot_series(forecasts_df=cv_result.drop(columns='cutoff')) ``` We can compute the RMSE on each split. ```python theme={null} from utilsforecast.losses import rmse ``` ```python theme={null} def evaluate_cv(df): return rmse(df, models=['LGBMRegressor'], id_col='cutoff').set_index('cutoff') split_rmse = evaluate_cv(cv_result) split_rmse ``` | | LGBMRegressor | | ------ | ------------- | | cutoff | | | 816 | 29.418172 | | 864 | 34.257598 | | 912 | 13.145763 | | 960 | 35.066261 | And the average RMSE across splits. ```python theme={null} split_rmse.mean() ``` ```text theme={null} LGBMRegressor 27.971949 dtype: float64 ``` You can quickly try different features and evaluate them this way. We can try removing the differencing and using an exponentially weighted average of the lag 1 instead of the expanding mean. ```python theme={null} from mlforecast.lag_transforms import ExponentiallyWeightedMean ``` ```python theme={null} fcst = MLForecast( models=lgb.LGBMRegressor(**lgb_params), freq=1, lags=[1, 24], lag_transforms={ 1: [ExponentiallyWeightedMean(alpha=0.5)], 24: [RollingMean(window_size=48)], }, date_features=[hour_index], ) cv_result2 = fcst.cross_validation( df, n_windows=4, h=48, ) evaluate_cv(cv_result2).mean() ``` ```text theme={null} LGBMRegressor 25.874439 dtype: float64 ``` ### LightGBMCV In the same spirit of estimating our model’s performance, `LightGBMCV` allows us to train a few [LightGBM](https://github.com/microsoft/LightGBM) models on different partitions of the data. The main differences with `MLForecast.cross_validation` are: * It can only train LightGBM models. * It trains all models **simultaneously** and gives us per-iteration averages of the errors across the complete forecasting window, which allows us to find the best iteration. ```python theme={null} from mlforecast.lgb_cv import LightGBMCV ``` ```python theme={null} cv = LightGBMCV( freq=1, target_transforms=[Differences([24])], lags=[1, 24], lag_transforms={ 1: [ExpandingMean()], 24: [RollingMean(window_size=48)], }, date_features=[hour_index], num_threads=2, ) cv_hist = cv.fit( df, n_windows=4, h=48, params=lgb_params, eval_every=5, early_stopping_evals=5, compute_cv_preds=True, ) ``` ```text theme={null} [5] mape: 0.158639 [10] mape: 0.163739 [15] mape: 0.161535 [20] mape: 0.169491 [25] mape: 0.163690 [30] mape: 0.164198 Early stopping at round 30 Using best iteration: 5 ``` As you can see this gives us the error by iteration (controlled by the `eval_every` argument) and performs early stopping (which can be configured with `early_stopping_evals` and `early_stopping_pct`). If you set `compute_cv_preds=True` the out-of-fold predictions are computed using the best iteration found and are saved in the `cv_preds_` attribute. ```python theme={null} cv.cv_preds_ ``` | | unique\_id | ds | y | Booster | window | | --- | ---------- | ---- | ---- | --------- | ------ | | 0 | H196 | 817 | 15.3 | 15.473182 | 0 | | 1 | H196 | 818 | 14.9 | 15.038571 | 0 | | 2 | H196 | 819 | 14.6 | 14.849409 | 0 | | 3 | H196 | 820 | 14.2 | 14.448379 | 0 | | 4 | H196 | 821 | 13.9 | 14.148379 | 0 | | ... | ... | ... | ... | ... | ... | | 187 | H413 | 1004 | 99.0 | 61.425396 | 3 | | 188 | H413 | 1005 | 88.0 | 62.886890 | 3 | | 189 | H413 | 1006 | 47.0 | 57.886890 | 3 | | 190 | H413 | 1007 | 41.0 | 38.849009 | 3 | | 191 | H413 | 1008 | 34.0 | 44.720562 | 3 | ```python theme={null} fig = plot_series(forecasts_df=cv.cv_preds_.drop(columns='window')) ``` You can use this class to quickly try different configurations of features and hyperparameters. Once you’ve found a combination that works you can train a model with those features and hyperparameters on all the data by creating an `MLForecast` object from the `LightGBMCV` one as follows: ```python theme={null} final_fcst = MLForecast.from_cv(cv) final_fcst.fit(df) preds = final_fcst.predict(48) fig = plot_series(df, preds, max_insample_length=24 * 14) ``` # Install | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/docs/getting-started/install.html > Instructions to install the package from different sources. ## Released versions ### PyPI #### Latest release To install the latest release of mlforecast from [PyPI](https://pypi.org/project/mlforecast/) you just have to run the following in a terminal: `pip install mlforecast` #### Specific version If you want a specific version you can include a filter, for example: * `pip install "mlforecast==0.3.0"` to install the 0.3.0 version * `pip install "mlforecast<0.4.0"` to install any version prior to 0.4.0 #### Extras **polars** Using polars dataframes: `pip install "mlforecast[polars]"` **Saving to remote storages** If you want to save your forecast artifacts to a remote storage like S3 or GCS you can use the following extras: * Saving to S3: `pip install "mlforecast[aws]"` * Saving to Google Cloud Storage: `pip install "mlforecast[gcp]"` * Saving to Azure Data Lake: `pip install "mlforecast[azure]"` **Distributed training** If you want to perform distributed training you can use either dask, ray or spark. Once you know which framework you want to use you can include its extra: * dask: `pip install "mlforecast[dask]"` * ray: `pip install "mlforecast[ray]"` * spark: `pip install "mlforecast[spark]"` ### Conda #### Latest release The mlforecast package is also published to [conda-forge](https://anaconda.org/conda-forge/mlforecast), which you can install by running the following in a terminal: `conda install -c conda-forge mlforecast` Note that this happens about a day later after it is published to PyPI, so you may have to wait to get the latest release. #### Specific version If you want a specific version you can include a filter, for example: * `conda install -c conda-forge "mlforecast==0.3.0"` to install the 0.3.0 version * `conda install -c conda-forge "mlforecast<0.4.0"` to install any version prior to 0.4.0 ## Development version If you want to try out a new feature that hasn’t made it into a release yet you have the following options: * Install from github: `pip install git+https://github.com/Nixtla/mlforecast` * Clone and install: `git clone https://github.com/Nixtla/mlforecast mlforecast-dev && pip install mlforecast-dev/`, which will install the version from the current main branch. # Quick start (distributed) Source: https://nixtlaverse.nixtla.io/mlforecast/docs/getting-started/quick_start_distributed.html > Minimal example of distributed training with MLForecast The `DistributedMLForecast` class is a high level abstraction that encapsulates all the steps in the pipeline (preprocessing, fitting the model and computing predictions) and applies them in a distributed way. The different things that you need to use `DistributedMLForecast` (as opposed to `MLForecast`) are: 1. You need to set up a cluster. We currently support dask, ray and spark. 2. Your data needs to be a distributed collection (dask, ray or spark dataframe). 3. You need to use a model that implements distributed training in your framework of choice, e.g. SynapseML for LightGBM in spark. ```python theme={null} import platform import sys import tempfile import matplotlib.pyplot as plt import git import numpy as np import pandas as pd import s3fs from sklearn.base import BaseEstimator from utilsforecast.feature_engineering import fourier from mlforecast.distributed import DistributedMLForecast from mlforecast.lag_transforms import ExpandingMean, ExponentiallyWeightedMean, RollingMean from mlforecast.target_transforms import Differences from mlforecast.utils import generate_daily_series, generate_prices_for_series ``` ## Dask ```python theme={null} import dask.dataframe as dd from dask.distributed import Client ``` ### Client setup ```python theme={null} client = Client(n_workers=2, threads_per_worker=1) ``` Here we define a client that connects to a `dask.distributed.LocalCluster`, however it could be any other kind of cluster. ### Data setup For dask, the data must be a `dask.dataframe.DataFrame`. You need to make sure that each time series is only in one partition and it is recommended that you have as many partitions as you have workers. If you have more partitions than workers make sure to set `num_threads=1` to avoid having nested parallelism. The required input format is the same as for `MLForecast`, except that it’s a `dask.dataframe.DataFrame` instead of a `pandas.Dataframe`. ```python theme={null} series = generate_daily_series(100, n_static_features=2, equal_ends=True, static_as_categorical=False, min_length=500, max_length=1_000) train, future = fourier(series, freq='d', season_length=7, k=2, h=7) npartitions = 10 partitioned_series = dd.from_pandas(train.set_index('unique_id'), npartitions=npartitions) # make sure we split by the id_col partitioned_series = partitioned_series.map_partitions(lambda df: df.reset_index()) partitioned_series['unique_id'] = partitioned_series['unique_id'].astype(str) # can't handle categoricals atm partitioned_series ``` | | unique\_id | ds | y | static\_0 | static\_1 | sin1\_7 | sin2\_7 | cos1\_7 | cos2\_7 | | -------------- | ---------- | --------------- | ------- | --------- | --------- | ------- | ------- | ------- | ------- | | npartitions=10 | | | | | | | | | | | id\_00 | object | datetime64\[ns] | float64 | int64 | int64 | float32 | float32 | float32 | float32 | | id\_10 | ... | ... | ... | ... | ... | ... | ... | ... | ... | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | id\_90 | ... | ... | ... | ... | ... | ... | ... | ... | ... | | id\_99 | ... | ... | ... | ... | ... | ... | ... | ... | ... | ### Models In order to perform distributed forecasting, we need to use a model that is able to train in a distributed way using `dask`. The current implementations are in `DaskLGBMForecast` and `DaskXGBForecast` which are just wrappers around the native implementations. ```python theme={null} from mlforecast.distributed.models.dask.lgb import DaskLGBMForecast from mlforecast.distributed.models.dask.xgb import DaskXGBForecast ``` ```python theme={null} models = [ DaskXGBForecast(random_state=0), DaskLGBMForecast(random_state=0, verbosity=-1), ] ``` ### Training Once we have our models we instantiate a `DistributedMLForecast` object defining our features. We can then call `fit` on this object passing our dask dataframe. ```python theme={null} fcst = DistributedMLForecast( models=models, freq='D', target_transforms=[Differences([7])], lags=[7], lag_transforms={ 1: [ExpandingMean(), ExponentiallyWeightedMean(alpha=0.9)], 7: [RollingMean(window_size=14)], }, date_features=['dayofweek', 'month'], num_threads=1, engine=client, ) fcst.fit(partitioned_series, static_features=['static_0', 'static_1']) ``` Once we have our fitted models we can compute the predictions for the next 7 timesteps. ### Forecasting ```python theme={null} preds = fcst.predict(7, X_df=future).compute() preds.head() ``` | | unique\_id | ds | DaskXGBForecast | DaskLGBMForecast | | - | ---------- | ------------------- | --------------- | ---------------- | | 0 | id\_00 | 2002-09-27 00:00:00 | 21.722841 | 21.725511 | | 1 | id\_00 | 2002-09-28 00:00:00 | 84.918194 | 84.606362 | | 2 | id\_00 | 2002-09-29 00:00:00 | 162.067624 | 163.36802 | | 3 | id\_00 | 2002-09-30 00:00:00 | 249.001477 | 246.422894 | | 4 | id\_00 | 2002-10-01 00:00:00 | 317.149512 | 315.538403 | ```python theme={null} preds2 = fcst.predict(7, X_df=future).compute() preds3 = fcst.predict(7, new_df=partitioned_series, X_df=future).compute() pd.testing.assert_frame_equal(preds, preds2) pd.testing.assert_frame_equal(preds, preds3) ``` ### Saving and loading Once you’ve trained your model you can use the `DistributedMLForecast.save` method to save the artifacts for inference. Keep in mind that if you’re on a remote cluster you should set a remote storage like S3 as the destination. mlforecast uses [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) to handle the different filesystems, so if you’re using s3 for example you also need to install [s3fs](https://s3fs.readthedocs.io/en/latest/). If you’re using pip you can just include the aws extra, e.g. `pip install 'mlforecast[aws,dask]'`, which will install the required dependencies to perform distributed training with dask and saving to S3. If you’re using conda you’ll have to manually install them (`conda install dask fsspec fugue s3fs`). ```python theme={null} # define unique name for CI def build_unique_name(engine): pyver = f'{sys.version_info.major}_{sys.version_info.minor}' repo = git.Repo(search_parent_directories=True) sha = repo.head.object.hexsha return f'{sys.platform}-{pyver}-{engine}-{sha}' ``` ```python theme={null} save_dir = build_unique_name('dask') save_path = f's3://nixtla-tmp/mlf/{save_dir}' tmpdir = tempfile.TemporaryDirectory() try: s3fs.S3FileSystem().ls('s3://nixtla-tmp/') fcst.save(save_path) except Exception as e: print(e) save_path = f'{tmpdir.name}/{save_dir}' fcst.save(save_path) ``` Once you’ve saved your forecast object you can then load it back by specifying the path where it was saved along with an engine, which will be used to perform the distributed computations (in this case the dask client). ```python theme={null} fcst2 = DistributedMLForecast.load(save_path, engine=client) ``` We can verify that this object produces the same results. ```python theme={null} preds = fa.as_pandas(fcst.predict(7, X_df=future)).sort_values(['unique_id', 'ds']).reset_index(drop=True) preds2 = fa.as_pandas(fcst2.predict(7, X_df=future)).sort_values(['unique_id', 'ds']).reset_index(drop=True) pd.testing.assert_frame_equal(preds, preds2) ``` ### Converting to local Another option to store your distributed forecast object is to first turn it into a local one and then save it. Keep in mind that in order to do that all the remote data that is stored from the series will have to be pulled into a single machine (the scheduler in dask, driver in spark, etc.), so you have to be sure that it’ll fit in memory, it should consume about 2x the size of your target column (you can reduce this further by using the `keep_last_n` argument in the `fit` method). ```python theme={null} local_fcst = fcst.to_local() local_preds = local_fcst.predict(7, X_df=future) # we don't check the dtype because sometimes these are arrow dtypes # or different precisions of float pd.testing.assert_frame_equal(preds, local_preds, check_dtype=False) ``` ### Cross validation ```python theme={null} cv_res = fcst.cross_validation( partitioned_series, n_windows=3, h=14, static_features=['static_0', 'static_1'], ) ``` ```python theme={null} cv_res.compute().head() ``` | | unique\_id | ds | DaskXGBForecast | DaskLGBMForecast | cutoff | y | | --- | ---------- | ------------------- | --------------- | ---------------- | ------------------- | ---------- | | 61 | id\_04 | 2002-08-21 00:00:00 | 68.3418 | 68.944539 | 2002-08-15 00:00:00 | 69.699857 | | 83 | id\_15 | 2002-08-29 00:00:00 | 199.315403 | 199.663555 | 2002-08-15 00:00:00 | 206.082864 | | 103 | id\_17 | 2002-08-21 00:00:00 | 156.822598 | 158.018246 | 2002-08-15 00:00:00 | 152.227984 | | 61 | id\_24 | 2002-08-21 00:00:00 | 136.598356 | 136.576865 | 2002-08-15 00:00:00 | 138.559945 | | 36 | id\_33 | 2002-08-24 00:00:00 | 95.6072 | 96.249354 | 2002-08-15 00:00:00 | 102.068997 | ```python theme={null} non_std_series = partitioned_series.copy() non_std_series = non_std_series.rename(columns={'ds': 'time', 'y': 'value', 'unique_id': 'some_id'}) flow_params = dict( models=[DaskXGBForecast(random_state=0)], target_transforms=[Differences([7])], lags=[7], lag_transforms={ 1: [ExpandingMean()], 7: [RollingMean(window_size=14)] }, num_threads=1, ) fcst = DistributedMLForecast(freq='D', **flow_params) fcst.fit(partitioned_series, static_features=['static_0', 'static_1']) preds = fcst.predict(7, X_df=future).compute() fcst2 = DistributedMLForecast(freq='D', **flow_params) fcst2.preprocess( non_std_series, id_col='some_id', time_col='time', target_col='value', static_features=['static_0', 'static_1'], ) fcst2.models_ = fcst.models_ # distributed training can end up with different fits non_std_preds = fcst2.predict(7, X_df=future.rename(columns={'ds': 'time', 'unique_id': 'some_id'})).compute() pd.testing.assert_frame_equal( preds.drop(columns='ds'), non_std_preds.drop(columns='time').rename(columns={'some_id': 'unique_id'}) ) ``` ```python theme={null} client.close() ``` ## Spark ### Session setup ```python theme={null} from pyspark.sql import SparkSession ``` ```python theme={null} spark = ( SparkSession .builder .config("spark.jars.packages", "com.microsoft.azure:synapseml_2.12:0.10.2") .config("spark.jars.repositories", "https://mmlspark.azureedge.net/maven") .getOrCreate() ) ``` ### Data setup For spark, the data must be a `pyspark DataFrame`. You need to make sure that each time series is only in one partition (which you can do using `repartitionByRange`, for example) and it is recommended that you have as many partitions as you have workers. If you have more partitions than workers make sure to set `num_threads=1` to avoid having nested parallelism. The required input format is the same as for `MLForecast`, i.e. it should have at least an id column, a time column and a target column. ```python theme={null} series = generate_daily_series(100, n_static_features=2, equal_ends=True, static_as_categorical=False, min_length=500, max_length=1_000) series['unique_id'] = series['unique_id'].astype(str) # can't handle categoricals atm train, future = fourier(series, freq='d', season_length=7, k=2, h=7) numPartitions = 4 spark_series = spark.createDataFrame(train).repartitionByRange(numPartitions, 'unique_id') ``` ### Models In order to perform distributed forecasting, we need to use a model that is able to train in a distributed way using `spark`. The current implementations are in `SparkLGBMForecast` and `SparkXGBForecast` which are just wrappers around the native implementations. ```python theme={null} from mlforecast.distributed.models.spark.lgb import SparkLGBMForecast from mlforecast.distributed.models.spark.xgb import SparkXGBForecast ``` ```python theme={null} models = [ SparkLGBMForecast(seed=0, verbosity=-1), SparkXGBForecast(random_state=0), ] ``` ### Training ```python theme={null} fcst = DistributedMLForecast( models, freq='D', target_transforms=[Differences([7])], lags=[1], lag_transforms={ 1: [ExpandingMean(), ExponentiallyWeightedMean(alpha=0.9)], }, date_features=['dayofweek'], ) fcst.fit( spark_series, static_features=['static_0', 'static_1'], ) ``` ### Forecasting ```python theme={null} preds = fcst.predict(7, X_df=future).toPandas() ``` ```text theme={null} ``` ```python theme={null} preds.head() ``` | | unique\_id | ds | SparkLGBMForecast | SparkXGBForecast | | - | ---------- | ---------- | ----------------- | ---------------- | | 0 | id\_00 | 2002-09-27 | 15.053577 | 18.631477 | | 1 | id\_00 | 2002-09-28 | 93.010037 | 93.796269 | | 2 | id\_00 | 2002-09-29 | 160.120148 | 159.582315 | | 3 | id\_00 | 2002-09-30 | 250.445885 | 250.861651 | | 4 | id\_00 | 2002-10-01 | 323.335956 | 321.564089 | ### Saving and loading Once you’ve trained your model you can use the `DistributedMLForecast.save` method to save the artifacts for inference. Keep in mind that if you’re on a remote cluster you should set a remote storage like S3 as the destination. mlforecast uses [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) to handle the different filesystems, so if you’re using s3 for example you also need to install [s3fs](https://s3fs.readthedocs.io/en/latest/). If you’re using pip you can just include the aws extra, e.g. `pip install 'mlforecast[aws,spark]'`, which will install the required dependencies to perform distributed training with spark and saving to S3. If you’re using conda you’ll have to manually install them (`conda install fsspec fugue pyspark s3fs`). ```python theme={null} save_dir = build_unique_name('spark') save_path = f's3://nixtla-tmp/mlf/{save_dir}' try: s3fs.S3FileSystem().ls('s3://nixtla-tmp/') fcst.save(save_path) except Exception as e: print(e) save_path = f'{tmpdir.name}/{save_dir}' fcst.save(save_path) ``` ```text theme={null} ``` Once you’ve saved your forecast object you can then load it back by specifying the path where it was saved along with an engine, which will be used to perform the distributed computations (in this case the spark session). ```python theme={null} fcst2 = DistributedMLForecast.load(save_path, engine=spark) ``` ```text theme={null} ``` We can verify that this object produces the same results. ```python theme={null} preds = fa.as_pandas(fcst.predict(7, X_df=future)).sort_values(['unique_id', 'ds']).reset_index(drop=True) preds2 = fa.as_pandas(fcst2.predict(7, X_df=future)).sort_values(['unique_id', 'ds']).reset_index(drop=True) pd.testing.assert_frame_equal(preds, preds2) ``` ```text theme={null} ``` ### Converting to local Another option to store your distributed forecast object is to first turn it into a local one and then save it. Keep in mind that in order to do that all the remote data that is stored from the series will have to be pulled into a single machine (the scheduler in dask, driver in spark, etc.), so you have to be sure that it’ll fit in memory, it should consume about 2x the size of your target column (you can reduce this further by using the `keep_last_n` argument in the `fit` method). ```python theme={null} local_fcst = fcst.to_local() local_preds = local_fcst.predict(7, X_df=future) # we don't check the dtype because sometimes these are arrow dtypes # or different precisions of float pd.testing.assert_frame_equal(preds, local_preds, check_dtype=False) ``` ### Cross validation ```python theme={null} cv_res = fcst.cross_validation( spark_series, n_windows=3, h=14, static_features=['static_0', 'static_1'], ).toPandas() ``` ```python theme={null} cv_res.head() ``` | | unique\_id | ds | SparkLGBMForecast | SparkXGBForecast | cutoff | y | | - | ---------- | ---------- | ----------------- | ---------------- | ---------- | ---------- | | 0 | id\_03 | 2002-08-18 | 3.272922 | 3.348874 | 2002-08-15 | 3.060194 | | 1 | id\_09 | 2002-08-20 | 402.718091 | 402.622501 | 2002-08-15 | 398.784459 | | 2 | id\_25 | 2002-08-22 | 87.189811 | 86.891632 | 2002-08-15 | 82.731377 | | 3 | id\_06 | 2002-08-21 | 20.416790 | 20.478502 | 2002-08-15 | 19.196394 | | 4 | id\_22 | 2002-08-23 | 357.718513 | 360.502024 | 2002-08-15 | 394.770699 | ```python theme={null} spark.stop() ``` ## Ray ### Session setup ```python theme={null} # ray import ray from ray.cluster_utils import Cluster ``` ```python theme={null} # ray ray_cluster = Cluster( initialize_head=True, head_node_args={"num_cpus": 2} ) ray.init(address=ray_cluster.address, ignore_reinit_error=True) # add mock node to simulate a cluster mock_node = ray_cluster.add_node(num_cpus=2) ``` ### Data setup For ray, the data must be a `ray DataFrame`. It is recommended that you have as many partitions as you have workers. If you have more partitions than workers make sure to set `num_threads=1` to avoid having nested parallelism. The required input format is the same as for `MLForecast`, i.e. it should have at least an id column, a time column and a target column. ```python theme={null} # ray series = generate_daily_series(100, n_static_features=2, equal_ends=True, static_as_categorical=False, min_length=500, max_length=1_000) series['unique_id'] = series['unique_id'].astype(str) # can't handle categoricals atm train, future = fourier(series, freq='d', season_length=7, k=2, h=7) ray_series = ray.data.from_pandas(train) ``` ### Models The ray integration allows to include `lightgbm` (`RayLGBMRegressor`), and `xgboost` (`RayXGBRegressor`). ```python theme={null} # ray from mlforecast.distributed.models.ray.lgb import RayLGBMForecast from mlforecast.distributed.models.ray.xgb import RayXGBForecast ``` ```python theme={null} # ray models = [ RayLGBMForecast(random_state=0, verbosity=-1), RayXGBForecast(random_state=0), ] ``` ### Training To control the number of partitions to use using Ray, we have to include `num_partitions` to `DistributedMLForecast`. ```python theme={null} # ray num_partitions = 4 fcst = DistributedMLForecast( models, freq='D', target_transforms=[Differences([7])], lags=[1], lag_transforms={ 1: [ExpandingMean(), ExponentiallyWeightedMean(alpha=0.9)], }, date_features=['dayofweek'], num_partitions=num_partitions, # Use num_partitions to reduce overhead ) fcst.fit( ray_series, static_features=['static_0', 'static_1'], ) ``` ### Forecasting ```python theme={null} # ray preds = fcst.predict(7, X_df=future).to_pandas() ``` ```python theme={null} # ray preds.head() ``` | | unique\_id | ds | RayLGBMForecast | RayXGBForecast | | - | ---------- | ---------- | --------------- | -------------- | | 0 | id\_00 | 2002-09-27 | 15.232455 | 10.38301 | | 1 | id\_00 | 2002-09-28 | 92.288994 | 92.531502 | | 2 | id\_00 | 2002-09-29 | 160.043472 | 160.722885 | | 3 | id\_00 | 2002-09-30 | 250.03212 | 252.821899 | | 4 | id\_00 | 2002-10-01 | 322.905182 | 324.387695 | ### Saving and loading Once you’ve trained your model you can use the `DistributedMLForecast.save` method to save the artifacts for inference. Keep in mind that if you’re on a remote cluster you should set a remote storage like S3 as the destination. mlforecast uses [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) to handle the different filesystems, so if you’re using s3 for example you also need to install [s3fs](https://s3fs.readthedocs.io/en/latest/). If you’re using pip you can just include the aws extra, e.g. `pip install 'mlforecast[aws,ray]'`, which will install the required dependencies to perform distributed training with ray and saving to S3. If you’re using conda you’ll have to manually install them (`conda install fsspec fugue ray s3fs`). ```python theme={null} # ray save_dir = build_unique_name('ray') save_path = f's3://nixtla-tmp/mlf/{save_dir}' try: s3fs.S3FileSystem().ls('s3://nixtla-tmp/') fcst.save(save_path) except Exception as e: print(e) save_path = f'{tmpdir.name}/{save_dir}' fcst.save(save_path) ``` Once you’ve saved your forecast object you can then load it back by specifying the path where it was saved along with an engine, which will be used to perform the distributed computations (in this case the ‘ray’ string). ```python theme={null} # ray fcst2 = DistributedMLForecast.load(save_path, engine='ray') ``` We can verify that this object produces the same results. ```python theme={null} # ray preds = fa.as_pandas(fcst.predict(7, X_df=future)).sort_values(['unique_id', 'ds']).reset_index(drop=True) preds2 = fa.as_pandas(fcst2.predict(7, X_df=future)).sort_values(['unique_id', 'ds']).reset_index(drop=True) pd.testing.assert_frame_equal(preds, preds2) ``` ### Converting to local Another option to store your distributed forecast object is to first turn it into a local one and then save it. Keep in mind that in order to do that all the remote data that is stored from the series will have to be pulled into a single machine (the scheduler in dask, driver in spark, etc.), so you have to be sure that it’ll fit in memory, it should consume about 2x the size of your target column (you can reduce this further by using the `keep_last_n` argument in the `fit` method). ```python theme={null} # ray local_fcst = fcst.to_local() local_preds = local_fcst.predict(7, X_df=future) # we don't check the dtype because sometimes these are arrow dtypes # or different precisions of float pd.testing.assert_frame_equal(preds, local_preds, check_dtype=False) ``` ### Cross validation ```python theme={null} # ray cv_res = fcst.cross_validation( ray_series, n_windows=3, h=14, static_features=['static_0', 'static_1'], ).to_pandas() ``` ```python theme={null} # ray cv_res.head() ``` | | unique\_id | ds | RayLGBMForecast | RayXGBForecast | cutoff | y | | - | ---------- | ---------- | --------------- | -------------- | ---------- | ---------- | | 0 | id\_05 | 2002-09-21 | 108.285187 | 108.619698 | 2002-09-12 | 108.726387 | | 1 | id\_08 | 2002-09-16 | 26.287956 | 26.589603 | 2002-09-12 | 27.980670 | | 2 | id\_08 | 2002-09-25 | 83.210945 | 84.194962 | 2002-09-12 | 86.344885 | | 3 | id\_11 | 2002-09-22 | 416.994843 | 417.106506 | 2002-09-12 | 425.434661 | | 4 | id\_16 | 2002-09-14 | 377.916382 | 375.421600 | 2002-09-12 | 400.361977 | ```python theme={null} # ray ray.shutdown() ``` # Quick start (local) Source: https://nixtlaverse.nixtla.io/mlforecast/docs/getting-started/quick_start_local.html > Minimal example of MLForecast ## Main concepts The main component of mlforecast is the `MLForecast` class, which abstracts away: * Feature engineering and model training through `MLForecast.fit` * Feature updates and multi step ahead predictions through `MLForecast.predict` ## Data format The data is expected to be a pandas dataframe in long format, that is, each row represents an observation of a single series at a given time, with at least three columns: * `id_col`: column that identifies each series. * `target_col`: column that has the series values at each timestamp. * `time_col`: column that contains the time the series value was observed. These are usually timestamps, but can also be consecutive integers. Here we present an example using the classic Box & Jenkins airline data, which measures monthly totals of international airline passengers from 1949 to 1960 \[1]. ```python theme={null} import pandas as pd from utilsforecast.plotting import plot_series ``` ```python theme={null} df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/air-passengers.csv', parse_dates=['ds']) df.head() ``` | | unique\_id | ds | y | | - | ------------- | ---------- | --- | | 0 | AirPassengers | 1949-01-01 | 112 | | 1 | AirPassengers | 1949-02-01 | 118 | | 2 | AirPassengers | 1949-03-01 | 132 | | 3 | AirPassengers | 1949-04-01 | 129 | | 4 | AirPassengers | 1949-05-01 | 121 | ```python theme={null} df['unique_id'].value_counts() ``` ```text theme={null} AirPassengers 144 Name: unique_id, dtype: int64 ``` Here the `unique_id` column has the same value for all rows because this is a single time series, you can have multiple time series by stacking them together and having a column that differentiates them. We also have the `ds` column that contains the timestamps, in this case with a monthly frequency, and the `y` column that contains the series values in each timestamp. ## Modeling ```python theme={null} fig = plot_series(df) ``` We can see that the series has a clear trend, so we can take the first difference, i.e. take each value and subtract the value at the previous month. This can be achieved by passing an `mlforecast.target_transforms.Differences([1])` instance to `target_transforms`. We can then train a linear regression using the value from the same month at the previous year (lag 12) as a feature, this is done by passing `lags=[12]`. ```python theme={null} from mlforecast import MLForecast from mlforecast.target_transforms import Differences from sklearn.linear_model import LinearRegression ``` ```python theme={null} fcst = MLForecast( models=LinearRegression(), freq='MS', # our series has a monthly frequency lags=[12], target_transforms=[Differences([1])], ) fcst.fit(df) ``` ```text theme={null} MLForecast(models=[LinearRegression], freq=MS, lag_features=['lag12'], date_features=[], num_threads=1) ``` The previous line computed the features and trained the model, so now we’re ready to compute our forecasts. ## Forecasting Compute the forecast for the next 12 months ```python theme={null} preds = fcst.predict(12) preds ``` | | unique\_id | ds | LinearRegression | | -- | ------------- | ---------- | ---------------- | | 0 | AirPassengers | 1961-01-01 | 444.656555 | | 1 | AirPassengers | 1961-02-01 | 417.470734 | | 2 | AirPassengers | 1961-03-01 | 446.903046 | | 3 | AirPassengers | 1961-04-01 | 491.014130 | | 4 | AirPassengers | 1961-05-01 | 502.622223 | | 5 | AirPassengers | 1961-06-01 | 568.751465 | | 6 | AirPassengers | 1961-07-01 | 660.044312 | | 7 | AirPassengers | 1961-08-01 | 643.343323 | | 8 | AirPassengers | 1961-09-01 | 540.666687 | | 9 | AirPassengers | 1961-10-01 | 491.462708 | | 10 | AirPassengers | 1961-11-01 | 417.095154 | | 11 | AirPassengers | 1961-12-01 | 461.206238 | ## Visualize the results We can visualize what our prediction looks like. ```python theme={null} fig = plot_series(df, preds) ``` And that’s it! You’ve trained a linear regression to predict the air passengers for 1961. ## References \[1] Box, G. E. P., Jenkins, G. M. and Reinsel, G. C. (1976) Time Series Analysis, Forecasting and Control. Third Edition. Holden-Day. Series G. # Analyzing the trained models Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/analyzing_models.html > Access and interpret the models after fitting ## Data setup ```python theme={null} from mlforecast.utils import generate_daily_series ``` ```python theme={null} series = generate_daily_series(10) series.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | -------- | | 0 | id\_0 | 2000-01-01 | 0.322947 | | 1 | id\_0 | 2000-01-02 | 1.218794 | | 2 | id\_0 | 2000-01-03 | 2.445887 | | 3 | id\_0 | 2000-01-04 | 3.481831 | | 4 | id\_0 | 2000-01-05 | 4.191721 | ## Training Suppose that you want to train a linear regression model using the day of the week and lag1 as features. ```python theme={null} from sklearn.linear_model import LinearRegression from mlforecast import MLForecast ``` ```python theme={null} fcst = MLForecast( freq='D', models={'lr': LinearRegression()}, lags=[1], date_features=['dayofweek'], ) ``` ```python theme={null} fcst.fit(series) ``` ```text theme={null} MLForecast(models=[lr], freq=, lag_features=['lag1'], date_features=['dayofweek'], num_threads=1) ``` What `MLForecast.fit` does is save the required data for the predict step and also train the models (in this case the linear regression). The trained models are available in the `MLForecast.models_` attribute, which is a dictionary where the keys are the model names and the values are the model themselves. ```python theme={null} fcst.models_ ``` ```text theme={null} {'lr': LinearRegression()} ``` ## Inspect parameters We can access the linear regression coefficients in the following way: ```python theme={null} fcst.models_['lr'].intercept_, fcst.models_['lr'].coef_ ``` ```text theme={null} (3.2476337167384415, array([ 0.19896416, -0.21441331])) ``` ## SHAP ```python theme={null} import shap ``` ### Training set If you need to generate the training data you can use `MLForecast.preprocess`. ```python theme={null} prep = fcst.preprocess(series) prep.head() ``` | | unique\_id | ds | y | lag1 | dayofweek | | - | ---------- | ---------- | -------- | -------- | --------- | | 1 | id\_0 | 2000-01-02 | 1.218794 | 0.322947 | 6 | | 2 | id\_0 | 2000-01-03 | 2.445887 | 1.218794 | 0 | | 3 | id\_0 | 2000-01-04 | 3.481831 | 2.445887 | 1 | | 4 | id\_0 | 2000-01-05 | 4.191721 | 3.481831 | 2 | | 5 | id\_0 | 2000-01-06 | 5.395863 | 4.191721 | 3 | We extract the X, which involves dropping the info columns (id + times) and the target ```python theme={null} X = prep.drop(columns=['unique_id', 'ds', 'y']) X.head() ``` | | lag1 | dayofweek | | - | -------- | --------- | | 1 | 0.322947 | 6 | | 2 | 1.218794 | 0 | | 3 | 2.445887 | 1 | | 4 | 3.481831 | 2 | | 5 | 4.191721 | 3 | We can now compute the shap values ```python theme={null} X100 = shap.utils.sample(X, 100) explainer = shap.Explainer(fcst.models_['lr'].predict, X100) shap_values = explainer(X) ``` And visualize them ```python theme={null} shap.plots.beeswarm(shap_values) ``` ### Predictions Sometimes you want to determine why the model gave a specific prediction. In order to do this you need the input features, which aren’t returned by default, but you can retrieve them using a callback. ```python theme={null} from mlforecast.callbacks import SaveFeatures ``` ```python theme={null} save_feats = SaveFeatures() preds = fcst.predict(1, before_predict_callback=save_feats) preds.head() ``` | | unique\_id | ds | lr | | - | ---------- | ---------- | -------- | | 0 | id\_0 | 2000-08-10 | 3.468643 | | 1 | id\_1 | 2000-04-07 | 3.016877 | | 2 | id\_2 | 2000-06-16 | 2.815249 | | 3 | id\_3 | 2000-08-30 | 4.048894 | | 4 | id\_4 | 2001-01-08 | 3.524532 | You can now retrieve the features by using `SaveFeatures.get_features` ```python theme={null} features = save_feats.get_features() features.head() ``` | | lag1 | dayofweek | | - | -------- | --------- | | 0 | 4.343744 | 3 | | 1 | 3.150799 | 4 | | 2 | 2.137412 | 4 | | 3 | 6.182456 | 2 | | 4 | 1.391698 | 0 | And use those features to compute the shap values. ```python theme={null} shap_values_predictions = explainer(features) ``` We can now analyze what influenced the prediction for `'id_4'`. ```python theme={null} round(preds.loc[4, 'lr'], 3) ``` ```text theme={null} 3.525 ``` ```python theme={null} shap.plots.waterfall(shap_values_predictions[4]) ``` # Cross validation | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/cross_validation.html > In this example, we’ll implement time series cross-validation to > evaluate model’s performance. > **Prerequesites** > > This tutorial assumes basic familiarity with `MLForecast`. For a > minimal example visit the [Quick > Start](https://nixtlaverse.nixtla.io/mlforecast/docs/getting-started/quick_start_local.html) ## Introduction Time series cross-validation is a method for evaluating how a model would have performed in the past. It works by defining a sliding window across the historical data and predicting the period following it. ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) [MLForecast](https://nixtlaverse.nixtla.io/mlforecast/) has an implementation of time series cross-validation that is fast and easy to use. This implementation makes cross-validation an efficient operation, which makes it less time-consuming. In this notebook, we’ll use it on a subset of the [M4 Competition](https://www.sciencedirect.com/science/article/pii/S0169207019301128) hourly dataset. **Outline:** 1. Install libraries 2. Load and explore data 3. Train model 4. Perform time series cross-validation 5. Evaluate results > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Install libraries We assume that you have `MLForecast` already installed. If not, check this guide for instructions on [how to install MLForecast](https://nixtlaverse.nixtla.io/mlforecast/docs/getting-started/install.html) Install the necessary packages with `pip install mlforecast`. ```python theme={null} import pandas as pd from utilsforecast.plotting import plot_series from mlforecast import MLForecast # required to instantiate MLForecast object and use cross-validation method ``` ## Load and explore the data As stated in the introduction, we’ll use the M4 Competition hourly dataset. We’ll first import the data from an URL using `pandas`. ```python theme={null} Y_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.csv') # load the data Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | The input to `MLForecast` is a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int, or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestamp in format YYYY-MM-DD or YYYY-MM-DD HH:MM:SS. * The `y` (numeric) represents the measurement we wish to forecast. The data in this example already has this format, so no changes are needed. We can plot the time series we’ll work with using the following function. ```python theme={null} fig = plot_series(Y_df, max_ids=4, plot_random=False, max_insample_length=24 * 14) ``` ## Define forecast object For this example, we’ll use LightGBM. We first need to import it and then we need to instantiate a new [MLForecast](https://nixtlaverse.nixtla.io/mlforecast/forecast.html#mlforecast) object. In this example, we are only using `differences` and `lags` to produce features. See [the full documentation](https://nixtlaverse.nixtla.io/mlforecast/index.html) to see all available features. Any settings are passed into the constructor. Then you call its `fit` method and pass in the historical data frame `df`. ```python theme={null} import lightgbm as lgb from mlforecast.target_transforms import Differences ``` ```python theme={null} models = [lgb.LGBMRegressor(verbosity=-1)] mlf = MLForecast( models=models, freq=1,# our series have integer timestamps, so we'll just add 1 in every timestep, target_transforms=[Differences([24])], lags=range(1, 25) ) ``` ## Perform time series cross-validation Once the `MLForecast` object has been instantiated, we can use the [cross\_validation method](https://nixtlaverse.nixtla.io/mlforecast/forecast.html#mlforecast-cross_validation) For this particular example, we’ll use 3 windows of 24 hours. ```python theme={null} cv_df = mlf.cross_validation( df=Y_df, h=24, n_windows=3, ) ``` The crossvalidation\_df object is a new data frame that includes the following columns: * `unique_id`: identifies each time series. * `ds`: datestamp or temporal index. * `cutoff`: the last datestamp or temporal index for the `n_windows`. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_df.head() ``` | | unique\_id | ds | cutoff | y | LGBMRegressor | | - | ---------- | --- | ------ | ----- | ------------- | | 0 | H1 | 677 | 676 | 691.0 | 673.703191 | | 1 | H1 | 678 | 676 | 618.0 | 552.306270 | | 2 | H1 | 679 | 676 | 563.0 | 541.778027 | | 3 | H1 | 680 | 676 | 529.0 | 502.778027 | | 4 | H1 | 681 | 676 | 504.0 | 480.778027 | We’ll now plot the forecast for each cutoff period. ```python theme={null} import matplotlib.pyplot as plt ``` ```python theme={null} def plot_cv(df, df_cv, uid, fname, last_n=24 * 14): cutoffs = df_cv.query('unique_id == @uid')['cutoff'].unique() fig, ax = plt.subplots(nrows=len(cutoffs), ncols=1, figsize=(14, 6), gridspec_kw=dict(hspace=0.8)) for cutoff, axi in zip(cutoffs, ax.flat): df.query('unique_id == @uid').tail(last_n).set_index('ds').plot(ax=axi, title=uid, y='y') df_cv.query('unique_id == @uid & cutoff == @cutoff').set_index('ds').plot(ax=axi, title=uid, y='LGBMRegressor') fig.savefig(fname, bbox_inches='tight') plt.close() ``` ```python theme={null} plot_cv(Y_df, cv_df, 'H1', '../../figs/cross_validation__predictions.png') ``` Notice that in each cutoff period, we generated a forecast for the next 24 hours using only the data `y` before said period. ## Evaluate results We can now compute the accuracy of the forecast using an appropiate accuracy metric. Here we’ll use the [Root Mean Squared Error (RMSE).](https://en.wikipedia.org/wiki/Root-mean-square_deviation) To do this, we can use `utilsforecast`, a Python library developed by Nixtla that includes a function to compute the RMSE. ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import rmse ``` ```python theme={null} cv_rmse = evaluate( cv_df.drop(columns='cutoff'), metrics=[rmse], agg_fn='mean', ) print(f"RMSE using cross-validation: {cv_rmse['LGBMRegressor'].item():.1f}") ``` ```text theme={null} RMSE using cross-validation: 269.0 ``` This measure should better reflect the predictive abilities of our model, since it used different time periods to test its accuracy. ## References [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, Time series cross-validation”](https://otexts.com/fpp3/tscv.html). # Custom date features Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/custom_date_features.html > Define your own functions to be used as date features ```python theme={null} from mlforecast import MLForecast from mlforecast.utils import generate_daily_series ``` The `date_features` argument of MLForecast can take pandas date attributes as well as functions that take a [pandas DatetimeIndex](https://pandas.pydata.org/docs/reference/api/pandas.DatetimeIndex.html) and return a numeric value. The name of the function is used as the name of the feature, so please use unique and descriptive names. ```python theme={null} series = generate_daily_series(1, min_length=6, max_length=6) ``` ```python theme={null} def even_day(dates): """Day of month is even""" return dates.day % 2 == 0 def month_start_or_end(dates): """Date is month start or month end""" return dates.is_month_start | dates.is_month_end def is_monday(dates): """Date is monday""" return dates.dayofweek == 0 ``` ```python theme={null} fcst = MLForecast( [], freq='D', date_features=['dayofweek', 'dayofyear', even_day, month_start_or_end, is_monday] ) fcst.preprocess(series) ``` | | unique\_id | ds | y | dayofweek | dayofyear | even\_day | month\_start\_or\_end | is\_monday | | - | ---------- | ---------- | -------- | --------- | --------- | --------- | --------------------- | ---------- | | 0 | id\_0 | 2000-01-01 | 0.274407 | 5 | 1 | False | True | False | | 1 | id\_0 | 2000-01-02 | 1.357595 | 6 | 2 | True | False | False | | 2 | id\_0 | 2000-01-03 | 2.301382 | 0 | 3 | False | False | True | | 3 | id\_0 | 2000-01-04 | 3.272442 | 1 | 4 | True | False | False | | 4 | id\_0 | 2000-01-05 | 4.211827 | 2 | 5 | False | False | False | | 5 | id\_0 | 2000-01-06 | 5.322947 | 3 | 6 | True | False | False | # Custom training Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/custom_training.html > Customize the training procedure for your models mlforecast abstracts away most of the training details, which is useful for iterating quickly. However, sometimes you want more control over the fit parameters, the data that goes into the model, etc. This guide shows how you can train a model in a specific way and then giving it back to mlforecast to produce forecasts with it. ## Data setup ```python theme={null} from mlforecast.utils import generate_daily_series ``` ```python theme={null} series = generate_daily_series(5) ``` ## Creating forecast object ```python theme={null} import lightgbm as lgb import numpy as np from sklearn.linear_model import LinearRegression from mlforecast import MLForecast ``` Suppose we want to train a linear regression with the default settings. ```python theme={null} fcst = MLForecast( models={'lr': LinearRegression()}, freq='D', lags=[1], date_features=['dayofweek'], ) ``` ## Generate training set Use `MLForecast.preprocess` to generate the training data. ```python theme={null} prep = fcst.preprocess(series) prep.head() ``` | | unique\_id | ds | y | lag1 | dayofweek | | - | ---------- | ---------- | -------- | -------- | --------- | | 1 | id\_0 | 2000-01-02 | 1.423626 | 0.428973 | 6 | | 2 | id\_0 | 2000-01-03 | 2.311782 | 1.423626 | 0 | | 3 | id\_0 | 2000-01-04 | 3.192191 | 2.311782 | 1 | | 4 | id\_0 | 2000-01-05 | 4.148767 | 3.192191 | 2 | | 5 | id\_0 | 2000-01-06 | 5.028356 | 4.148767 | 3 | ```python theme={null} X = prep.drop(columns=['unique_id', 'ds', 'y']) y = prep['y'] ``` ## Regular training Since we don’t want to do anything special in our training process for the linear regression, we can just call `MLForecast.fit_models` ```python theme={null} fcst.fit_models(X, y) ``` ```text theme={null} MLForecast(models=[lr], freq=D, lag_features=['lag1'], date_features=['dayofweek'], num_threads=1) ``` This has trained the linear regression model and is now available in the `MLForecast.models_` attribute. ```python theme={null} fcst.models_ ``` ```text theme={null} {'lr': LinearRegression()} ``` ## Custom training Now suppose you also want to train a LightGBM model on the same data, but treating the day of the week as a categorical feature and logging the train loss. ```python theme={null} model = lgb.LGBMRegressor(n_estimators=100, verbosity=-1) model.fit( X, y, eval_set=[(X, y)], categorical_feature=['dayofweek'], callbacks=[lgb.log_evaluation(20)], ); ``` ```text theme={null} [20] training's l2: 0.0823528 [40] training's l2: 0.0230292 [60] training's l2: 0.0207829 [80] training's l2: 0.019675 [100] training's l2: 0.018778 ``` ## Computing forecasts Now we just assign this model to the `MLForecast.models_` dictionary. Note that you can assign as many models as you want. ```python theme={null} fcst.models_['lgbm'] = model fcst.models_ ``` ```text theme={null} {'lr': LinearRegression(), 'lgbm': LGBMRegressor(verbosity=-1)} ``` And now when calling `MLForecast.predict`, mlforecast will use those models to compute the forecasts. ```python theme={null} fcst.predict(1) ``` | | unique\_id | ds | lr | lgbm | | - | ---------- | ---------- | -------- | -------- | | 0 | id\_0 | 2000-08-10 | 3.549124 | 5.166797 | | 1 | id\_1 | 2000-04-07 | 3.154285 | 4.252490 | | 2 | id\_2 | 2000-06-16 | 2.880933 | 3.224506 | | 3 | id\_3 | 2000-08-30 | 4.061801 | 0.245443 | | 4 | id\_4 | 2001-01-08 | 2.904872 | 2.225106 | # Exogenous features Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/exogenous_features.html > Use exogenous regressors for training and predicting ```python theme={null} import lightgbm as lgb import pandas as pd from mlforecast import MLForecast from mlforecast.lag_transforms import ExpandingMean, RollingMean from mlforecast.utils import generate_daily_series, generate_prices_for_series ``` ## Data setup ```python theme={null} series = generate_daily_series( 100, equal_ends=True, n_static_features=2 ).rename(columns={'static_1': 'product_id'}) series.head() ``` | | unique\_id | ds | y | static\_0 | product\_id | | - | ---------- | ---------- | ---------- | --------- | ----------- | | 0 | id\_00 | 2000-10-05 | 39.811983 | 79 | 45 | | 1 | id\_00 | 2000-10-06 | 103.274013 | 79 | 45 | | 2 | id\_00 | 2000-10-07 | 176.574744 | 79 | 45 | | 3 | id\_00 | 2000-10-08 | 258.987900 | 79 | 45 | | 4 | id\_00 | 2000-10-09 | 344.940404 | 79 | 45 | ## Use existing exogenous features In mlforecast the required columns are the series identifier, time and target. Any extra columns you have, like `static_0` and `product_id` here are considered to be static and are replicated when constructing the features for the next timestamp. You can disable this by passing `static_features` to `MLForecast.preprocess` or `MLForecast.fit`, which will only keep the columns you define there as static. Keep in mind that all features in your input dataframe will be used for training, so you’ll have to provide the future values of exogenous features to `MLForecast.predict` through the `X_df` argument. Consider the following example. Suppose that we have a prices catalog for each id and date. ```python theme={null} prices_catalog = generate_prices_for_series(series) prices_catalog.head() ``` | | ds | unique\_id | price | | - | ---------- | ---------- | -------- | | 0 | 2000-10-05 | id\_00 | 0.548814 | | 1 | 2000-10-06 | id\_00 | 0.715189 | | 2 | 2000-10-07 | id\_00 | 0.602763 | | 3 | 2000-10-08 | id\_00 | 0.544883 | | 4 | 2000-10-09 | id\_00 | 0.423655 | And that you have already merged these prices into your series dataframe. ```python theme={null} series_with_prices = series.merge(prices_catalog, how='left') series_with_prices.head() ``` | | unique\_id | ds | y | static\_0 | product\_id | price | | - | ---------- | ---------- | ---------- | --------- | ----------- | -------- | | 0 | id\_00 | 2000-10-05 | 39.811983 | 79 | 45 | 0.548814 | | 1 | id\_00 | 2000-10-06 | 103.274013 | 79 | 45 | 0.715189 | | 2 | id\_00 | 2000-10-07 | 176.574744 | 79 | 45 | 0.602763 | | 3 | id\_00 | 2000-10-08 | 258.987900 | 79 | 45 | 0.544883 | | 4 | id\_00 | 2000-10-09 | 344.940404 | 79 | 45 | 0.423655 | This dataframe will be passed to `MLForecast.fit` (or `MLForecast.preprocess`). However, since the price is dynamic we have to tell that method that only `static_0` and `product_id` are static. ```python theme={null} fcst = MLForecast( models=lgb.LGBMRegressor(n_jobs=1, random_state=0, verbosity=-1), freq='D', lags=[7], lag_transforms={ 1: [ExpandingMean()], 7: [RollingMean(window_size=14)], }, date_features=['dayofweek', 'month'], num_threads=2, ) fcst.fit(series_with_prices, static_features=['static_0', 'product_id']) ``` ```text theme={null} MLForecast(models=[LGBMRegressor], freq=D, lag_features=['lag7', 'expanding_mean_lag1', 'rolling_mean_lag7_window_size14'], date_features=['dayofweek', 'month'], num_threads=2) ``` The features used for training are stored in `MLForecast.ts.features_order_`. As you can see `price` was used for training. ```python theme={null} fcst.ts.features_order_ ``` ```text theme={null} ['static_0', 'product_id', 'price', 'lag7', 'expanding_mean_lag1', 'rolling_mean_lag7_window_size14', 'dayofweek', 'month'] ``` So in order to update the price in each timestep we just call `MLForecast.predict` with our forecast horizon and pass the prices catalog through `X_df`. ```python theme={null} preds = fcst.predict(h=7, X_df=prices_catalog) preds.head() ``` | | unique\_id | ds | LGBMRegressor | | - | ---------- | ---------- | ------------- | | 0 | id\_00 | 2001-05-15 | 418.930093 | | 1 | id\_00 | 2001-05-16 | 499.487368 | | 2 | id\_00 | 2001-05-17 | 20.321885 | | 3 | id\_00 | 2001-05-18 | 102.310778 | | 4 | id\_00 | 2001-05-19 | 185.340281 | ## Generating exogenous features Nixtla provides some utilities to generate exogenous features for both training and forecasting such as [statsforecast’s mstl\_decomposition](https://nixtlaverse.nixtla.io/statsforecast/docs/how-to-guides/generating_features.html) or the [transform\_exog function](./transforming_exog.html). We also have [utilsforecast’s fourier function](https://nixtlaverse.nixtla.io/utilsforecast/feature_engineering.html#fourier), which we’ll demonstrate here. ```python theme={null} from sklearn.linear_model import LinearRegression from utilsforecast.feature_engineering import fourier ``` Suppose you start with some data like the one above where we have a couple of static features. ```python theme={null} series.head() ``` | | unique\_id | ds | y | static\_0 | product\_id | | - | ---------- | ---------- | ---------- | --------- | ----------- | | 0 | id\_00 | 2000-10-05 | 39.811983 | 79 | 45 | | 1 | id\_00 | 2000-10-06 | 103.274013 | 79 | 45 | | 2 | id\_00 | 2000-10-07 | 176.574744 | 79 | 45 | | 3 | id\_00 | 2000-10-08 | 258.987900 | 79 | 45 | | 4 | id\_00 | 2000-10-09 | 344.940404 | 79 | 45 | Now we’d like to add some fourier terms to model the seasonality. We can do that with the following: ```python theme={null} transformed_df, future_df = fourier(series, freq='D', season_length=7, k=2, h=7) ``` This provides an extended training dataset. ```python theme={null} transformed_df.head() ``` | | unique\_id | ds | y | static\_0 | product\_id | sin1\_7 | sin2\_7 | cos1\_7 | cos2\_7 | | - | ---------- | ---------- | ---------- | --------- | ----------- | --------- | --------- | --------- | --------- | | 0 | id\_00 | 2000-10-05 | 39.811983 | 79 | 45 | 0.781832 | 0.974928 | 0.623490 | -0.222521 | | 1 | id\_00 | 2000-10-06 | 103.274013 | 79 | 45 | 0.974928 | -0.433884 | -0.222521 | -0.900969 | | 2 | id\_00 | 2000-10-07 | 176.574744 | 79 | 45 | 0.433884 | -0.781831 | -0.900969 | 0.623490 | | 3 | id\_00 | 2000-10-08 | 258.987900 | 79 | 45 | -0.433884 | 0.781832 | -0.900969 | 0.623490 | | 4 | id\_00 | 2000-10-09 | 344.940404 | 79 | 45 | -0.974928 | 0.433884 | -0.222521 | -0.900969 | Along with the future values of the features. ```python theme={null} future_df.head() ``` | | unique\_id | ds | sin1\_7 | sin2\_7 | cos1\_7 | cos2\_7 | | - | ---------- | ---------- | --------- | --------- | --------- | --------- | | 0 | id\_00 | 2001-05-15 | -0.781828 | -0.974930 | 0.623494 | -0.222511 | | 1 | id\_00 | 2001-05-16 | 0.000006 | 0.000011 | 1.000000 | 1.000000 | | 2 | id\_00 | 2001-05-17 | 0.781835 | 0.974925 | 0.623485 | -0.222533 | | 3 | id\_00 | 2001-05-18 | 0.974927 | -0.433895 | -0.222527 | -0.900963 | | 4 | id\_00 | 2001-05-19 | 0.433878 | -0.781823 | -0.900972 | 0.623500 | We can now train using only these features (and the static ones). ```python theme={null} fcst2 = MLForecast(models=LinearRegression(), freq='D') fcst2.fit(transformed_df, static_features=['static_0', 'product_id']) ``` ```text theme={null} MLForecast(models=[LinearRegression], freq=D, lag_features=[], date_features=[], num_threads=1) ``` And provide the future values to the predict method. ```python theme={null} fcst2.predict(h=7, X_df=future_df).head() ``` | | unique\_id | ds | LinearRegression | | - | ---------- | ---------- | ---------------- | | 0 | id\_00 | 2001-05-15 | 275.822342 | | 1 | id\_00 | 2001-05-16 | 262.258117 | | 2 | id\_00 | 2001-05-17 | 238.195850 | | 3 | id\_00 | 2001-05-18 | 240.997814 | | 4 | id\_00 | 2001-05-19 | 262.247123 | ```python theme={null} preds2 = fcst.predict(7, X_df=prices_catalog) preds3 = fcst.predict(7, new_df=series_with_prices, X_df=prices_catalog) pd.testing.assert_frame_equal(preds, preds2) pd.testing.assert_frame_equal(preds, preds3) ``` # Hyperparameter optimization | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/hyperparameter_optimization.html > Tune your forecasting models ## Imports ```python theme={null} import os import tempfile import time import lightgbm as lgb import optuna import pandas as pd from datasetsforecast.m4 import M4, M4Evaluation, M4Info from sklearn.linear_model import Ridge from sklearn.compose import ColumnTransformer from sklearn.pipeline import make_pipeline from sklearn.preprocessing import OneHotEncoder from utilsforecast.plotting import plot_series from mlforecast import MLForecast from mlforecast.auto import ( AutoLightGBM, AutoMLForecast, AutoModel, AutoRidge, ridge_space, ) from mlforecast.lag_transforms import ExponentiallyWeightedMean, RollingMean ``` ```text theme={null} /Users/janrathfelder/miniconda3/envs/mlforecast-dev/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm ``` ## Data setup ```python theme={null} def get_data(group, horizon): df, *_ = M4.load(directory='data', group=group) df['ds'] = df['ds'].astype('int') df['unique_id'] = df['unique_id'].astype('category') return df.groupby('unique_id').head(-horizon).copy() group = 'Hourly' horizon = M4Info[group].horizon train = get_data(group, horizon) ``` ```text theme={null} /var/folders/nk/kvcs64mn4nbfqfw1ff_czjn80000gn/T/ipykernel_89215/3410956497.py:5: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning. return df.groupby('unique_id').head(-horizon).copy() ``` ## Optimization ### Default optimization We have default search spaces for some models and we can define default features to look for based on the length of the seasonal period of your data. For this example we’ll use hourly data, for which we’ll set 24 (one day) as the season length. ```python theme={null} optuna.logging.set_verbosity(optuna.logging.ERROR) auto_mlf = AutoMLForecast( models={'lgb': AutoLightGBM(), 'ridge': AutoRidge()}, freq=1, season_length=24, ) auto_mlf.fit( train, n_windows=2, h=horizon, num_samples=2, # number of trials to run ) ``` ```text theme={null} AutoMLForecast(models={'lgb': AutoModel(model=LGBMRegressor), 'ridge': AutoModel(model=Ridge)}) ``` We can now use these models to predict ```python theme={null} preds = auto_mlf.predict(horizon) preds.head() ``` | | unique\_id | ds | lgb | ridge | | - | ---------- | --- | ---------- | ---------- | | 0 | H1 | 701 | 680.534943 | 604.140123 | | 1 | H1 | 702 | 599.038307 | 523.364874 | | 2 | H1 | 703 | 572.808421 | 479.174481 | | 3 | H1 | 704 | 564.573783 | 444.540062 | | 4 | H1 | 705 | 543.046026 | 419.987657 | And evaluate them ```python theme={null} def evaluate(df, group): results = [] for model in df.columns.drop(['unique_id', 'ds']): model_res = M4Evaluation.evaluate( 'data', group, df[model].to_numpy().reshape(-1, horizon) ) model_res.index = [model] results.append(model_res) return pd.concat(results).T.round(2) evaluate(preds, group) ``` | | lgb | ridge | | ----- | ----- | ----- | | SMAPE | 18.78 | 20.00 | | MASE | 5.07 | 1.29 | | OWA | 1.57 | 0.81 | ### Tuning model parameters You can provide your own model with its search space to perform the optimization. The search space should be a function that takes an optuna trial and returns the model parameters. ```python theme={null} def my_lgb_config(trial: optuna.Trial): return { 'learning_rate': 0.05, 'verbosity': -1, 'num_leaves': trial.suggest_int('num_leaves', 2, 128, log=True), 'objective': trial.suggest_categorical('objective', ['l1', 'l2', 'mape']), } my_lgb = AutoModel( model=lgb.LGBMRegressor(), config=my_lgb_config, ) auto_mlf = AutoMLForecast( models={'my_lgb': my_lgb}, freq=1, season_length=24, ).fit( train, n_windows=2, h=horizon, num_samples=2, ) preds = auto_mlf.predict(horizon) evaluate(preds, group) ``` | | my\_lgb | | ----- | ------- | | SMAPE | 18.64 | | MASE | 4.76 | | OWA | 1.50 | #### Tuning scikit-learn pipelines We internally use [BaseEstimator.set\_params](https://scikit-learn.org/stable/modules/generated/sklearn.base.BaseEstimator.html#sklearn.base.BaseEstimator.set_params) for each configuration, so if you’re using a scikit-learn pipeline you can tune its parameters as you normally would with scikit-learn’s searches. ```python theme={null} ridge_pipeline = make_pipeline( ColumnTransformer( [('encoder', OneHotEncoder(), ['unique_id'])], remainder='passthrough', ), Ridge() ) my_auto_ridge = AutoModel( ridge_pipeline, # the space must have the name of the estimator followed by the parameter # you could also tune the encoder here lambda trial: {f'ridge__{k}': v for k, v in ridge_space(trial).items()}, ) auto_mlf = AutoMLForecast( models={'ridge': my_auto_ridge}, freq=1, season_length=24, fit_config=lambda trial: {'static_features': ['unique_id']} ).fit( train, n_windows=2, h=horizon, num_samples=2, ) preds = auto_mlf.predict(horizon) evaluate(preds, group) ``` | | ridge | | ----- | ----- | | SMAPE | 18.50 | | MASE | 1.24 | | OWA | 0.76 | ### Tuning features The `MLForecast` class defines the features to build in its constructor. You can tune the features by providing a function through the `init_config` argument, which will take an optuna trial and produce a configuration to pass to the `MLForecast` constructor. ```python theme={null} def my_init_config(trial: optuna.Trial): lag_transforms = [ ExponentiallyWeightedMean(alpha=0.3), RollingMean(window_size=24 * 7, min_samples=1), ] lag_to_transform = trial.suggest_categorical('lag_to_transform', [24, 48]) return { 'lags': [24 * i for i in range(1, 7)], # this won't be tuned 'lag_transforms': {lag_to_transform: lag_transforms}, } auto_mlf = AutoMLForecast( models=[AutoRidge()], freq=1, season_length=24, init_config=my_init_config, ).fit( train, n_windows=2, h=horizon, num_samples=2, ) preds = auto_mlf.predict(horizon) evaluate(preds, group) ``` ```text theme={null} /Users/janrathfelder/Documents/data_science/GitHub/mlforecast/mlforecast/auto.py:269: UserWarning: `season_length` is not used when `init_config` is provided. warnings.warn("`season_length` is not used when `init_config` is provided.") ``` | | AutoRidge | | ----- | --------- | | SMAPE | 13.31 | | MASE | 1.67 | | OWA | 0.71 | ### Tuning fit parameters The `MLForecast.fit` method takes some arguments that could improve the forecasting performance of your models, such as `dropna` and `static_features`. If you want to tune those you can provide a function to the `fit_config` argument. ```python theme={null} def my_fit_config(trial: optuna.Trial): if trial.suggest_int('use_id', 0, 1): static_features = ['unique_id'] else: static_features = None return { 'static_features': static_features } auto_mlf = AutoMLForecast( models=[AutoLightGBM()], freq=1, season_length=24, fit_config=my_fit_config, ).fit( train, n_windows=2, h=horizon, num_samples=2, ) preds = auto_mlf.predict(horizon) evaluate(preds, group) ``` | | AutoLightGBM | | ----- | ------------ | | SMAPE | 18.78 | | MASE | 5.07 | | OWA | 1.57 | ### M5 example: reuse CV splits + global/group rolling means This example shows both features in two small snippets: We can speed up the tuning process by reusing the cv splits. In traditional tuning, the cv splits are created inside each tuning round, which can be slow depending on data size and number of iterations. With reuse\_cv\_splits=True these splits are created once and consumed inside the tuning rounds without the need to split the data again. * We benchmark `reuse_cv_splits=True` against `False` while keeping the search space fixed to a single configuration. We still run multiple Optuna trials, but every trial evaluates the exact same model and feature settings, so the timing difference comes from reusing the cached CV windows instead of rebuilding them on each trial. * For the example below, the speed up when setting reuse\_cv\_splits=True is around 22% compared to reuse\_cv\_splits=True (standard setting) * We inspect the resulting feature values for `RollingMean(..., global_=True)` and `RollingMean(..., groupby=[...])` to see how the local, global, and grouped aggregations differ. All rolling window functions support `global_` and `groupby`; here we only show `RollingMean`. ```python theme={null} from datasetsforecast.m5 import M5 m5_static = ['item_id', 'dept_id', 'cat_id', 'store_id', 'state_id'] def get_m5_subset(directory='data', n_series=100): y_df, _, S_df = M5.load(directory=directory) y_df['ds'] = pd.to_datetime(y_df['ds']) # global_ lag transforms require aligned series ends end_ds = y_df.groupby('unique_id')['ds'].max() common_end = end_ds.mode().iat[0] keep_ids = end_ds[end_ds == common_end].index[:n_series] train = y_df[y_df['unique_id'].isin(keep_ids)].copy() train = train.merge(S_df, on='unique_id') return train m5_train = get_m5_subset() ``` ```text theme={null} /var/folders/nk/kvcs64mn4nbfqfw1ff_czjn80000gn/T/ipykernel_89215/1808854623.py:11: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning. end_ds = y_df.groupby('unique_id')['ds'].max() ``` ```python theme={null} m5_benchmark_model = AutoModel( model=make_pipeline( ColumnTransformer( [('encoder', OneHotEncoder(handle_unknown='ignore'), m5_static)], remainder='passthrough', ), Ridge(), ), config=lambda trial: {'ridge__alpha': 1.0}, ) def m5_benchmark_init_config(trial: optuna.Trial): return { 'lags': [1, 7, 28], 'lag_transforms': { 1: [ RollingMean(window_size=28), RollingMean(window_size=28, global_=True), RollingMean(window_size=28, groupby=['cat_id']), RollingMean(window_size=28, groupby=['state_id', 'cat_id']), ] }, } def m5_benchmark_fit_config(trial: optuna.Trial): return {'static_features': m5_static} def benchmark_m5_tuning(reuse_cv_splits: bool) -> float: automl = AutoMLForecast( models={'ridge': m5_benchmark_model}, freq='D', init_config=m5_benchmark_init_config, fit_config=m5_benchmark_fit_config, reuse_cv_splits=reuse_cv_splits, ) start = time.perf_counter() automl.fit( m5_train, n_windows=20, h=7, num_samples=30, ) return time.perf_counter() - start m5_timing = pd.DataFrame( [ {'reuse_cv_splits': False, 'seconds': benchmark_m5_tuning(False)}, {'reuse_cv_splits': True, 'seconds': benchmark_m5_tuning(True)}, ] ) baseline = m5_timing.loc[m5_timing["reuse_cv_splits"].eq(False), "seconds"].iloc[0] reuse = m5_timing.loc[m5_timing["reuse_cv_splits"].eq(True), "seconds"].iloc[0] pct_faster = (baseline / reuse - 1) * 100 print( f"Using reuse_cv_splits=True is {pct_faster:.1f}% faster than the traditional way of tuning " f"(reuse_cv_splits=False): {baseline:.2f}s -> {reuse:.2f}s." ) ``` ```text theme={null} Using reuse_cv_splits=True is 22.5% faster than the traditional way of tuning (reuse_cv_splits=False): 46.25s -> 37.74s. ``` ```python theme={null} m5_feature_demo = MLForecast( models=Ridge(), freq='D', lags=[1], lag_transforms={ 1: [ RollingMean(window_size=7), RollingMean(window_size=7, global_=True), RollingMean(window_size=7, groupby=['state_id']), RollingMean(window_size=7, groupby=['state_id', 'store_id']), ] }, ) m5_feature_values = m5_feature_demo.preprocess( m5_train, static_features=m5_static, dropna=False, ) feature_cols = [ 'rolling_mean_lag1_window_size7', 'global_rolling_mean_lag1_window_size7', 'groupby_state_id_rolling_mean_lag1_window_size7', 'groupby_state_id__store_id_rolling_mean_lag1_window_size7', ] last_ds = m5_feature_values['ds'].max() ( m5_feature_values.loc[ m5_feature_values['ds'].eq(last_ds), ['ds', 'unique_id', 'state_id', 'cat_id'] + feature_cols, ] .sort_values(['state_id', 'cat_id', 'unique_id']) .head(12) ) ``` | | ds | unique\_id | state\_id | cat\_id | rolling\_mean\_lag1\_window\_size7 | global\_rolling\_mean\_lag1\_window\_size7 | groupby\_state\_id\_rolling\_mean\_lag1\_window\_size7 | groupby\_state\_id\_\_store\_id\_rolling\_mean\_lag1\_window\_size7 | | ----- | ---------- | -------------------- | --------- | ------- | ---------------------------------- | ------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------------- | | 1968 | 2016-06-19 | FOODS\_1\_001\_CA\_1 | CA | FOODS | 0.857143 | 137.428574 | 64.428574 | 11.428572 | | 3937 | 2016-06-19 | FOODS\_1\_001\_CA\_2 | CA | FOODS | 1.142857 | 137.428574 | 64.428574 | 23.714285 | | 5906 | 2016-06-19 | FOODS\_1\_001\_CA\_3 | CA | FOODS | 1.714286 | 137.428574 | 64.428574 | 19.571428 | | 7874 | 2016-06-19 | FOODS\_1\_001\_CA\_4 | CA | FOODS | 0.428571 | 137.428574 | 64.428574 | 9.714286 | | 21632 | 2016-06-19 | FOODS\_1\_002\_CA\_1 | CA | FOODS | 1.285714 | 137.428574 | 64.428574 | 11.428572 | | 23601 | 2016-06-19 | FOODS\_1\_002\_CA\_2 | CA | FOODS | 0.714286 | 137.428574 | 64.428574 | 23.714285 | | 25570 | 2016-06-19 | FOODS\_1\_002\_CA\_3 | CA | FOODS | 0.285714 | 137.428574 | 64.428574 | 19.571428 | | 27538 | 2016-06-19 | FOODS\_1\_002\_CA\_4 | CA | FOODS | 0.571429 | 137.428574 | 64.428574 | 9.714286 | | 41300 | 2016-06-19 | FOODS\_1\_003\_CA\_1 | CA | FOODS | 0.142857 | 137.428574 | 64.428574 | 11.428572 | | 43269 | 2016-06-19 | FOODS\_1\_003\_CA\_2 | CA | FOODS | 1.428571 | 137.428574 | 64.428574 | 23.714285 | | 45238 | 2016-06-19 | FOODS\_1\_003\_CA\_3 | CA | FOODS | 0.428571 | 137.428574 | 64.428574 | 19.571428 | | 47206 | 2016-06-19 | FOODS\_1\_003\_CA\_4 | CA | FOODS | 0.142857 | 137.428574 | 64.428574 | 9.714286 | ```python theme={null} m5_feature_values.describe() ``` | | ds | y | lag1 | rolling\_mean\_lag1\_window\_size7 | global\_rolling\_mean\_lag1\_window\_size7 | groupby\_state\_id\_rolling\_mean\_lag1\_window\_size7 | groupby\_state\_id\_\_store\_id\_rolling\_mean\_lag1\_window\_size7 | | ----- | ----------------------------- | ------------- | ------------- | ---------------------------------- | ------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------------- | | count | 181878 | 181878.000000 | 181778.000000 | 181178.000000 | 181552.000000 | 181552.000000 | 181552.000000 | | mean | 2013-12-12 09:05:38.112361216 | 1.341410 | 1.341383 | 1.338734 | 128.771957 | 44.179596 | 12.859269 | | min | 2011-01-29 00:00:00 | 0.000000 | 0.000000 | 0.000000 | 32.857143 | 5.285714 | 0.428571 | | 25% | 2012-09-22 00:00:00 | 0.000000 | 0.000000 | 0.142857 | 92.285713 | 29.142857 | 6.857143 | | 50% | 2013-12-23 00:00:00 | 0.000000 | 0.000000 | 0.428571 | 129.071426 | 41.571430 | 11.285714 | | 75% | 2015-03-23 00:00:00 | 1.000000 | 1.000000 | 1.142857 | 162.000000 | 57.000000 | 17.000000 | | max | 2016-06-19 00:00:00 | 116.000000 | 116.000000 | 49.285713 | 298.714294 | 149.571426 | 61.000000 | | std | NaN | 3.615288 | 3.615864 | 3.123487 | 51.012020 | 21.452202 | 7.960689 | ## Accessing the optimization results After the process has finished the results are available under the `results_` attribute of the `AutoMLForecast` object. There will be one result per model and the best configuration can be found under the `config` user attribute. ```python theme={null} len(auto_mlf.results_) ``` ```text theme={null} 1 ``` ```python theme={null} auto_mlf.results_['AutoLightGBM'].best_trial.user_attrs['config'] ``` ```text theme={null} {'model_params': {'bagging_freq': 1, 'learning_rate': 0.05, 'verbosity': -1, 'n_estimators': 169, 'lambda_l1': 0.02733406969031059, 'lambda_l2': 0.002659931083868188, 'num_leaves': 112, 'feature_fraction': 0.7118273996694524, 'bagging_fraction': 0.8229470565333281, 'objective': 'l2'}, 'mlf_init_params': {'lags': [48], 'target_transforms': None, 'lag_transforms': {1: [ExponentiallyWeightedMean(alpha=0.9)]}, 'date_features': None, 'num_threads': 1}, 'mlf_fit_params': {'static_features': None}} ``` ### Individual models There is one optimization process per model. This is because different models can make use of different features. So after the optimization process is done for each model the best configuration is used to retrain the model using all of the data. These final models are `MLForecast` objects and are saved in the `models_` attribute. ```python theme={null} auto_mlf.models_ ``` ```text theme={null} {'AutoLightGBM': MLForecast(models=[AutoLightGBM], freq=1, lag_features=['lag48', 'exponentially_weighted_mean_lag1_alpha0.9'], date_features=[], num_threads=1)} ``` ## Saving You can use the `AutoMLForecast.save` method to save the best models found. This produces one directory per model. ```python theme={null} with tempfile.TemporaryDirectory() as tmpdir: auto_mlf.save(tmpdir) print(os.listdir(tmpdir)) ``` ```text theme={null} ['AutoLightGBM'] ``` Since each model is an `MLForecast` object you can load it by itself. ```python theme={null} with tempfile.TemporaryDirectory() as tmpdir: auto_mlf.save(tmpdir) loaded = MLForecast.load(f'{tmpdir}/AutoLightGBM') print(loaded) ``` ```text theme={null} MLForecast(models=[AutoLightGBM], freq=1, lag_features=['lag48', 'exponentially_weighted_mean_lag1_alpha0.9'], date_features=[], num_threads=1) ``` # Lag transformations | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/lag_transforms_guide.html > Compute features based on lags mlforecast allows you to define transformations on the lags to use as features. These are provided through the `lag_transforms` argument, which is a dict where the keys are the lags and the values are a list of transformations to apply to that lag. ## Data setup ```python theme={null} import numpy as np from mlforecast import MLForecast from mlforecast.utils import generate_daily_series ``` ```python theme={null} data = generate_daily_series(10) ``` ## Built-in transformations The built-in lag transformations are in the `mlforecast.lag_transforms` module. ```python theme={null} from mlforecast.lag_transforms import RollingMean, ExpandingStd ``` ```python theme={null} fcst = MLForecast( models=[], freq='D', lag_transforms={ 1: [ExpandingStd()], 7: [RollingMean(window_size=7, min_samples=1), RollingMean(window_size=14)] }, ) ``` Once you define your transformations you can see what they look like with `MLForecast.preprocess`. ```python theme={null} fcst.preprocess(data).head(2) ``` | | unique\_id | ds | y | expanding\_std\_lag1 | rolling\_mean\_lag7\_window\_size7\_min\_samples1 | rolling\_mean\_lag7\_window\_size14 | | -- | ---------- | ---------- | -------- | -------------------- | ------------------------------------------------- | ----------------------------------- | | 20 | id\_0 | 2000-01-21 | 6.319961 | 1.956363 | 3.234486 | 3.283064 | | 21 | id\_0 | 2000-01-22 | 0.071677 | 2.028545 | 3.256055 | 3.291068 | ### Extending the built-in transformations You can compose the built-in transformations by using the `Combine` class, which takes two transformations and an operator. ```python theme={null} import operator from mlforecast.lag_transforms import Combine ``` ```python theme={null} fcst = MLForecast( models=[], freq='D', lag_transforms={ 1: [ RollingMean(window_size=7), RollingMean(window_size=14), Combine( RollingMean(window_size=7), RollingMean(window_size=14), operator.truediv, ) ], }, ) prep = fcst.preprocess(data) prep.head(2) ``` | | unique\_id | ds | y | rolling\_mean\_lag1\_window\_size7 | rolling\_mean\_lag1\_window\_size14 | rolling\_mean\_lag1\_window\_size7\_truediv\_rolling\_mean\_lag1\_window\_size14 | | -- | ---------- | ---------- | -------- | ---------------------------------- | ----------------------------------- | -------------------------------------------------------------------------------- | | 14 | id\_0 | 2000-01-15 | 0.435006 | 3.234486 | 3.283064 | 0.985204 | | 15 | id\_0 | 2000-01-16 | 1.489309 | 3.256055 | 3.291068 | 0.989361 | ```python theme={null} np.testing.assert_allclose( prep['rolling_mean_lag1_window_size7'] / prep['rolling_mean_lag1_window_size14'], prep['rolling_mean_lag1_window_size7_truediv_rolling_mean_lag1_window_size14'] ) ``` If you want one of the transformations in `Combine` to be applied to a different lag you can use the `Offset` class, which will apply the offset first and then the transformation. ```python theme={null} from mlforecast.lag_transforms import Offset ``` ```python theme={null} fcst = MLForecast( models=[], freq='D', lag_transforms={ 1: [ RollingMean(window_size=7), Combine( RollingMean(window_size=7), Offset(RollingMean(window_size=7), n=1), operator.truediv, ) ], 2: [RollingMean(window_size=7)] }, ) prep = fcst.preprocess(data) prep.head(2) ``` | | unique\_id | ds | y | rolling\_mean\_lag1\_window\_size7 | rolling\_mean\_lag1\_window\_size7\_truediv\_rolling\_mean\_lag2\_window\_size7 | rolling\_mean\_lag2\_window\_size7 | | - | ---------- | ---------- | -------- | ---------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------- | | 8 | id\_0 | 2000-01-09 | 1.462798 | 3.326081 | 0.998331 | 3.331641 | | 9 | id\_0 | 2000-01-10 | 2.035518 | 3.360938 | 1.010480 | 3.326081 | ```python theme={null} np.testing.assert_allclose( prep['rolling_mean_lag1_window_size7'] / prep['rolling_mean_lag2_window_size7'], prep['rolling_mean_lag1_window_size7_truediv_rolling_mean_lag2_window_size7'] ) ``` ## Cross-series features: `global_`, `groupby`, and `partition_by` Every built-in rolling, expanding, seasonal-rolling, and exponentially weighted transform also accepts `global_=True` (compute across all series), `groupby=["col", ...]` (compute within a static-feature group), or `partition_by=["col", ...]` (split further along a *dynamic* column that can vary over time, like `promo` or `regime`). This is useful when a single series has too little history to support per-series rolling statistics, or when cross-series aggregates carry more signal than any single series’ lag. These **pooled** transforms use SQL `RANGE BETWEEN ... PRECEDING` semantics over actual timestamps, so they handle staggered series starts correctly and treat `min_samples` as a coverage threshold across the bucket. `partition_by` extends this to time-varying keys and is supplied via `X_df` at prediction time. See the [Pooled lag transforms](./pooled_lag_transforms.html) guide for a full walkthrough. ## numba-based transformations The [window-ops package](https://github.com/jmoralez/window_ops) provides transformations defined as [numba](https://numba.pydata.org/) [JIT compiled](https://en.wikipedia.org/wiki/Just-in-time_compilation) functions. We use numba because it makes them really fast and can also bypass [python’s GIL](https://wiki.python.org/moin/GlobalInterpreterLock), which allows running them concurrently with multithreading. The main benefit of using these transformations is that they’re very easy to implement. However, when we need to update their values on the predict step they can very slow, because we have to call the function again on the complete history and just keep the last value, so if performance is a concern you should try to use the built-in ones or set `keep_last_n` in `MLForecast.preprocess` or `MLForecast.fit` to the minimum number of samples that your transformations require. ```python theme={null} from numba import njit from window_ops.expanding import expanding_mean from window_ops.shift import shift_array ``` ```python theme={null} @njit def ratio_over_previous(x, offset=1): """Computes the ratio between the current value and its `offset` lag""" return x / shift_array(x, offset=offset) @njit def diff_over_previous(x, offset=1): """Computes the difference between the current value and its `offset` lag""" return x - shift_array(x, offset=offset) ``` If your function takes more arguments than the input array you can provide a tuple like: `(func, arg1, arg2, ...)` ```python theme={null} fcst = MLForecast( models=[], freq='D', lags=[1, 2, 3], lag_transforms={ 1: [expanding_mean, ratio_over_previous, (ratio_over_previous, 2)], # the second ratio sets offset=2 2: [diff_over_previous], }, ) prep = fcst.preprocess(data) prep.head(2) ``` | | unique\_id | ds | y | lag1 | lag2 | lag3 | expanding\_mean\_lag1 | ratio\_over\_previous\_lag1 | ratio\_over\_previous\_lag1\_offset2 | diff\_over\_previous\_lag2 | | - | ---------- | ---------- | -------- | -------- | -------- | -------- | --------------------- | --------------------------- | ------------------------------------ | -------------------------- | | 3 | id\_0 | 2000-01-04 | 3.481831 | 2.445887 | 1.218794 | 0.322947 | 1.329209 | 2.006809 | 7.573645 | 0.895847 | | 4 | id\_0 | 2000-01-05 | 4.191721 | 3.481831 | 2.445887 | 1.218794 | 1.867365 | 1.423546 | 2.856785 | 1.227093 | As you can see the name of the function is used as the transformation name plus the `_lag` suffix. If the function has other arguments and they’re not set to their default values they’re included as well, as is done with `offset=2` here. ```python theme={null} np.testing.assert_allclose(prep['lag1'] / prep['lag2'], prep['ratio_over_previous_lag1']) np.testing.assert_allclose(prep['lag1'] / prep['lag3'], prep['ratio_over_previous_lag1_offset2']) np.testing.assert_allclose(prep['lag2'] - prep['lag3'], prep['diff_over_previous_lag2']) ``` # MLflow | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/mlflow.html > Log your metrics and models ## Libraries ```python theme={null} import copy import subprocess import time import lightgbm as lgb import mlflow import pandas as pd import requests from sklearn.linear_model import LinearRegression from utilsforecast.data import generate_series from utilsforecast.losses import rmse, smape from utilsforecast.evaluation import evaluate from utilsforecast.feature_engineering import fourier import mlforecast.flavor from mlforecast import MLForecast from mlforecast.lag_transforms import ExponentiallyWeightedMean from mlforecast.utils import PredictionIntervals ``` ## Data setup ```python theme={null} freq = 'h' h = 10 series = generate_series(5, freq=freq) valid = series.groupby('unique_id', observed=True).tail(h) train = series.drop(valid.index) train, X_df = fourier(train, freq=freq, season_length=24, k=2, h=h) ``` ## Parameters ```python theme={null} params = { 'init': { 'models': { 'lgb': lgb.LGBMRegressor( n_estimators=50, num_leaves=16, verbosity=-1 ), 'lr': LinearRegression(), }, 'freq': freq, 'lags': [24], 'lag_transforms': { 1: [ExponentiallyWeightedMean(0.9)], }, 'num_threads': 2, }, 'fit': { 'static_features': ['unique_id'], 'prediction_intervals': PredictionIntervals(n_windows=2, h=h), } } ``` ## Logging If you have a tracking server, you can run `mlflow.set_tracking_uri(your_server_uri)` to connect to it. ```python theme={null} mlflow.set_experiment("mlforecast") with mlflow.start_run() as run: train_ds = mlflow.data.from_pandas(train) valid_ds = mlflow.data.from_pandas(valid) mlflow.log_input(train_ds, context="training") mlflow.log_input(valid_ds, context="validation") logged_params = copy.deepcopy(params) logged_params['init']['models'] = { k: (v.__class__.__name__, v.get_params()) for k, v in params['init']['models'].items() } mlflow.log_params(logged_params) mlf = MLForecast(**params['init']) mlf.fit(train, **params['fit']) preds = mlf.predict(h, X_df=X_df) eval_result = evaluate( valid.merge(preds, on=['unique_id', 'ds']), metrics=[rmse, smape], agg_fn='mean', ) models = mlf.models_.keys() logged_metrics = {} for _, row in eval_result.iterrows(): metric = row['metric'] for model in models: logged_metrics[f'{metric}_{model}'] = row[model] mlflow.log_metrics(logged_metrics) mlforecast.flavor.log_model(model=mlf, artifact_path="model", registered_model_name=None) model_uri = mlflow.get_artifact_uri("model") run_id = run.info.run_id ``` ## Load model ```python theme={null} mlflow.get_artifact_uri() ``` ```text theme={null} 'file:///Users/deven367/projects/public/mlforecast/nbs/docs/how-to-guides/mlruns/135880152043890861/2a94793bbbe244f09dd68961d5883f5b/artifacts' ``` ```python theme={null} fallback_uri = f"runs:/{run_id}/model" loaded_model = mlforecast.flavor.load_model(model_uri=fallback_uri) print("Model loaded successfully from run artifacts!") results = loaded_model.predict(h=h, X_df=X_df, ids=[3]) results.head(2) ``` ```text theme={null} Downloading artifacts: 0%| | 0/1 [00:00 Train one model to predict each step of the forecasting horizon By default mlforecast uses the recursive strategy, i.e. a model is trained to predict the next value and if we’re predicting several values we do it one at a time and then use the model’s predictions as the new target, recompute the features and predict the next step. There’s another approach called **direct forecasting** where if we want to predict 10 steps ahead we train 10 different models, where each model is trained to predict the value at each specific step, i.e. one model predicts the next value, another one predicts the value two steps ahead and so on. This can be very time consuming but can also provide better results. mlforecast provides two ways to use direct forecasting: 1. **`max_horizon`**: Train models for all horizons from 1 to `max_horizon`. For example, `max_horizon=10` trains 10 models (for steps 1, 2, 3, …, 10). 2. **`horizons`**: Train models only for specific horizons. For example, `horizons=[7, 14]` trains only 2 models (for steps 7 and 14\), which reduces computational cost when you only need predictions at certain steps. Both parameters are mutually exclusive - you can use one or the other, but not both. ## Setup ```python theme={null} import random import lightgbm as lgb import pandas as pd from datasetsforecast.m4 import M4, M4Info from utilsforecast.evaluation import evaluate from utilsforecast.losses import smape from mlforecast import MLForecast from mlforecast.lag_transforms import ExponentiallyWeightedMean, RollingMean from mlforecast.target_transforms import Differences ``` ### Data We will use four random series from the M4 dataset ```python theme={null} group = 'Hourly' await M4.async_download('data', group=group) df, *_ = M4.load(directory='data', group=group) df['ds'] = df['ds'].astype('int') ids = df['unique_id'].unique() random.seed(0) sample_ids = random.choices(ids, k=4) sample_df = df[df['unique_id'].isin(sample_ids)] info = M4Info[group] horizon = info.horizon valid = sample_df.groupby('unique_id').tail(horizon) train = sample_df.drop(valid.index) ``` ```python theme={null} def avg_smape(df): """Computes the SMAPE by series and then averages it across all series.""" full = df.merge(valid) return ( evaluate(full, metrics=[smape]) .drop(columns='metric') .set_index('unique_id') .squeeze() ) ``` ## Using `max_horizon` (all horizons) ```python theme={null} fcst = MLForecast( models=lgb.LGBMRegressor(random_state=0, verbosity=-1), freq=1, lags=[24 * (i+1) for i in range(7)], lag_transforms={ 1: [RollingMean(window_size=24)], 24: [RollingMean(window_size=24)], 48: [ExponentiallyWeightedMean(alpha=0.3)], }, num_threads=1, target_transforms=[Differences([24])], ) ``` ```python theme={null} horizon = 24 # Train 24 models using max_horizon (one for each step from 1 to 24) individual_fcst = fcst.fit(train, max_horizon=horizon) individual_preds = individual_fcst.predict(horizon) avg_smape_individual = avg_smape(individual_preds).rename('direct') # Train a single model using the recursive strategy recursive_fcst = fcst.fit(train) recursive_preds = recursive_fcst.predict(horizon) avg_smape_recursive = avg_smape(recursive_preds).rename('recursive') # Compare results print('Average SMAPE per method and series') avg_smape_individual.to_frame().join(avg_smape_recursive).applymap('{:.1%}'.format) ``` ```text theme={null} Average SMAPE per method and series ``` ```text theme={null} /var/folders/cc/cylsfhls0hb_9wg0wh8tvpyh0000gn/T/ipykernel_4013/3601618158.py:15: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead. avg_smape_individual.to_frame().join(avg_smape_recursive).applymap('{:.1%}'.format) ``` | | direct | recursive | | ---------- | ------ | --------- | | unique\_id | | | | H196 | 0.3% | 0.3% | | H256 | 0.4% | 0.3% | | H381 | 19.5% | 9.5% | | H413 | 11.9% | 13.6% | ## Using `horizons` (specific horizons only) When you only need predictions at specific time steps (e.g., weekly and bi-weekly forecasts), you can use the `horizons` parameter to train models only for those steps. This significantly reduces computational cost. For example, if you have hourly data and only need 12-hour and 24-hour ahead predictions: ```python theme={null} # Train models only for horizons 12 and 24 (instead of all 1-24) sparse_fcst = fcst.fit(train, horizons=[12, 24]) sparse_preds = sparse_fcst.predict(h=24) # Note: predictions are only returned for trained horizons print(f"Number of predictions per series: {len(sparse_preds) // sparse_preds['unique_id'].nunique()}") sparse_preds.head(8) ``` ```text theme={null} Number of predictions per series: 2 ``` | | unique\_id | ds | LGBMRegressor | | - | ---------- | --- | ------------- | | 0 | H196 | 972 | 16.095804 | | 1 | H196 | 984 | 15.696618 | | 2 | H256 | 972 | 13.295804 | | 3 | H256 | 984 | 12.696618 | | 4 | H381 | 972 | 12.271730 | | 5 | H381 | 984 | 49.347744 | | 6 | H413 | 972 | 23.099708 | | 7 | H413 | 984 | 17.449030 | Notice that with `horizons=[12, 24]`, the output only contains 2 predictions per series (at steps 12 and 24), not 24. This is the **sparse output** behavior - you only get predictions for the horizons you trained. ### Partial predictions If you call `predict(h=N)` where `N` is less than some of your trained horizons, you’ll only get predictions for horizons up to `N`: ```python theme={null} # With horizons=[12, 24], calling predict(h=15) only returns horizon 12 partial_preds = sparse_fcst.predict(h=15) print(f"Number of predictions per series: {len(partial_preds) // partial_preds['unique_id'].nunique()}") partial_preds.head(4) ``` ```text theme={null} Number of predictions per series: 1 ``` | | unique\_id | ds | LGBMRegressor | | - | ---------- | --- | ------------- | | 0 | H196 | 972 | 16.095804 | | 1 | H256 | 972 | 13.295804 | | 2 | H381 | 972 | 12.271730 | | 3 | H413 | 972 | 23.099708 | ### Cross-validation with specific horizons The `horizons` parameter also works with `cross_validation`: ```python theme={null} # Cross-validation with specific horizons cv_results = fcst.cross_validation( train, n_windows=2, h=24, horizons=[12, 24], ) print(f"CV results shape: {cv_results.shape}") cv_results.head(8) ``` ```text theme={null} CV results shape: (16, 5) ``` | | unique\_id | ds | cutoff | y | LGBMRegressor | | - | ---------- | --- | ------ | ----- | ------------- | | 0 | H196 | 924 | 912 | 22.7 | 15.770231 | | 1 | H196 | 936 | 912 | 16.4 | 15.317588 | | 2 | H256 | 924 | 912 | 17.9 | 13.270231 | | 3 | H256 | 936 | 912 | 13.3 | 12.617588 | | 4 | H381 | 924 | 912 | 166.0 | 18.920395 | | 5 | H381 | 936 | 912 | 50.0 | 51.129478 | | 6 | H413 | 924 | 912 | 45.0 | 26.609079 | | 7 | H413 | 936 | 912 | 31.0 | 24.801567 | ## Summary | Approach | Parameter | Use case | | -------------------------- | ------------------------ | --------------------------------------------------------------------------------- | | Recursive | (default) | General purpose, good when you need all horizons and want faster training | | Direct (all horizons) | `max_horizon=N` | When you need predictions for all steps 1 to N and can afford training N models | | Direct (specific horizons) | `horizons=[h1, h2, ...]` | When you only need predictions at specific steps (e.g., weekly/monthly forecasts) | Key points: - `max_horizon` and `horizons` are mutually exclusive - With `horizons`, the output only contains predictions for the specified horizons (sparse output) - Both direct forecasting approaches work with `cross_validation` and exogenous features # Pooled lag transforms Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/pooled_lag_transforms.html > Compute lag features across all series, or within groups of series, > using SQL-style RANGE semantics. Most lag transforms in `mlforecast` are computed **independently per series**: a rolling mean for series `A` only ever sees `A`’s own history. That works well when every series carries enough signal on its own, but it breaks down in two common situations: * **Short-history series.** A newly launched product, store, or SKU has too little history for per-series rolling statistics to stabilise. * **Cross-series signals.** Total demand across a brand, region, or category at a given timestamp is often a stronger feature than any one series’ lag. **Pooled lag transforms** address both. By passing `global_=True` (across all series) or `groupby=["col", ...]` (within a static-feature group) to any rolling, expanding, seasonal, or exponentially weighted transform, you ask `mlforecast` to compute the statistic over a **bucket of series** aggregated by timestamp. The result is a single value per timestamp per bucket that every series in the bucket then receives as a feature. ## Data setup ```python theme={null} import warnings import numpy as np import pandas as pd from mlforecast import MLForecast from mlforecast.lag_transforms import ( ExpandingMean, ExponentiallyWeightedMean, RollingMean, ) from mlforecast.utils import generate_daily_series ``` We’ll start from a small synthetic panel and attach a static `brand` column so we have something to group by. ```python theme={null} series = generate_daily_series( n_series=6, min_length=60, max_length=60, equal_ends=True, static_as_categorical=False, seed=0, ) brand_map = {f"id_{i}": "A" if i < 3 else "B" for i in range(6)} series["brand"] = series["unique_id"].map(brand_map) series.head() ``` ## Global features with `global_=True` Set `global_=True` on any built-in lag transform to compute the statistic **across all series**, aggregated by timestamp. Every series receives the same value at a given timestamp. ```python theme={null} fcst = MLForecast( models=[], freq="D", lag_transforms={ 1: [RollingMean(window_size=7, global_=True)], }, ) prep = fcst.preprocess(series, static_features=["brand"]) prep[prep["ds"] == prep["ds"].min()].head(6) ``` Notice that `global_rolling_mean_lag1_window_size7` is identical for every `unique_id` at a given `ds`: it’s the rolling mean of the **pooled** observations across all series, lagged by one day. The feature name is automatically prefixed with `global_` to make the pooling explicit. ## Group features with `groupby=[...]` Use `groupby` to compute the statistic **within each level of one or more static features**. Series in the same group share the feature value at each timestamp; series in different groups get different values. Any column used in `groupby` must be declared as a static feature when fitting. ```python theme={null} fcst = MLForecast( models=[], freq="D", lag_transforms={ 1: [RollingMean(window_size=7, groupby=["brand"])], }, ) prep = fcst.preprocess(series, static_features=["brand"]) prep[prep["ds"] == prep["ds"].min()].sort_values(["brand", "unique_id"]).head(6) ``` Series within brand `A` share one rolling-mean value; series within brand `B` share another. The column name is prefixed with `groupby_brand_` to record which static feature drove the pooling. ## RANGE semantics: staggered series and gaps Pooled transforms use **`RANGE BETWEEN ... PRECEDING`** semantics, the same model SQL window functions use. The window is defined by **timestamp distance**, not row position, and only **actual observations** are aggregated — no synthetic zeros are injected for series that haven’t started yet. Concretely, with `RollingMean(window_size=2, global_=True)` over the data below: | `unique_id` | `ds` | `y` | | ----------- | ---- | ---- | | a | 1 | 1.0 | | a | 2 | 2.0 | | a | 3 | 3.0 | | b | 2 | 20.0 | | b | 3 | 30.0 | Series `b` starts at `ds=2`; it does **not** contribute a phantom zero at `ds=1`. At each timestamp the window looks back over the last two days of *real* values across all series, applied at lag 1. ```python theme={null} staggered = pd.DataFrame( { "unique_id": ["a", "a", "a", "b", "b"], "ds": pd.to_datetime( ["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-02", "2024-01-03"] ), "y": [1.0, 2.0, 3.0, 20.0, 30.0], } ) fcst = MLForecast( models=[], freq="D", lag_transforms={1: [RollingMean(window_size=2, global_=True, min_samples=1)]}, ) fcst.preprocess(staggered, dropna=False) ``` Because pooled transforms assume a **continuous, gap-free time grid** within each series, you should validate your data (the default) before relying on these features. We come back to this in the `validate_data` section below. ## `min_samples` in pooled mode There’s an important semantic divergence between local and pooled modes: * **Local mode** (per series): `min_samples` is capped at `window_size` by `coreforecast`. It controls how many *non-NaN values in the window* the series needs. * **Pooled mode** (`global_=True` or `groupby=...`): `min_samples` counts the **total non-NaN observations across all series in the bucket**, with no capping at `window_size`. This makes it useful as a *coverage threshold*. For example, `RollingMean(window_size=1, min_samples=2, groupby=['brand'])` only produces a value when at least two series in the brand contributed an observation in the window — useful to suppress noise from sparsely populated groups. The default when `min_samples=None` also differs by mode. Everywhere else it defaults to `window_size`, but in **local partition mode** (`partition_by` without `global_`/`groupby`, covered below) it defaults to **1**: the window spans `window_size` calendar steps while only same-partition observations count toward `min_samples`, so requiring a full window is rarely attainable — `RollingMean(7, partition_by=['promo'])` on a panel with interleaved promo days would otherwise be almost entirely NaN. The default of 1 matches SQL `RANGE` window semantics, where the result is NULL only for empty windows. When `partition_by` is combined with `global_` or `groupby`, the default remains `window_size`, counted across all series in the (group, partition) bucket — pass `min_samples` explicitly if you want a different threshold. ```python theme={null} fcst = MLForecast( models=[], freq="D", lag_transforms={ 1: [RollingMean(window_size=1, min_samples=2, groupby=["brand"])], }, ) prep = fcst.preprocess(series, static_features=["brand"], dropna=False) prep[ [ "unique_id", "ds", "brand", "groupby_brand_rolling_mean_lag1_window_size1_min_samples2", ] ].head(6) ``` ## Aggregating rows per timestamp with `time_agg` By default a pooled transform treats every observation as an individual sample: `RollingMean(window_size=7, groupby=['brand'])` pools *all rows* of the brand that fall in the window and averages them, weighting a timestamp by how many series reported on it. Sometimes you instead want to first collapse all rows sharing a timestamp into a single value — for example the brand’s **daily total** — and then apply the transform over that per-timestamp series. `time_agg` does exactly that. Set it to one of `'sum'`, `'count'`, `'mean'`, `'min'`, or `'max'` and each `(bucket, timestamp)` is reduced to one value before the rolling/expanding/seasonal/EWM statistic runs: * `time_agg='sum'` — rolling mean of the brand’s daily *sums* (total demand) * `time_agg='count'` — e.g. expanding max of the number of active series per day * `time_agg='max'` — rolling max of the daily *peak* across the brand `time_agg` requires `global_=True` or `groupby=[...]` (optionally combined with `partition_by`). It is rejected in local or `partition_by`-only mode, where each `(bucket, timestamp)` already has a single row and the aggregation would be a no-op. With `time_agg=None`, each observation remains an individual pooled sample. `ExponentiallyWeightedMean` is the exception: its `time_agg` defaults to `'mean'`, and `None` is not accepted. ```python theme={null} fcst = MLForecast( models=[], freq="D", lag_transforms={ 1: [RollingMean(window_size=3, groupby=["brand"], time_agg="sum")], }, ) prep = fcst.preprocess(series, static_features=["brand"], dropna=False) prep[ [ "unique_id", "ds", "brand", "groupby_brand_rolling_mean_lag1_window_size3_time_aggsum", ] ].head(6) ``` Two semantics worth calling out: * With `time_agg`, `min_samples` counts **observed timestamps** in the window (each timestamp contributes at most one aggregated value), not rows. `RollingMean(window_size=3, min_samples=3, groupby=['brand'], time_agg='sum')` needs three distinct timestamps of history, regardless of how many series reported on each. * `RollingMean(..., time_agg='mean')` differs from `RollingMean(...)` without `time_agg`: the former is an unweighted mean of the per-timestamp means, the latter a row-weighted pooled mean (they coincide only when every timestamp has the same number of observations). On `ExponentiallyWeightedMean`, `time_agg='mean'` is the default because pooled EWM consumes each timestamp’s bucket-aggregate mean exactly once. ## Disabling validation: the `validate_data=False` warning Pooled transforms are correctness-sensitive to timestamp gaps because they rely on the *actual* timestamps when computing RANGE windows. If you bypass validation (`validate_data=False`) and your data has gaps, the features will be silently wrong. To make this hazard explicit, `mlforecast` emits a `UserWarning` when you combine `validate_data=False` with any pooled transform: ```python theme={null} fcst = MLForecast( models=[], freq="D", lag_transforms={1: [RollingMean(window_size=7, global_=True)]}, ) with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") fcst.preprocess(series, static_features=["brand"], validate_data=False) [str(w.message) for w in caught if issubclass(w.category, UserWarning)] ``` If you genuinely need to skip validation (e.g. on a very large pre-cleaned dataset), make sure your time grid is continuous and gap-free before doing so. ## Pooling with `partition_by` `partition_by=[col, ...]` splits a pooled bucket further along a **dynamic** column — typically a feature whose value changes over time (e.g. `promo`, `regime`, `store_format` flips). Each unique combination of partition values gets its own bucket; rolling/expanding statistics are computed per bucket. Unlike `groupby`, `partition_by` columns **do not need to be static**. They can vary across timestamps for the same `unique_id`. At prediction time you must supply their future values via `X_df`. Three combinations are supported: | Combination | Bucket key | | ----------------------------------- | --------------------------------------------------------------------------------------------------------- | | `partition_by=[...]` alone | `(unique_id, *partition_values)` — *local partition*, one bucket per series-partition pair | | `global_=True, partition_by=[...]` | `(*partition_values)` — *nonlocal*, aggregates across all series sharing the same partition values | | `groupby=[...], partition_by=[...]` | `(*group_values, *partition_values)` — *nonlocal*, aggregates within each group, separately per partition | Unlike `global_` and `groupby` (which are mutually exclusive), `partition_by` composes with either one — or stands alone. Let’s add a time-varying `promo` column to our panel and walk through the three combinations. ```python theme={null} rng = np.random.default_rng(0) series["promo"] = rng.choice([0, 1], size=len(series), p=[0.7, 0.3]) series.head() ``` ### Local partition: per-(id, partition\_vals) buckets When `partition_by` is used alone (no `global_`, no `groupby`), each `(unique_id, partition_value)` combination becomes its own bucket. The rolling window is computed within that bucket only — so a series sees a different rolling history depending on which `promo` regime it is currently in. ```python theme={null} fcst = MLForecast( models=[], freq="D", lag_transforms={1: [RollingMean(window_size=7, partition_by=["promo"])]}, ) fcst.preprocess(series).head(8) ``` The feature name is prefixed `partby_promo_` to indicate the partition. Each row’s feature is the 7-day rolling mean of `y` lagged by 1, restricted to rows of the same series with the same `promo` value. ### Global + `partition_by`: cross-series aggregates within each partition Combine with `global_=True` to aggregate across all series, but separately for each `promo` value: ```python theme={null} fcst = MLForecast( models=[], freq="D", lag_transforms={ 1: [RollingMean(window_size=7, global_=True, partition_by=["promo"])] }, ) fcst.preprocess(series).head(8) ``` Every series with `promo=0` at a given timestamp sees the same `global_partby_promo_...` value; every series with `promo=1` sees a different (also shared) value. ### Groupby + `partition_by`: group aggregates within each partition Combine with `groupby=[...]` to aggregate per group (e.g. per `brand`), separately for each partition value: ```python theme={null} fcst = MLForecast( models=[], freq="D", lag_transforms={ 1: [RollingMean(window_size=7, groupby=["brand"], partition_by=["promo"])] }, ) fcst.preprocess(series, static_features=["brand"]).head(8) ``` The bucket key here is `(brand, promo)`. There are at most `n_brands × n_promo_values` buckets. ## Dynamic partition values at prediction time Because partition columns are *not* static, you must supply their future values when forecasting. Pass an `X_df` containing `unique_id`, `ds`, and every partition column for every horizon step: ```python theme={null} from sklearn.linear_model import LinearRegression fcst = MLForecast( models=[LinearRegression()], freq="D", lag_transforms={ 1: [RollingMean(window_size=7, global_=True, partition_by=["promo"])] }, ) # `brand` plays no role in this global_ + partition_by example, so we drop it # rather than feed a raw string column to LinearRegression fcst.fit(series.drop(columns="brand")) last_date = series["ds"].max() unique_ids = sorted(series["unique_id"].unique()) future_promo = pd.DataFrame( [ { "unique_id": uid, "ds": last_date + pd.Timedelta(days=h), "promo": int(rng.integers(0, 2)), } for uid in unique_ids for h in range(1, 4) ] ) fcst.predict(h=3, X_df=future_promo).head() ``` A few notes on dynamic prediction: * If `X_df` introduces a partition value never seen during fit, mlforecast creates a fresh bucket on the fly. The first few predictions for that bucket may be `NaN` if its history is shorter than `window_size`/`min_samples`. * Subset prediction (`predict(ids=[...])`) is rejected when nonlocal `partition_by` is in play: the missing series would still contribute to the bucket aggregates, so partial subsets are ambiguous. * Partition columns are **automatically excluded from `static_features`** even when `static_features=None`. They must come from `X_df` at predict time. ## Understanding ordinals and parent calendars A `partition_by` bucket only contains the rows where the partition value is active for a series. When the partition is sparse — e.g. `promo=1` is only on at timestamps `[1, 3, 5]` while the parent calendar (the global or group scope) is `[1, 2, 3, 4, 5]` — the bucket’s *ordinals* come from the parent calendar: * Bucket has observations at parent positions `[0, 2, 4]`, not `[0, 1, 2]`. * `RollingMean(window_size=2)` at parent position 4 looks back to positions `[3, 4]`. The bucket has no observation at position 3, so only position 4 contributes to the window. This preserves SQL `RANGE BETWEEN ...` semantics across gaps: the window is defined in *time*, not in *number of rows*. Without parent-calendar ordinals, a partition with gaps would silently collapse to row-based semantics, mixing observations across non-adjacent timestamps. ## Aligned-end requirement All pooled modes — `global_`, `groupby`, and `partition_by` (including local, alone) — require every series to **end at the same timestamp**. This is checked at `fit` and `predict` time and raises if violated: ```text theme={null} ValueError: Pooled lag transforms require all series to end at the same timestamp (recursive prediction advances all series in lockstep). ``` Local `partition_by` shares this requirement because parent calendars (per-id, for local mode) must advance in lockstep during recursive prediction. If your series have ragged ends, pre-align them (pad to a common last timestamp, or drop series that end early) before fitting. ## Exponentially Weighted Mean with partitions `ExponentiallyWeightedMean` with `partition_by` has subtly different semantics from a regular EWM: * The decay is applied across the bucket’s **observed timestamp aggregates** (one contribution per timestamp, regardless of how many rows the aggregate covered). * Timestamps where the partition bucket has **no observation** are skipped — they do not contribute to the running EWM and do not advance the decay step. This means EWM treats each observed timestamp uniformly and ignores gaps. mlforecast emits a `UserWarning` when you construct an `ExponentiallyWeightedMean` with `partition_by` to make this explicit. ```python theme={null} with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") tfm = ExponentiallyWeightedMean(alpha=0.3, partition_by=["promo"]) [str(w.message) for w in caught if issubclass(w.category, UserWarning)] ``` ## `keep_last_n` and pooled history `keep_last_n` controls how much history is carried into the recursive prediction loop. It applies to pooled transforms too, **per state** (one state per `(mode, groupby, partition_by)` combination): * A state whose transforms are **all finite-window** — `Lag`, any `Rolling*` or `SeasonalRolling*`, and `Offset`/`Combine` built from them — is trimmed to its last `max(keep_last_n, W_state)` parent-calendar ordinals, where `W_state` is the state’s widest window. The dropped prefix can never enter a window, so predictions are unchanged. * A state containing any **unbounded** transform — `Expanding*` or `ExponentiallyWeightedMean` — keeps its **full** history. Unlike the local (coreforecast) path, pooled expanding/EWM carry no running accumulator: they recompute over the entire aggregate vectors at every step, so trimming them would change predictions. This applies whether `keep_last_n` is set explicitly or inferred — when you leave it unset it is inferred as the largest window across all transforms — mirroring how the per-series arrays used by local transforms are trimmed. ### Divergence from local transforms Pooled trimming is **floored at the state’s widest window** (`W_state`). A local rolling transform survives an explicit `keep_last_n` *smaller* than its window because coreforecast keeps a separate per-transform window buffer. Pooled mode has no such buffer — the per-timestamp aggregates *are* the buffer — so a `keep_last_n` below a pooled window is raised to that window for the affected state, keeping predictions correct. When `keep_last_n` is left to be inferred it already equals the largest window, so the floor never changes anything. ## Occurrence lookups with `LookupLag` `LookupLag` returns the target from the **previous matching occurrence** within each `(unique_id, partition_by...)` bucket, regardless of the calendar gap between occurrences. This is useful for moving events — e.g. “last year’s Easter value” — where the relevant previous target is not a fixed calendar lag. `partition_by` is required and defines the buckets. Because it is a pooled transform, the partition columns may change over time and must be supplied via `X_df` at prediction. Since the lookup is computed inside `mlforecast`, it stays aligned with any target transforms (e.g. `LocalStandardScaler`). ```python theme={null} from mlforecast.lag_transforms import LookupLag fcst = MLForecast( models=[], freq='D', lag_transforms={1: [LookupLag(partition_by=['promo'])]}, ) prep = fcst.preprocess(series, static_features=['brand']) prep.head() ``` ## Constraints to keep in mind * `global_=True` and `groupby=[...]` are **mutually exclusive** on the same transform. Use two separate transforms if you want both feature families. `partition_by` composes with either one, or stands alone. * Columns in `groupby` must be declared in `static_features` when you call `fit` / `preprocess`. Columns in `partition_by` must **not** be — they are dynamic and supplied via `X_df` at prediction. * `min_samples=0` in pooled mode produces NaN for timestamps with no observations in the window and triggers a warning at construction time — prefer `min_samples=1` if you want “as soon as there is any observation”. * All pooled modes — `global_`, `groupby`, and `partition_by` (including local, alone) — require **series to end at the same timestamp**. * Subset prediction (`predict(ids=[...])`) is rejected when nonlocal pooled transforms are in play: omitted series would still contribute to the bucket aggregates. * All supported rolling, expanding, seasonal-rolling, and exponentially weighted transforms accept `global_`, `groupby`, and `partition_by`. `Offset` and `Combine` delegate to their wrapped transforms. * **Performance**: `RollingQuantile`, `ExpandingQuantile` and the `SeasonalRolling*` transforms have no aggregate-cache fast path in pooled modes. They fall back to a row-level pass whose cost grows with `unique timestamps × bucket rows` at fit, and aggregates are rebuilt at every recursive prediction step. For large panels prefer the mean/std/min/max/EWM transforms, which use cached per-timestamp aggregates. * **Memory**: finite-window pooled states (`Lag`/`Rolling*`/`SeasonalRolling*`) are trimmed under `keep_last_n` just like the per-series arrays (see the [`keep_last_n` and pooled history](#keep_last_n-and-pooled-history) section above); a state containing an `Expanding*`/`ExponentiallyWeightedMean` transform keeps the full training history because it recomputes over all of it at predict. Each `predict` call also backs up the pooled state per model to isolate recursive mutations. Expect higher peak memory with unbounded pooled transforms on large panels. * `time_agg` (`'sum'`/`'count'`/`'mean'`/`'min'`/`'max'`) pre-aggregates rows sharing a timestamp within each bucket before the transform runs; it requires `global_` or `groupby` and counts observed timestamps for `min_samples`. All pooled-capable transforms support it, including the quantile and seasonal ones (via the row-level path). ## End-to-end example Pooled transforms behave like any other lag transform in the full `MLForecast` lifecycle: `fit`, `predict`, and `cross_validation` all carry the pooled features through automatically. ```python theme={null} from sklearn.linear_model import LinearRegression fcst = MLForecast( models=[LinearRegression()], freq="D", lags=[1, 7], lag_transforms={ 1: [ RollingMean(window_size=7, global_=True), RollingMean(window_size=7, groupby=["brand"]), ExpandingMean(groupby=["brand"]), ExponentiallyWeightedMean(alpha=0.3, global_=True), ], }, ) # `promo` was only needed for the partition_by examples above; this end-to-end # model groups by `brand` and doesn't use it, so drop it to keep predict X_df-free fcst.fit(series.drop(columns="promo"), static_features=["brand"]) fcst.predict(h=7).head(6) ``` ## Where to next * The general [Lag transformations](./lag_transforms_guide.html) guide covers the built-in transforms, `Combine`, `Offset`, and custom numba-based transforms. * The [`mlforecast.lag_transforms`](../../lag_transforms.html) API reference documents every parameter, including `global_`, `groupby`, and `min_samples`, for each transform class. # Predict callbacks Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/predict_callbacks.html > Get access to the input features and predictions in each forecasting > horizon If you want to do something to the input before predicting or something to the output before it gets used to update the target (and thus the next features that rely on lags), you can pass a function to run at any of these times. Here are a couple of examples: ```python theme={null} import copy import lightgbm as lgb import numpy as np from IPython.display import display from mlforecast import MLForecast from mlforecast.utils import generate_daily_series ``` ```python theme={null} series = generate_daily_series(1) ``` ## Before predicting ### Inspecting the input We can define a function that displays our input dataframe before predicting. ```python theme={null} def inspect_input(new_x): """Displays the model inputs to inspect them""" display(new_x) return new_x ``` And now we can pass this function to the `before_predict_callback` argument of `MLForecast.predict`. ```python theme={null} fcst = MLForecast(lgb.LGBMRegressor(verbosity=-1), freq='D', lags=[1, 2]) fcst.fit(series, static_features=['unique_id']) preds = fcst.predict(2, before_predict_callback=inspect_input) preds ``` | | unique\_id | lag1 | lag2 | | - | ---------- | ------- | -------- | | 0 | id\_0 | 4.15593 | 3.000028 | | | unique\_id | lag1 | lag2 | | - | ---------- | -------- | ------- | | 0 | id\_0 | 5.250205 | 4.15593 | | | unique\_id | ds | LGBMRegressor | | - | ---------- | ---------- | ------------- | | 0 | id\_0 | 2000-08-10 | 5.250205 | | 1 | id\_0 | 2000-08-11 | 6.241739 | ### Saving the input features Saving the features that are sent as input to the model in each timestamp can be helpful, for example to estimate SHAP values. This can be easily achieved with the `SaveFeatures` callback. ```python theme={null} from mlforecast.callbacks import SaveFeatures ``` ```python theme={null} fcst = MLForecast(lgb.LGBMRegressor(verbosity=-1), freq='D', lags=[1]) fcst.fit(series, static_features=['unique_id']) save_features_cbk = SaveFeatures() fcst.predict(2, before_predict_callback=save_features_cbk); ``` Once we’ve called predict we can just retrieve the features. ```python theme={null} save_features_cbk.get_features() ``` | | unique\_id | lag1 | | - | ---------- | -------- | | 0 | id\_0 | 4.155930 | | 1 | id\_0 | 5.281643 | ## After predicting When predicting with the recursive strategy (the default) the predictions for each timestamp are used to update the target and recompute the features. If you want to do something to these predictions before that happens you can use the `after_predict_callback` argument of `MLForecast.predict`. ### Increasing predictions values Suppose we know that our model always underestimates and we want to prevent that from happening by making our predictions 10% higher. We can achieve that with the following: ```python theme={null} def increase_predictions(predictions): """Increases all predictions by 10%""" return 1.1 * predictions ``` ```python theme={null} fcst = MLForecast( {'model': lgb.LGBMRegressor(verbosity=-1)}, freq='D', date_features=['dayofweek'], ) fcst.fit(series) original_preds = fcst.predict(2) scaled_preds = fcst.predict(2, after_predict_callback=increase_predictions) np.testing.assert_array_less( original_preds['model'].values, scaled_preds['model'].values, ) ``` ```python theme={null} fcst.ts._uids = fcst.ts.uids fcst.ts._idxs = None fcst.ts._static_features = fcst.ts.static_features_ fcst.ts._ga = copy.copy(fcst.ts.ga) fcst.ts._predict_setup() for attr in ('head', 'tail'): new_x = fcst.ts._get_features_for_next_step(None) original_preds = fcst.models_['model'].predict(new_x) expected = 1.1 * original_preds actual = getattr(scaled_preds.groupby('unique_id')['model'], attr)(1).values np.testing.assert_equal(expected, actual) fcst.ts._update_y(actual) ``` # Predicting a subset of ids Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/predict_subset.html > Compute predictions for only a subset of the training ids ```python theme={null} from lightgbm import LGBMRegressor from fastcore.test import test_fail from mlforecast import MLForecast from mlforecast.utils import generate_daily_series ``` ```python theme={null} series = generate_daily_series(5) fcst = MLForecast({'lgb': LGBMRegressor(verbosity=-1)}, freq='D', date_features=['dayofweek']) fcst.fit(series) all_preds = fcst.predict(1) all_preds ``` | | unique\_id | ds | lgb | | - | ---------- | ---------- | -------- | | 0 | id\_0 | 2000-08-10 | 3.728396 | | 1 | id\_1 | 2000-04-07 | 4.749133 | | 2 | id\_2 | 2000-06-16 | 4.749133 | | 3 | id\_3 | 2000-08-30 | 2.758949 | | 4 | id\_4 | 2001-01-08 | 3.331394 | By default all series seen during training will be forecasted with the predict method. If you’re only interested in predicting a couple of them you can use the `ids` argument. ```python theme={null} fcst.predict(1, ids=['id_0', 'id_4']) ``` | | unique\_id | ds | lgb | | - | ---------- | ---------- | -------- | | 0 | id\_0 | 2000-08-10 | 3.728396 | | 1 | id\_4 | 2001-01-08 | 3.331394 | Note that the ids must’ve been seen during training, if you try to predict an id that wasn’t there you’ll get an error. ```python theme={null} test_fail(lambda: fcst.predict(1, ids=['fake_id']), contains='fake_id') ``` # Probabilistic forecasting | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/prediction_intervals.html > In this example, we’ll implement prediction intervals > **Prerequisites** > > This tutorial assumes basic familiarity with MLForecast. For a minimal > example visit the [Quick > Start](https://nixtlaverse.nixtla.io/mlforecast/docs/getting-started/quick_start_local.html) ## Introduction When we generate a forecast, we usually produce a single value known as the point forecast. This value, however, doesn’t tell us anything about the uncertainty associated with the forecast. To have a measure of this uncertainty, we need **prediction intervals**. A prediction interval is a range of values that the forecast can take with a given probability. Hence, a 95% prediction interval should contain a range of values that include the actual future value with probability 95%. Probabilistic forecasting aims to generate the full forecast distribution. Point forecasting, on the other hand, usually returns the mean or the median or said distribution. However, in real-world scenarios, it is better to forecast not only the most probable future outcome, but many alternative outcomes as well. With [MLForecast](https://nixtla.github.io/mlforecast/) you can train `sklearn` models to generate point forecasts. It also takes the advantages of `ConformalPrediction` to generate the same point forecasts and adds them prediction intervals. By the end of this tutorial, you’ll have a good understanding of how to add probabilistic intervals to `sklearn` models for time series forecasting. Furthermore, you’ll also learn how to generate plots with the historical data, the point forecasts, and the prediction intervals. > **Important** > > Although the terms are often confused, prediction intervals are not > the same as [confidence > intervals](https://robjhyndman.com/hyndsight/intervals/). > **Warning** > > In practice, most prediction intervals are too narrow since models do > not account for all sources of uncertainty. A discussion about this > can be found [here](https://robjhyndman.com/hyndsight/narrow-pi/). **Outline:** 1. Install libraries 2. Load and explore the data 3. Train models 4. Plot prediction intervals > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Install libraries Install the necessary packages using `pip install mlforecast utilsforecast` ## Load and explore the data For this example, we’ll use the hourly dataset from the [M4 Competition](https://www.sciencedirect.com/science/article/pii/S0169207019301128). We first need to download the data from a URL and then load it as a `pandas` dataframe. Notice that we’ll load the train and the test data separately. We’ll also rename the `y` column of the test data as `y_test`. ```python theme={null} import pandas as pd from utilsforecast.plotting import plot_series ``` ```python theme={null} train = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly.csv') test = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly-test.csv') ``` ```python theme={null} train.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | ```python theme={null} test.head() ``` | | unique\_id | ds | y | | - | ---------- | --- | ----- | | 0 | H1 | 701 | 619.0 | | 1 | H1 | 702 | 565.0 | | 2 | H1 | 703 | 532.0 | | 3 | H1 | 704 | 495.0 | | 4 | H1 | 705 | 481.0 | Since the goal of this notebook is to generate prediction intervals, we’ll only use the first 8 series of the dataset to reduce the total computational time. ```python theme={null} n_series = 8 uids = train['unique_id'].unique()[:n_series] # select first n_series of the dataset train = train.query('unique_id in @uids') test = test.query('unique_id in @uids') ``` We can plot these series using the `plot_series` function from the [utilsforecast](https://nixtla.github.io/utilsforecast/plotting.html) library. This function has multiple parameters, and the required ones to generate the plots in this notebook are explained below. * `df`: A `pandas` dataframe with columns \[`unique_id`, `ds`, `y`]. * `forecasts_df`: A `pandas` dataframe with columns \[`unique_id`, `ds`] and models. * `plot_random`: bool = `True`. Plots the time series randomly. * `models`: List\[str]. A list with the models we want to plot. * `level`: List\[float]. A list with the prediction intervals we want to plot. * `engine`: str = `matplotlib`. It can also be `plotly`. `plotly` generates interactive plots, while `matplotlib` generates static plots. ```python theme={null} fig = plot_series(train, test.rename(columns={'y': 'y_test'}), models=['y_test'], plot_random=False) ``` ## Train models MLForecast can train multiple models that follow the `sklearn` syntax (`fit` and `predict`) on different time series efficiently. For this example, we’ll use the following `sklearn` baseline models: * [Lasso](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Lasso.html) * [LinearRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html) * [Ridge](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Ridge.html) * [K-Nearest Neighbors](https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsRegressor.html) * [Multilayer Perceptron (NeuralNetwork)](https://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPRegressor.html) To use these models, we first need to import them from `sklearn` and then we need to instantiate them. ```python theme={null} from mlforecast import MLForecast from mlforecast.target_transforms import Differences from mlforecast.utils import PredictionIntervals from sklearn.linear_model import Lasso, LinearRegression, Ridge from sklearn.neighbors import KNeighborsRegressor from sklearn.neural_network import MLPRegressor ``` ```python theme={null} # Create a list of models and instantiation parameters models = [ KNeighborsRegressor(), Lasso(), LinearRegression(), MLPRegressor(), Ridge(), ] ``` To instantiate a new MLForecast object, we need the following parameters: * `models`: The list of models defined in the previous step. * `target_transforms`: Transformations to apply to the target before computing the features. These are restored at the forecasting step. * `lags`: Lags of the target to use as features. ```python theme={null} mlf = MLForecast( models=[Ridge(), Lasso(), LinearRegression(), KNeighborsRegressor(), MLPRegressor(random_state=0)], freq=1, target_transforms=[Differences([1])], lags=[24 * (i+1) for i in range(7)], ) ``` Now we’re ready to generate the point forecasts and the prediction intervals. To do this, we’ll use the `fit` method, which takes the following arguments: * `data`: Series data in long format. * `id_col`: Column that identifies each series. In our case, `unique_id`. * `time_col`: Column that identifies each timestep, its values can be timestamps or integers. In our case, `ds`. * `target_col`: Column that contains the target. In our case, `y`. * `prediction_intervals`: A `PredicitonIntervals` class. The class takes two parameters: `n_windows` and `h`. `n_windows` represents the number of cross-validation windows used to calibrate the intervals and `h` is the forecast horizon. The strategy will adjust the intervals for each horizon step, resulting in different widths for each step. ```python theme={null} mlf.fit( train, prediction_intervals=PredictionIntervals(n_windows=10, h=48), ); ``` After fitting the models, we will call the `predict` method to generate forecasts with prediction intervals. The method takes the following arguments: * `horizon`: An integer that represent the forecasting horizon. In this case, we’ll forecast the next 48 hours. * `level`: A list of floats with the confidence levels of the prediction intervals. For example, `level=[95]` means that the range of values should include the actual future value with probability 95%. ```python theme={null} levels = [50, 80, 95] forecasts = mlf.predict(48, level=levels) forecasts.head() ``` | | unique\_id | ds | Ridge | Lasso | LinearRegression | KNeighborsRegressor | MLPRegressor | Ridge-lo-95 | Ridge-lo-80 | Ridge-lo-50 | ... | KNeighborsRegressor-lo-50 | KNeighborsRegressor-hi-50 | KNeighborsRegressor-hi-80 | KNeighborsRegressor-hi-95 | MLPRegressor-lo-95 | MLPRegressor-lo-80 | MLPRegressor-lo-50 | MLPRegressor-hi-50 | MLPRegressor-hi-80 | MLPRegressor-hi-95 | | - | ---------- | --- | ---------- | ---------- | ---------------- | ------------------- | ------------ | ----------- | ----------- | ----------- | --- | ------------------------- | ------------------------- | ------------------------- | ------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | | 0 | H1 | 701 | 612.418170 | 612.418079 | 612.418170 | 615.2 | 612.651532 | 590.473256 | 594.326570 | 603.409944 | ... | 609.45 | 620.95 | 627.20 | 631.310 | 584.736193 | 591.084898 | 597.462107 | 627.840957 | 634.218166 | 640.566870 | | 1 | H1 | 702 | 552.309298 | 552.308073 | 552.309298 | 551.6 | 548.791801 | 498.721501 | 518.433843 | 532.710850 | ... | 535.85 | 567.35 | 569.16 | 597.525 | 497.308756 | 500.417799 | 515.452396 | 582.131207 | 597.165804 | 600.274847 | | 2 | H1 | 703 | 494.943384 | 494.943367 | 494.943384 | 509.6 | 490.226796 | 448.253304 | 463.266064 | 475.006125 | ... | 492.70 | 526.50 | 530.92 | 544.180 | 424.587658 | 436.042788 | 448.682502 | 531.771091 | 544.410804 | 555.865935 | | 3 | H1 | 704 | 462.815779 | 462.815363 | 462.815779 | 474.6 | 459.619069 | 409.975219 | 422.243593 | 436.128272 | ... | 451.80 | 497.40 | 510.26 | 525.500 | 379.291083 | 392.580306 | 413.353178 | 505.884959 | 526.657832 | 539.947054 | | 4 | H1 | 705 | 440.141034 | 440.140586 | 440.141034 | 451.6 | 438.091712 | 377.999588 | 392.523016 | 413.474795 | ... | 427.40 | 475.80 | 488.96 | 503.945 | 348.618034 | 362.503767 | 386.303325 | 489.880099 | 513.679657 | 527.565389 | ```python theme={null} test = test.merge(forecasts, how='left', on=['unique_id', 'ds']) ``` ## Plot prediction intervals To plot the point and the prediction intervals, we’ll use the `plot_series` function again. Notice that now we also need to specify the model and the levels that we want to plot. ### KNeighborsRegressor ```python theme={null} fig = plot_series( train, test, plot_random=False, models=['KNeighborsRegressor'], level=levels, max_insample_length=48 ) ``` ### Lasso ```python theme={null} fig = plot_series( train, test, plot_random=False, models=['Lasso'], level=levels, max_insample_length=48 ) ``` ### Linear Regression ```python theme={null} fig = plot_series( train, test, plot_random=False, models=['LinearRegression'], level=levels, max_insample_length=48 ) ``` ### MLPRegressor ```python theme={null} fig = plot_series( train, test, plot_random=False, models=['MLPRegressor'], level=levels, max_insample_length=48 ) ``` ### Ridge ```python theme={null} fig = plot_series( train, test, plot_random=False, models=['Ridge'], level=levels, max_insample_length=48 ) ``` From these plots, we can conclude that the uncertainty around each forecast varies according to the model that is being used. For the same time series, one model can predict a wider range of possible future values than others. ## References * [Kamile Stankeviciute, Ahmed M. Alaa and Mihaela van der Schaar (2021). “Conformal Time-Series Forecasting”](https://proceedings.neurips.cc/paper/2021/file/312f1ba2a72318edaaa995a67835fad5-Paper.pdf) * [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, The Statistical Forecasting Perspective”](https://otexts.com/fpp3/perspective.html). # Sample weights Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/sample_weights.html > Provide a column to pass through to the underlying models as sample > weights ## Data setup ```python theme={null} import numpy as np from mlforecast.utils import generate_daily_series ``` ```python theme={null} series = generate_daily_series(2) series['weight'] = np.random.default_rng(seed=0).random(series.shape[0]) series.head(2) ``` | | unique\_id | ds | y | weight | | - | ---------- | ---------- | -------- | -------- | | 0 | id\_0 | 2000-01-01 | 0.357595 | 0.636962 | | 1 | id\_0 | 2000-01-02 | 1.301382 | 0.269787 | ## Creating forecast object ```python theme={null} import lightgbm as lgb from sklearn.linear_model import LinearRegression from mlforecast import MLForecast ``` ```python theme={null} fcst = MLForecast( models={ 'lr': LinearRegression(), 'lgbm': lgb.LGBMRegressor(verbosity=-1), }, freq='D', lags=[1], date_features=['dayofweek'], ) ``` ## Forecasting You can provide the `weight_col` argument to `MLForecast.fit` to indicate which column should be used as the sample weights. ```python theme={null} fcst.fit(series, weight_col='weight').predict(1) ``` | | unique\_id | ds | lr | lgbm | | - | ---------- | ---------- | -------- | -------- | | 0 | id\_0 | 2000-08-10 | 3.336019 | 5.283677 | | 1 | id\_1 | 2000-04-07 | 3.300786 | 4.230655 | ## Cross validation You can provide the `weight_col` argument to `MLForecast.cross_validation` to indicate which column should be used as the sample weights. ```python theme={null} fcst.cross_validation(series, n_windows=2, h=1, weight_col='weight') ``` | | unique\_id | ds | cutoff | y | lr | lgbm | | - | ---------- | ---------- | ---------- | -------- | -------- | -------- | | 0 | id\_0 | 2000-08-08 | 2000-08-07 | 3.436325 | 2.770717 | 3.242790 | | 1 | id\_1 | 2000-04-05 | 2000-04-04 | 2.430276 | 2.687932 | 2.075247 | | 2 | id\_0 | 2000-08-09 | 2000-08-08 | 4.136771 | 3.095140 | 4.239010 | | 3 | id\_1 | 2000-04-06 | 2000-04-05 | 3.363522 | 3.016661 | 3.436962 | # Using scikit-learn pipelines Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/sklearn_pipelines.html > Leverage scikit-learn’s composability to define pipelines as models mlforecast takes scikit-learn estimators as models, which means you can provide [scikit-learn’s pipelines](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html) as models in order to further apply transformations to the data before passing it to the model. ## Data setup ```python theme={null} from mlforecast.utils import generate_daily_series ``` ```python theme={null} series = generate_daily_series(5) series.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | -------- | | 0 | id\_0 | 2000-01-01 | 0.428973 | | 1 | id\_0 | 2000-01-02 | 1.423626 | | 2 | id\_0 | 2000-01-03 | 2.311782 | | 3 | id\_0 | 2000-01-04 | 3.192191 | | 4 | id\_0 | 2000-01-05 | 4.148767 | ## Pipelines definition Suppose that you want to use a linear regression model with the lag1 and the day of the week as features. mlforecast returns the day of the week as a single column, however, that’s not the optimal format for a linear regression model, which benefits more from having indicator columns for each day of the week (removing one to avoid colinearity). We can achieve this by using [scikit-learn’s OneHotEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html) and then fitting our linear regression model, which we can do in the following way: ```python theme={null} from mlforecast import MLForecast from sklearn.compose import ColumnTransformer from sklearn.linear_model import LinearRegression from sklearn.pipeline import make_pipeline from sklearn.preprocessing import OneHotEncoder ``` ```python theme={null} fcst = MLForecast( models=[], freq='D', lags=[1], date_features=['dayofweek'] ) X, y = fcst.preprocess(series, return_X_y=True) X.head() ``` | | lag1 | dayofweek | | - | -------- | --------- | | 1 | 0.428973 | 6 | | 2 | 1.423626 | 0 | | 3 | 2.311782 | 1 | | 4 | 3.192191 | 2 | | 5 | 4.148767 | 3 | This is what will be passed to our model, so we’d like to get the `dayofweek` column and perform one hot encoding, leaving the `lag1` column untouched. We can achieve that with the following: ```python theme={null} ohe = ColumnTransformer( transformers=[ ('encoder', OneHotEncoder(drop='first'), ['dayofweek']) ], remainder='passthrough', ) X_transformed = ohe.fit_transform(X) X_transformed.shape ``` ```text theme={null} (1096, 7) ``` We can see that our data now has 7 columns, 1 for the lag plus 6 for the days of the week (we dropped the first one). ```python theme={null} ohe.get_feature_names_out() ``` ```text theme={null} array(['encoder__dayofweek_1', 'encoder__dayofweek_2', 'encoder__dayofweek_3', 'encoder__dayofweek_4', 'encoder__dayofweek_5', 'encoder__dayofweek_6', 'remainder__lag1'], dtype=object) ``` ## Training We can now build a pipeline that does this and then passes it to our linear regression model. ```python theme={null} model = make_pipeline(ohe, LinearRegression()) ``` And provide this as a model to mlforecast ```python theme={null} fcst = MLForecast( models={'ohe_lr': model}, freq='D', lags=[1], date_features=['dayofweek'] ) fcst.fit(series) ``` ```text theme={null} MLForecast(models=[ohe_lr], freq=, lag_features=['lag1'], date_features=['dayofweek'], num_threads=1) ``` ## Forecasting Finally, we compute the forecasts. ```python theme={null} fcst.predict(1) ``` | | unique\_id | ds | ohe\_lr | | - | ---------- | ---------- | -------- | | 0 | id\_0 | 2000-08-10 | 4.312748 | | 1 | id\_1 | 2000-04-07 | 4.537019 | | 2 | id\_2 | 2000-06-16 | 4.160505 | | 3 | id\_3 | 2000-08-30 | 3.777040 | | 4 | id\_4 | 2001-01-08 | 2.676933 | ## Summary You can provide complex scikit-learn pipelines as models to mlforecast, which allows you to perform different transformations depending on the model and use any of scikit-learn’s compatible estimators. # Target transformations Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/target_transforms_guide.html > Seamlessly transform target values Since mlforecast uses a single global model it can be helpful to apply some transformations to the target to ensure that all series have similar distributions. They can also help remove trend for models that can’t deal with it out of the box. ## Data setup For this example we’ll use a single series from the M4 dataset. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import pandas as pd from datasetsforecast.m4 import M4 from sklearn.base import BaseEstimator from mlforecast import MLForecast from mlforecast.target_transforms import Differences, LocalStandardScaler ``` ```python theme={null} data_path = 'data' await M4.async_download(data_path, group='Hourly') df, *_ = M4.load(data_path, 'Hourly') df['ds'] = df['ds'].astype('int32') serie = df[df['unique_id'].eq('H196')] ``` ## Local transformations > Transformations applied per series ### Differences We’ll take a look at our series to see possible differences that would help our models. ```python theme={null} def plot(series, fname): n_series = len(series) fig, ax = plt.subplots(ncols=n_series, figsize=(7 * n_series, 6), squeeze=False) for (title, serie), axi in zip(series.items(), ax.flat): serie.set_index('ds')['y'].plot(title=title, ax=axi) fig.savefig(f'../../figs/{fname}', bbox_inches='tight') plt.close() ``` ```python theme={null} plot({'original': serie}, 'target_transforms__eda.png') ``` We can see that our data has a trend as well as a clear seasonality. We can try removing the trend first. ```python theme={null} fcst = MLForecast( models=[], freq=1, target_transforms=[Differences([1])], ) without_trend = fcst.preprocess(serie) plot({'original': serie, 'without trend': without_trend}, 'target_transforms__diff1.png') ``` The trend is gone, we can now try taking the 24 difference (subtract the value at the same hour in the previous day). ```python theme={null} fcst = MLForecast( models=[], freq=1, target_transforms=[Differences([1, 24])], ) without_trend_and_seasonality = fcst.preprocess(serie) plot({'original': serie, 'without trend and seasonality': without_trend_and_seasonality}, 'target_transforms__diff2.png') ``` ### LocalStandardScaler We see that our series is random noise now. Suppose we also want to standardize it, i.e. make it have a mean of 0 and variance of 1. We can add the LocalStandardScaler transformation after these differences. ```python theme={null} fcst = MLForecast( models=[], freq=1, target_transforms=[Differences([1, 24]), LocalStandardScaler()], ) standardized = fcst.preprocess(serie) plot({'original': serie, 'standardized': standardized}, 'target_transforms__standardized.png') standardized['y'].agg(['mean', 'var']).round(2) ``` ```text theme={null} mean -0.0 var 1.0 Name: y, dtype: float64 ``` Now that we’ve captured the components of the series (trend + seasonality), we could try forecasting it with a model that always predicts 0, which will basically project the trend and seasonality. ```python theme={null} class Zeros(BaseEstimator): def fit(self, X, y=None): return self def predict(self, X, y=None): return np.zeros(X.shape[0]) fcst = MLForecast( models={'zeros_model': Zeros()}, freq=1, target_transforms=[Differences([1, 24]), LocalStandardScaler()], ) preds = fcst.fit(serie).predict(48) fig, ax = plt.subplots() pd.concat([serie.tail(24 * 10), preds]).set_index('ds').plot(ax=ax) plt.close() ``` ## Global transformations > Transformations applied to all series ### GlobalSklearnTransformer There are some transformations that don’t require to learn any parameters, such as applying logarithm for example. These can be easily defined using the `GlobalSklearnTransformer`, which takes a scikit-learn compatible transformer and applies it to all series. Here’s an example on how to define a transformation that applies logarithm to each value of the series + 1, which can help avoid computing the log of 0. ```python theme={null} import numpy as np from sklearn.preprocessing import FunctionTransformer from mlforecast.target_transforms import GlobalSklearnTransformer sk_log1p = FunctionTransformer(func=np.log1p, inverse_func=np.expm1) fcst = MLForecast( models={'zeros_model': Zeros()}, freq=1, target_transforms=[GlobalSklearnTransformer(sk_log1p)], ) log1p_transformed = fcst.preprocess(serie) plot({'original': serie, 'Log transformed': log1p_transformed}, 'target_transforms__log.png') ``` We can also combine this with local transformations. For example we can apply log first and then differencing. ```python theme={null} fcst = MLForecast( models=[], freq=1, target_transforms=[GlobalSklearnTransformer(sk_log1p), Differences([1, 24])], ) log_diffs = fcst.preprocess(serie) plot({'original': serie, 'Log + Differences': log_diffs}, 'target_transforms__log_diffs.png') ``` ## Custom transformations > Implementing your own target transformations In order to implement your own target transformation you have to define a class that inherits from `mlforecast.target_transforms.BaseTargetTransform` (this takes care of setting the column names as the `id_col`, `time_col` and `target_col` attributes) and implement the `fit_transform` and `inverse_transform` methods. Here’s an example on how to define a min-max scaler. ```python theme={null} from mlforecast.target_transforms import BaseTargetTransform ``` ```python theme={null} class LocalMinMaxScaler(BaseTargetTransform): """Scales each series to be in the [0, 1] interval.""" def fit_transform(self, df: pd.DataFrame) -> pd.DataFrame: self.stats_ = df.groupby(self.id_col)[self.target_col].agg(['min', 'max']) df = df.merge(self.stats_, on=self.id_col) df[self.target_col] = (df[self.target_col] - df['min']) / (df['max'] - df['min']) df = df.drop(columns=['min', 'max']) return df def inverse_transform(self, df: pd.DataFrame) -> pd.DataFrame: df = df.merge(self.stats_, on=self.id_col) for col in df.columns.drop([self.id_col, self.time_col, 'min', 'max']): df[col] = df[col] * (df['max'] - df['min']) + df['min'] df = df.drop(columns=['min', 'max']) return df ``` And now you can pass an instance of this class to the `target_transforms` argument. ```python theme={null} fcst = MLForecast( models=[], freq=1, target_transforms=[LocalMinMaxScaler()], ) minmax_scaled = fcst.preprocess(serie) plot({'original': serie, 'min-max scaled': minmax_scaled}, 'target_transforms__minmax.png') ``` # Training with numpy arrays Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/training_with_numpy.html > Convert your dataframes to arrays to use less memory and train faster Most of the machine learning libraries use numpy arrays, even when you provide a dataframe it ends up being converted into a numpy array. By providing an array to those models we can make the process faster, since the conversion will only happen once. ## Data setup ```python theme={null} from mlforecast.utils import generate_daily_series ``` ```python theme={null} series = generate_daily_series(5) ``` ## fit and cross\_validation methods ```python theme={null} import numpy as np from lightgbm import LGBMRegressor from sklearn.linear_model import LinearRegression from mlforecast import MLForecast ``` ```python theme={null} fcst = MLForecast( models={'lr': LinearRegression(), 'lgbm': LGBMRegressor(verbosity=-1)}, freq='D', lags=[7, 14], date_features=['dayofweek'], ) ``` If you’re using the fit/cross\_validation methods from `MLForecast` all you have to do to train with numpy arrays is provide the `as_numpy` argument, which will cast the features to an array before passing them to the models. ```python theme={null} fcst.fit(series, as_numpy=True) ``` ```text theme={null} MLForecast(models=[lr, lgbm], freq=, lag_features=['lag7', 'lag14'], date_features=['dayofweek'], num_threads=1) ``` When predicting, the new features will also be cast to arrays, so it can also be faster. ```python theme={null} fcst.predict(1) ``` | | unique\_id | ds | lr | lgbm | | - | ---------- | ---------- | -------- | -------- | | 0 | id\_0 | 2000-08-10 | 5.268787 | 6.322262 | | 1 | id\_1 | 2000-04-07 | 4.437316 | 5.213255 | | 2 | id\_2 | 2000-06-16 | 3.246518 | 4.373904 | | 3 | id\_3 | 2000-08-30 | 0.144860 | 1.285219 | | 4 | id\_4 | 2001-01-08 | 2.211318 | 3.236700 | For cross\_validation we also just need to specify `as_numpy=True`. ```python theme={null} cv_res = fcst.cross_validation(series, n_windows=2, h=2, as_numpy=True) ``` ## preprocess method Having the features as a numpy array can also be helpful in cases where you have categorical columns and the library doesn’t support them, for example LightGBM with polars. In order to use categorical features with LightGBM and polars we have to convert them to their integer representation and tell LightGBM to treat those features as categorical, which we can achieve in the following way: ```python theme={null} series_pl = generate_daily_series(5, n_static_features=1, engine='polars') series_pl.head(2) ``` | unique\_id | ds | y | static\_0 | | ---------- | ------------------- | ---------- | --------- | | cat | datetime\[ns] | f64 | cat | | "id\_0" | 2000-01-01 00:00:00 | 36.462689 | "84" | | "id\_0" | 2000-01-02 00:00:00 | 121.008199 | "84" | ```python theme={null} fcst = MLForecast( models=[], freq='1d', lags=[7, 14], date_features=['weekday'], ) ``` In order to get the features as an array with the preprocess method we also have to ask for the X, y tuple. ```python theme={null} X, y = fcst.preprocess(series_pl, return_X_y=True, as_numpy=True) X[:2] ``` ```text theme={null} array([[ 0. , 20.30076749, 36.46268875, 6. ], [ 0. , 119.51717097, 121.0081989 , 7. ]]) ``` The feature names are available in `fcst.ts.features_order_` ```python theme={null} fcst.ts.features_order_ ``` ```text theme={null} ['static_0', 'lag7', 'lag14', 'weekday'] ``` Now we can just train a LightGBM model specifying the feature names and which features should be treated as categorical. ```python theme={null} model = LGBMRegressor(verbosity=-1) model.fit( X=X, y=y, feature_name=fcst.ts.features_order_, categorical_feature=['static_0', 'weekday'], ); ``` We can now add this model to our models dict, as described in the [custom training guide](./custom_training.html). ```python theme={null} fcst.models_ = {'lgbm': model} ``` And use it to predict. ```python theme={null} fcst.predict(1) ``` | unique\_id | ds | lgbm | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "id\_0" | 2000-08-10 00:00:00 | 448.796188 | | "id\_1" | 2000-04-07 00:00:00 | 81.058211 | | "id\_2" | 2000-06-16 00:00:00 | 4.450549 | | "id\_3" | 2000-08-30 00:00:00 | 14.219603 | | "id\_4" | 2001-01-08 00:00:00 | 87.361881 | # Transfer Learning | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/transfer_learning.html Transfer learning refers to the process of pre-training a flexible model on a large dataset and using it later on other data with little to no training. It is one of the most outstanding 🚀 achievements in Machine Learning and has many practical applications. For time series forecasting, the technique allows you to get lightning-fast predictions ⚡ bypassing the tradeoff between accuracy and speed (more than 30 times faster than our already fast [AutoARIMA](https://github.com/Nixtla/statsforecast) for a similar accuracy). This notebook shows how to generate a pre-trained model to forecast new time series never seen by the model. Table of Contents * Installing MLForecast * Load M3 Monthly Data * Instantiate NeuralForecast core, Fit, and save * Use the pre-trained model to predict on AirPassengers * Evaluate Results You can run these experiments with Google Colab. Open In Colab ## Installing Libraries ```python theme={null} %%capture # !pip install mlforecast datasetsforecast utilsforecast s3fs ``` ```python theme={null} import lightgbm as lgb import numpy as np import pandas as pd from datasetsforecast.m3 import M3 from sklearn.metrics import mean_absolute_error from utilsforecast.plotting import plot_series from mlforecast import MLForecast from mlforecast.target_transforms import Differences ``` ## Load M3 Data The `M3` class will automatically download the complete M3 dataset and process it. It return three Dataframes: `Y_df` contains the values for the target variables, `X_df` contains exogenous calendar features and `S_df` contains static features for each time-series. For this example we will only use `Y_df`. If you want to use your own data just replace `Y_df`. Be sure to use a long format and have a similar structure than our data set. ```python theme={null} Y_df_M3, _, _ = M3.load(directory='./', group='Monthly') ``` In this tutorial we are only using `1_000` series to speed up computations. Remove the filter to use the whole dataset. ```python theme={null} fig = plot_series(Y_df_M3) ``` ## Model Training Using the `MLForecast.fit` method you can train a set of models to your dataset. You can modify the hyperparameters of the model to get a better accuracy, in this case we will use the default hyperparameters of `lgb.LGBMRegressor`. ```python theme={null} models = [lgb.LGBMRegressor(verbosity=-1)] ``` The `MLForecast` object has the following parameters: * `models`: a list of sklearn-like (`fit` and `predict`) models. * `freq`: a string indicating the frequency of the data. See [panda’s available frequencies.](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases) * `differences`: Differences to take of the target before computing the features. These are restored at the forecasting step. * `lags`: Lags of the target to use as features. In this example, we are only using `differences` and `lags` to produce features. See [the full documentation](https://nixtlaverse.nixtla.io/mlforecast/forecast.html) to see all available features. Any settings are passed into the constructor. Then you call its `fit` method and pass in the historical data frame `Y_df_M3`. ```python theme={null} fcst = MLForecast( models=models, lags=range(1, 13), freq='MS', target_transforms=[Differences([1, 12])], ) fcst.fit(Y_df_M3); ``` ## Transfer M3 to AirPassengers Now we can transfer the trained model to forecast `AirPassengers` with the `MLForecast.predict` method, we just have to pass the new dataframe to the `new_data` argument. ```python theme={null} Y_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/air-passengers.csv', parse_dates=['ds']) # We define the train df. Y_train_df = Y_df[Y_df.ds<='1959-12-31'] # 132 train Y_test_df = Y_df[Y_df.ds>'1959-12-31'] # 12 test ``` ```python theme={null} Y_hat_df = fcst.predict(h=12, new_df=Y_train_df) Y_hat_df.head() ``` | | unique\_id | ds | LGBMRegressor | | - | ------------- | ---------- | ------------- | | 0 | AirPassengers | 1960-01-01 | 422.740096 | | 1 | AirPassengers | 1960-02-01 | 399.480193 | | 2 | AirPassengers | 1960-03-01 | 458.220289 | | 3 | AirPassengers | 1960-04-01 | 442.960385 | | 4 | AirPassengers | 1960-05-01 | 461.700482 | ```python theme={null} Y_hat_df = Y_test_df.merge(Y_hat_df, how='left', on=['unique_id', 'ds']) ``` ```python theme={null} fig = plot_series(Y_train_df, Y_hat_df) ``` ## Evaluate Results We evaluate the forecasts of the pre-trained model with the Mean Absolute Error (`mae`). $$ \qquad MAE = \frac{1}{Horizon} \sum_{\tau} |y_{\tau} - \hat{y}_{\tau}|\qquad $$ ```python theme={null} y_true = Y_test_df.y.values y_hat = Y_hat_df['LGBMRegressor'].values ``` ```python theme={null} print(f'LGBMRegressor MAE: {mean_absolute_error(y_hat, y_true):.3f}') print('ETS MAE: 16.222') print('AutoARIMA MAE: 18.551') ``` ```text theme={null} LGBMRegressor MAE: 13.560 ETS MAE: 16.222 AutoARIMA MAE: 18.551 ``` # Transforming exogenous features Source: https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/transforming_exog.html > Compute transformations on your exogenous features for MLForecast The MLForecast class allows you to compute lag transformations on your target, however, sometimes you want to also compute transformations on your dynamic exogenous features. This guide shows you how to accomplish that. ## Data setup ```python theme={null} from mlforecast.utils import generate_series, generate_prices_for_series ``` ```python theme={null} series = generate_series(10, equal_ends=True) prices = generate_prices_for_series(series) prices.head(2) ``` | | ds | unique\_id | price | | - | ---------- | ---------- | -------- | | 0 | 2000-10-05 | 0 | 0.548814 | | 1 | 2000-10-06 | 0 | 0.715189 | Suppose that you have some series along with their prices for each id and date and you want to compute forecasts for the next 7 days. Since the price is a dynamic feature you have to provide the future values through `X_df` in `MLForecast.predict`. If you want to use not only the price but the lag7 of the price and the expanding mean of the lag1 for example, you can compute them before training, merge them with your series and then provide the future values through `X_df`. Consider the following example. ## Computing the transformations ```python theme={null} from mlforecast.lag_transforms import ExpandingMean from mlforecast.feature_engineering import transform_exog ``` ```python theme={null} transformed_prices = transform_exog(prices, lags=[7], lag_transforms={1: [ExpandingMean()]}) transformed_prices.head(10) ``` | | ds | unique\_id | price | price\_lag7 | price\_expanding\_mean\_lag1 | | - | ---------- | ---------- | -------- | ----------- | ---------------------------- | | 0 | 2000-10-05 | 0 | 0.548814 | NaN | NaN | | 1 | 2000-10-06 | 0 | 0.715189 | NaN | 0.548814 | | 2 | 2000-10-07 | 0 | 0.602763 | NaN | 0.632001 | | 3 | 2000-10-08 | 0 | 0.544883 | NaN | 0.622255 | | 4 | 2000-10-09 | 0 | 0.423655 | NaN | 0.602912 | | 5 | 2000-10-10 | 0 | 0.645894 | NaN | 0.567061 | | 6 | 2000-10-11 | 0 | 0.437587 | NaN | 0.580200 | | 7 | 2000-10-12 | 0 | 0.891773 | 0.548814 | 0.559827 | | 8 | 2000-10-13 | 0 | 0.963663 | 0.715189 | 0.601320 | | 9 | 2000-10-14 | 0 | 0.383442 | 0.602763 | 0.641580 | You can now merge this with your original series ```python theme={null} series_with_prices = series.merge(transformed_prices, on=['unique_id', 'ds']) series_with_prices.head(10) ``` | | unique\_id | ds | y | price | price\_lag7 | price\_expanding\_mean\_lag1 | | - | ---------- | ---------- | -------- | -------- | ----------- | ---------------------------- | | 0 | 0 | 2000-10-05 | 0.322947 | 0.548814 | NaN | NaN | | 1 | 0 | 2000-10-06 | 1.218794 | 0.715189 | NaN | 0.548814 | | 2 | 0 | 2000-10-07 | 2.445887 | 0.602763 | NaN | 0.632001 | | 3 | 0 | 2000-10-08 | 3.481831 | 0.544883 | NaN | 0.622255 | | 4 | 0 | 2000-10-09 | 4.191721 | 0.423655 | NaN | 0.602912 | | 5 | 0 | 2000-10-10 | 5.395863 | 0.645894 | NaN | 0.567061 | | 6 | 0 | 2000-10-11 | 6.264447 | 0.437587 | NaN | 0.580200 | | 7 | 0 | 2000-10-12 | 0.284022 | 0.891773 | 0.548814 | 0.559827 | | 8 | 0 | 2000-10-13 | 1.462798 | 0.963663 | 0.715189 | 0.601320 | | 9 | 0 | 2000-10-14 | 2.035518 | 0.383442 | 0.602763 | 0.641580 | You can then define your forecast object. Note that you can still compute lag features based on the target as you normally would. ```python theme={null} from sklearn.linear_model import LinearRegression from mlforecast import MLForecast ``` ```python theme={null} fcst = MLForecast( models=[LinearRegression()], freq='D', lags=[1], date_features=['dayofweek'], ) fcst.preprocess(series_with_prices, static_features=[], dropna=True).head() ``` | | unique\_id | ds | y | price | price\_lag7 | price\_expanding\_mean\_lag1 | lag1 | dayofweek | | - | ---------- | ---------- | -------- | -------- | ----------- | ---------------------------- | -------- | --------- | | 1 | 0 | 2000-10-06 | 1.218794 | 0.715189 | NaN | 0.548814 | 0.322947 | 4 | | 2 | 0 | 2000-10-07 | 2.445887 | 0.602763 | NaN | 0.632001 | 1.218794 | 5 | | 3 | 0 | 2000-10-08 | 3.481831 | 0.544883 | NaN | 0.622255 | 2.445887 | 6 | | 4 | 0 | 2000-10-09 | 4.191721 | 0.423655 | NaN | 0.602912 | 3.481831 | 0 | | 5 | 0 | 2000-10-10 | 5.395863 | 0.645894 | NaN | 0.567061 | 4.191721 | 1 | It’s important to note that the `dropna` argument only considers the null values generated by the lag features based on the target. If you want to drop all rows containing null values you have to do that in your original series. ```python theme={null} series_with_prices2 = series_with_prices.dropna() fcst.preprocess(series_with_prices2, dropna=True, static_features=[]).head() ``` | | unique\_id | ds | y | price | price\_lag7 | price\_expanding\_mean\_lag1 | lag1 | dayofweek | | -- | ---------- | ---------- | -------- | -------- | ----------- | ---------------------------- | -------- | --------- | | 8 | 0 | 2000-10-13 | 1.462798 | 0.963663 | 0.715189 | 0.601320 | 0.284022 | 4 | | 9 | 0 | 2000-10-14 | 2.035518 | 0.383442 | 0.602763 | 0.641580 | 1.462798 | 5 | | 10 | 0 | 2000-10-15 | 3.043565 | 0.791725 | 0.544883 | 0.615766 | 2.035518 | 6 | | 11 | 0 | 2000-10-16 | 4.010109 | 0.528895 | 0.423655 | 0.631763 | 3.043565 | 0 | | 12 | 0 | 2000-10-17 | 5.416310 | 0.568045 | 0.645894 | 0.623190 | 4.010109 | 1 | You can now train the model. ```python theme={null} fcst.fit(series_with_prices2, static_features=[]) ``` ```text theme={null} MLForecast(models=[LinearRegression], freq=D, lag_features=['lag1'], date_features=['dayofweek'], num_threads=1) ``` And predict using the prices. Note that you can provide the dataframe with the full history and mlforecast will filter the required dates for the forecasting horizon. ```python theme={null} fcst.predict(1, X_df=transformed_prices).head() ``` | | unique\_id | ds | LinearRegression | | - | ---------- | ---------- | ---------------- | | 0 | 0 | 2001-05-15 | 3.803967 | | 1 | 1 | 2001-05-15 | 3.512489 | | 2 | 2 | 2001-05-15 | 3.170019 | | 3 | 3 | 2001-05-15 | 4.307121 | | 4 | 4 | 2001-05-15 | 3.018758 | In this example we have prices for the next 7 days, if you try to forecast a longer horizon you’ll get an error. ```python theme={null} from fastcore.test import test_fail ``` ```python theme={null} test_fail(lambda: fcst.predict(8, X_df=transformed_prices), contains='Found missing inputs in X_df') ``` # Electricity Load Forecast | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/docs/tutorials/electricity_load_forecasting.html > In this example we will show how to perform electricity load > forecasting using MLForecast alongside many models. We also compare > them against the prophet library. ## Introduction Some time series are generated from very low frequency data. These data generally exhibit multiple seasonalities. For example, hourly data may exhibit repeated patterns every hour (every 24 observations) or every day (every 24 \* 7, hours per day, observations). This is the case for electricity load. Electricity load may vary hourly, e.g., during the evenings electricity consumption may be expected to increase. But also, the electricity load varies by week. Perhaps on weekends there is an increase in electrical activity. In this example we will show how to model the two seasonalities of the time series to generate accurate forecasts in a short time. We will use hourly PJM electricity load data. The original data can be found [here](https://www.kaggle.com/datasets/robikscube/hourly-energy-consumption). ## Libraries In this example we will use the following libraries: * [`mlforecast`](https://nixtlaverse.nixtla.io/mlforecast/). Accurate and ⚡️ fast forecasting with classical machine learning models. * [`prophet`](https://github.com/facebook/prophet). Benchmark model developed by Facebook. * [`utilsforecast`](https://nixtlaverse.nixtla.io/utilsforecast/). Library with different functions for forecasting evaluation. If you have already installed the libraries you can skip the next cell, if not be sure to run it. ```python theme={null} # %%capture # !pip install prophet # !pip install -U mlforecast # !pip install -U utilsforecast ``` ## Forecast using Multiple Seasonalities ### Electricity Load Data According to the [dataset’s page](https://www.kaggle.com/datasets/robikscube/hourly-energy-consumption), > PJM Interconnection LLC (PJM) is a regional transmission organization > (RTO) in the United States. It is part of the Eastern Interconnection > grid operating an electric transmission system serving all or parts of > Delaware, Illinois, Indiana, Kentucky, Maryland, Michigan, New Jersey, > North Carolina, Ohio, Pennsylvania, Tennessee, Virginia, West > Virginia, and the District of Columbia. The hourly power consumption > data comes from PJM’s website and are in megawatts (MW). Let’s take a look to the data. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import pandas as pd from utilsforecast.plotting import plot_series ``` ```python theme={null} pd.plotting.register_matplotlib_converters() plt.rc("figure", figsize=(10, 8)) plt.rc("font", size=10) ``` ```python theme={null} data_url = 'https://raw.githubusercontent.com/panambY/Hourly_Energy_Consumption/master/data/PJM_Load_hourly.csv' df = pd.read_csv(data_url, parse_dates=['Datetime']) df.columns = ['ds', 'y'] df.insert(0, 'unique_id', 'PJM_Load_hourly') df['ds'] = pd.to_datetime(df['ds']) df = df.sort_values(['unique_id', 'ds']).reset_index(drop=True) print(f'Shape of the data {df.shape}') df.tail() ``` ```text theme={null} Shape of the data (32896, 3) ``` | | unique\_id | ds | y | | ----- | ----------------- | ------------------- | ------- | | 32891 | PJM\_Load\_hourly | 2001-12-31 20:00:00 | 36392.0 | | 32892 | PJM\_Load\_hourly | 2001-12-31 21:00:00 | 35082.0 | | 32893 | PJM\_Load\_hourly | 2001-12-31 22:00:00 | 33890.0 | | 32894 | PJM\_Load\_hourly | 2001-12-31 23:00:00 | 32590.0 | | 32895 | PJM\_Load\_hourly | 2002-01-01 00:00:00 | 31569.0 | ```python theme={null} fig = plot_series(df) ``` We clearly observe that the time series exhibits seasonal patterns. Moreover, the time series contains `32,896` observations, so it is necessary to use very computationally efficient methods to display them in production. We are going to split our series in order to create a train and test set. The model will be tested using the last 24 hours of the timeseries. ```python theme={null} threshold_time = df['ds'].max() - pd.Timedelta(hours=24) # Split the dataframe df_train = df[df['ds'] <= threshold_time] df_last_24_hours = df[df['ds'] > threshold_time] ``` ### Analizing Seasonalities First we must visualize the seasonalities of the model. As mentioned before, the electricity load presents seasonalities every 24 hours (Hourly) and every 24 \* 7 (Daily) hours. Therefore, we will use `[24, 24 * 7]` as the seasonalities for the model. In order to analize how they affect our series we are going to use the `Difference` method. ```python theme={null} from mlforecast import MLForecast from mlforecast.target_transforms import Differences ``` We can use the `MLForecast.preprocess` method to explore different transformations. It looks like these series have a strong seasonality on the hour of the day, so we can subtract the value from the same hour in the previous day to remove it. This can be done with the `mlforecast.target_transforms.Differences` transformer, which we pass through `target_transforms`. In order to analize the trends individually and combined we are going to plot them individually and combined. Therefore, we can compare them against the original series. We can use the next function for that. ```python theme={null} def plot_differences(df, differences,fname): prep = [df] # Plot individual Differences for d in differences: fcst = MLForecast( models=[], # we're not interested in modeling yet freq='H', # our series have hourly frequency target_transforms=[Differences([d])], ) df_ = fcst.preprocess(df) df_['unique_id'] = df_['unique_id'] + f'_{d}' prep.append(df_) # Plot combined Differences fcst = MLForecast( models=[], # we're not interested in modeling yet freq='H', # our series have hourly frequency target_transforms=[Differences([24, 24*7])], ) df_ = fcst.preprocess(df) df_['unique_id'] = df_['unique_id'] + f'_all_diff' prep.append(df_) prep = pd.concat(prep, ignore_index=True) #return prep n_series = len(prep['unique_id'].unique()) fig, ax = plt.subplots(nrows=n_series, figsize=(7 * n_series, 10*n_series), squeeze=False) for title, axi in zip(prep['unique_id'].unique(), ax.flat): df_ = prep[prep['unique_id'] == title] df_.set_index('ds')['y'].plot(title=title, ax=axi) fig.savefig(f'../../figs/{fname}', bbox_inches='tight') plt.close() ``` Since the seasonalities are present at `24` hours (daily) and `24*7` (weekly) we are going to subtract them from the serie using `Differences([24, 24*7])` and plot them. ```python theme={null} plot_differences(df=df_train, differences=[24, 24*7], fname='load_forecasting__differences.png') ``` As we can see when we extract the 24 difference (daily) in `PJM_Load_hourly_24` the series seem to stabilize since the peaks seem more uniform in comparison with the original series `PJM_Load_hourly`. When we extract the 24\*7 (weekly) `PJM_Load_hourly_168` difference we can see there is more periodicity in the peaks in comparison with the original series. Finally we can see the result from the combined result from subtracting all the differences `PJM_Load_hourly_all_diff`. For modeling we are going to use both difference for the forecasting, therefore we are setting the argument `target_transforms` from the `MLForecast` object equal to `[Differences([24, 24*7])]`, if we wanted to include a yearly difference we would need to add the term `24*365`. ```python theme={null} fcst = MLForecast( models=[], # we're not interested in modeling yet freq='H', # our series have hourly frequency target_transforms=[Differences([24, 24*7])], ) prep = fcst.preprocess(df_train) prep ``` | | unique\_id | ds | y | | ----- | ----------------- | ------------------- | ------ | | 192 | PJM\_Load\_hourly | 1998-04-09 02:00:00 | 831.0 | | 193 | PJM\_Load\_hourly | 1998-04-09 03:00:00 | 918.0 | | 194 | PJM\_Load\_hourly | 1998-04-09 04:00:00 | 760.0 | | 195 | PJM\_Load\_hourly | 1998-04-09 05:00:00 | 849.0 | | 196 | PJM\_Load\_hourly | 1998-04-09 06:00:00 | 710.0 | | ... | ... | ... | ... | | 32867 | PJM\_Load\_hourly | 2001-12-30 20:00:00 | 3417.0 | | 32868 | PJM\_Load\_hourly | 2001-12-30 21:00:00 | 3596.0 | | 32869 | PJM\_Load\_hourly | 2001-12-30 22:00:00 | 3501.0 | | 32870 | PJM\_Load\_hourly | 2001-12-30 23:00:00 | 3939.0 | | 32871 | PJM\_Load\_hourly | 2001-12-31 00:00:00 | 4235.0 | ```python theme={null} fig = plot_series(prep) ``` ### Model Selection with Cross-Validation We can test many models simultaneously using MLForecast `cross_validation`. We can import `lightgbm` and `scikit-learn` models and try different combinations of them, alongside different target transformations (as the ones we created previously) and historical variables.\ You can see an in-depth tutorial on how to use `MLForecast` [Cross Validation methods here](https://nixtlaverse.nixtla.io/mlforecast/docs/how-to-guides/cross_validation.html) ```python theme={null} import lightgbm as lgb from sklearn.base import BaseEstimator from sklearn.linear_model import Lasso, LinearRegression, Ridge from sklearn.neighbors import KNeighborsRegressor from sklearn.neural_network import MLPRegressor from sklearn.ensemble import RandomForestRegressor from mlforecast.lag_transforms import ExpandingMean, RollingMean from mlforecast.target_transforms import Differences ``` We can create a benchmark `Naive` model that uses the electricity load of the last hour as prediction `lag1` as showed in the next cell. You can create your own models and try them with `MLForecast` using the same structure. ```python theme={null} class Naive(BaseEstimator): def fit(self, X, y): return self def predict(self, X): return X['lag1'] ``` Now let’s try differen models from the `scikit-learn` library: `Lasso`, `LinearRegression`, `Ridge`, `KNN`, `MLP` and `Random Forest` alongside the `LightGBM`. You can add any model to the dictionary to train and compare them by adding them to the dictionary (`models`) as shown. ```python theme={null} # Model dictionary models ={ 'naive': Naive(), 'lgbm': lgb.LGBMRegressor(verbosity=-1), 'lasso': Lasso(), 'lin_reg': LinearRegression(), 'ridge': Ridge(), 'knn': KNeighborsRegressor(), 'mlp': MLPRegressor(), 'rf': RandomForestRegressor() } ``` The we can instanciate the `MLForecast` class with the models we want to try along side `target_transforms`, `lags`, `lag_transforms`, and `date_features`. All this features are applied to the models we selected. In this case we use the 1st, 12th and 24th lag, which are passed as a list. Potentially you could pass a `range`. ```text theme={null} lags=[1,12,24] ``` Lag transforms are defined as a dictionary where the keys are the lags and the values are lists of the transformations that we want to apply to that lag. You can refer to the [lag transformations guide](../how-to-guides/lag_transforms_guide.html) for more details. For using the date features you need to be sure that your time column is made of timestamps. Then it might make sense to extract features like week, dayofweek, quarter, etc. You can do that by passing a list of strings with [pandas time/date components](https://pandas.pydata.org/docs/user_guide/timeseries.html#time-date-components). You can also pass functions that will take the time column as input, as we’ll show here.\ Here we add month, hour and dayofweek features: ```text theme={null} date_features=['month', 'hour', 'dayofweek'] ``` ```python theme={null} mlf = MLForecast( models = models, freq='H', # our series have hourly frequency target_transforms=[Differences([24, 24*7])], lags=[1,12,24], # Lags to be used as features lag_transforms={ 1: [ExpandingMean()], 24: [RollingMean(window_size=48)], }, date_features=['month', 'hour', 'dayofweek'] ) ``` Now we use the `cross_validation` method to train and evalaute the models. + `df`: Receives the training data + `h`: Forecast horizon + `n_windows`: The number of folds we want to predict You can specify the names of the time series id, time and target columns. + `id_col`:Column that identifies each serie ( Default *unique\_id* ) + `time_col`: Column that identifies each timestep, its values can be timestamps or integer( Default *ds* ) + `target_col`:Column that contains the target ( Default *y* ) ```python theme={null} crossvalidation_df = mlf.cross_validation( df=df_train, h=24, n_windows=4, refit=False, ) crossvalidation_df.head() ``` | | unique\_id | ds | cutoff | y | naive | lgbm | lasso | lin\_reg | ridge | knn | mlp | rf | | - | ----------------- | ------------------- | ---------- | ------- | ------- | ------------ | ------------ | ------------ | ------------ | ------- | ------------ | -------- | | 0 | PJM\_Load\_hourly | 2001-12-27 01:00:00 | 2001-12-27 | 28332.0 | 28837.0 | 28526.505572 | 28703.185712 | 28702.625949 | 28702.625956 | 28479.0 | 28660.021947 | 27995.17 | | 1 | PJM\_Load\_hourly | 2001-12-27 02:00:00 | 2001-12-27 | 27329.0 | 27969.0 | 27467.860847 | 27693.502318 | 27692.395954 | 27692.395969 | 27521.6 | 27584.635434 | 27112.50 | | 2 | PJM\_Load\_hourly | 2001-12-27 03:00:00 | 2001-12-27 | 26986.0 | 27435.0 | 26605.710615 | 26991.795124 | 26990.157567 | 26990.157589 | 26451.6 | 26809.412477 | 26529.72 | | 3 | PJM\_Load\_hourly | 2001-12-27 04:00:00 | 2001-12-27 | 27009.0 | 27401.0 | 26284.065138 | 26789.418399 | 26787.262262 | 26787.262291 | 26388.4 | 26523.416348 | 26490.83 | | 4 | PJM\_Load\_hourly | 2001-12-27 05:00:00 | 2001-12-27 | 27555.0 | 28169.0 | 26823.617078 | 27369.643789 | 27366.983075 | 27366.983111 | 26779.6 | 26986.355992 | 27180.69 | Now we can plot each model and window (fold) to see how it behaves ```python theme={null} def plot_cv(df, df_cv, uid, fname, last_n=24 * 14, models={}): cutoffs = df_cv.query('unique_id == @uid')['cutoff'].unique() fig, ax = plt.subplots(nrows=len(cutoffs), ncols=1, figsize=(14, 14), gridspec_kw=dict(hspace=0.8)) for cutoff, axi in zip(cutoffs, ax.flat): max_date = df_cv.query('unique_id == @uid & cutoff == @cutoff')['ds'].max() df[df['ds'] < max_date].query('unique_id == @uid').tail(last_n).set_index('ds').plot(ax=axi, title=uid, y='y') for m in models.keys(): df_cv.query('unique_id == @uid & cutoff == @cutoff').set_index('ds').plot(ax=axi, title=uid, y=m) fig.savefig(f'../../figs/{fname}', bbox_inches='tight') plt.close() ``` ```python theme={null} plot_cv(df_train, crossvalidation_df, 'PJM_Load_hourly', 'load_forecasting__predictions.png', models=models) ``` Visually examining the forecasts can give us some idea of how the model is behaving, yet in order to asses the performace we need to evaluate them trough metrics. For that we use the [utilsforecast](https://nixtlaverse.nixtla.io/utilsforecast/) library that contains many useful metrics and an evaluate function. ```python theme={null} from utilsforecast.losses import mae, mape, rmse, smape from utilsforecast.evaluation import evaluate ``` ```python theme={null} # Metrics to be used for evaluation metrics = [ mae, rmse, mape, smape ] ``` ```python theme={null} # Function to evaluate the crossvalidation def evaluate_crossvalidation(crossvalidation_df, metrics, models): evaluations = [] for c in crossvalidation_df['cutoff'].unique(): df_cv = crossvalidation_df.query('cutoff == @c') evaluation = evaluate( df = df_cv, metrics=metrics, models=list(models.keys()) ) evaluations.append(evaluation) evaluations = pd.concat(evaluations, ignore_index=True).drop(columns='unique_id') evaluations = evaluations.groupby('metric').mean() return evaluations.style.background_gradient(cmap='RdYlGn_r', axis=1) ``` ```python theme={null} evaluate_crossvalidation(crossvalidation_df, metrics, models) ``` |   | naive | lgbm | lasso | lin\_reg | ridge | knn | mlp | rf | | ------ | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | | metric |   |   |   |   |   |   |   |   | | mae | 1631.395833 | 971.536200 | 1003.796433 | 1007.998597 | 1007.998547 | 1248.145833 | 1870.547722 | 1017.957813 | | mape | 0.049759 | 0.030966 | 0.031760 | 0.031888 | 0.031888 | 0.038721 | 0.057504 | 0.032341 | | rmse | 1871.398919 | 1129.713256 | 1148.616156 | 1153.262719 | 1153.262664 | 1451.964390 | 2102.098238 | 1154.647164 | | smape | 0.024786 | 0.015886 | 0.016269 | 0.016338 | 0.016338 | 0.019549 | 0.029917 | 0.016563 | We can see that the model `lgbm` has top performance in most metrics followed by the `lasso regression`. Both models perform way better than the `naive`. ### Test Evaluation Now we are going to evaluate their performance in the test set. We can use both of them for forecasting the test alongside some prediction intervals. For that we can use the [`PredictionIntervals`](https://nixtlaverse.nixtla.io/mlforecast/utils.html#predictionintervals) function in `mlforecast.utils`.\ You can see an in-depth tutorial of [Probabilistic Forecasting here](https://nixtlaverse.nixtla.io/mlforecast/docs/tutorials/prediction_intervals_in_forecasting_models.html) ```python theme={null} from mlforecast.utils import PredictionIntervals ``` ```python theme={null} models_evaluation ={ 'lgbm': lgb.LGBMRegressor(verbosity=-1), 'lasso': Lasso(), } mlf_evaluation = MLForecast( models = models_evaluation, freq='H', # our series have hourly frequency target_transforms=[Differences([24, 24*7])], lags=[1,12,24], lag_transforms={ 1: [ExpandingMean()], 24: [RollingMean(window_size=48)], }, date_features=['month', 'hour', 'dayofweek'] ) ``` Now we’re ready to generate the point forecasts and the prediction intervals. To do this, we’ll use the `fit` method, which takes the following arguments: * `df`: Series data in long format. * `id_col`: Column that identifies each series. In our case, unique\_id. * `time_col`: Column that identifies each timestep, its values can be timestamps or integers. In our case, ds. * `target_col`: Column that contains the target. In our case, y. The `PredictionIntervals` function is used to compute prediction intervals for the models using [Conformal Prediction](https://valeman.medium.com/how-to-predict-full-probability-distribution-using-machine-learning-conformal-predictive-f8f4d805e420). The function takes the following arguments: + `n_windows`: represents the number of cross-validation windows used to calibrate the intervals + `h`: the forecast horizon ```python theme={null} mlf_evaluation.fit( df = df_train, prediction_intervals=PredictionIntervals(n_windows=4, h=24) ) ``` ```text theme={null} MLForecast(models=[lgbm, lasso], freq=H, lag_features=['lag1', 'lag12', 'lag24', 'expanding_mean_lag1', 'rolling_mean_lag24_window_size48'], date_features=['month', 'hour', 'dayofweek'], num_threads=1) ``` Now that the model has been trained we are going to forecast the next 24 hours using the `predict` method so we can compare them to our `test` data. Additionally, we are going to create prediction intervals at `levels` `[90,95]`. ```python theme={null} levels = [90, 95] # Levels for prediction intervals forecasts = mlf_evaluation.predict(24, level=levels) forecasts.head() ``` | | unique\_id | ds | lgbm | lasso | lgbm-lo-95 | lgbm-lo-90 | lgbm-hi-90 | lgbm-hi-95 | lasso-lo-95 | lasso-lo-90 | lasso-hi-90 | lasso-hi-95 | | - | ----------------- | ------------------- | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | | 0 | PJM\_Load\_hourly | 2001-12-31 01:00:00 | 28847.573176 | 29124.085976 | 28544.593464 | 28567.603130 | 29127.543222 | 29150.552888 | 28762.752269 | 28772.604275 | 29475.567677 | 29485.419682 | | 1 | PJM\_Load\_hourly | 2001-12-31 02:00:00 | 27862.589195 | 28365.330749 | 27042.311414 | 27128.839888 | 28596.338503 | 28682.866977 | 27528.548959 | 27619.065224 | 29111.596275 | 29202.112539 | | 2 | PJM\_Load\_hourly | 2001-12-31 03:00:00 | 27044.418960 | 27712.161676 | 25596.659896 | 25688.230426 | 28400.607493 | 28492.178023 | 26236.955369 | 26338.087102 | 29086.236251 | 29187.367984 | | 3 | PJM\_Load\_hourly | 2001-12-31 04:00:00 | 26976.104125 | 27661.572733 | 25249.961527 | 25286.024722 | 28666.183529 | 28702.246724 | 25911.133521 | 25959.815715 | 29363.329750 | 29412.011944 | | 4 | PJM\_Load\_hourly | 2001-12-31 05:00:00 | 26694.246238 | 27393.922370 | 25044.220845 | 25051.548832 | 28336.943644 | 28344.271631 | 25751.547897 | 25762.524815 | 29025.319924 | 29036.296843 | The `predict` method returns a DataFrame witht the predictions for each model (`lasso` and `lgbm`) along side the prediction tresholds. The high-threshold is indicated by the keyword `hi`, the low-threshold by the keyword `lo`, and the level by the number in the column names. ```python theme={null} test = df_last_24_hours.merge(forecasts, how='left', on=['unique_id', 'ds']) test.head() ``` | | unique\_id | ds | y | lgbm | lasso | lgbm-lo-95 | lgbm-lo-90 | lgbm-hi-90 | lgbm-hi-95 | lasso-lo-95 | lasso-lo-90 | lasso-hi-90 | lasso-hi-95 | | - | ----------------- | ------------------- | ------- | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | | 0 | PJM\_Load\_hourly | 2001-12-31 01:00:00 | 29001.0 | 28847.573176 | 29124.085976 | 28544.593464 | 28567.603130 | 29127.543222 | 29150.552888 | 28762.752269 | 28772.604275 | 29475.567677 | 29485.419682 | | 1 | PJM\_Load\_hourly | 2001-12-31 02:00:00 | 28138.0 | 27862.589195 | 28365.330749 | 27042.311414 | 27128.839888 | 28596.338503 | 28682.866977 | 27528.548959 | 27619.065224 | 29111.596275 | 29202.112539 | | 2 | PJM\_Load\_hourly | 2001-12-31 03:00:00 | 27830.0 | 27044.418960 | 27712.161676 | 25596.659896 | 25688.230426 | 28400.607493 | 28492.178023 | 26236.955369 | 26338.087102 | 29086.236251 | 29187.367984 | | 3 | PJM\_Load\_hourly | 2001-12-31 04:00:00 | 27874.0 | 26976.104125 | 27661.572733 | 25249.961527 | 25286.024722 | 28666.183529 | 28702.246724 | 25911.133521 | 25959.815715 | 29363.329750 | 29412.011944 | | 4 | PJM\_Load\_hourly | 2001-12-31 05:00:00 | 28427.0 | 26694.246238 | 27393.922370 | 25044.220845 | 25051.548832 | 28336.943644 | 28344.271631 | 25751.547897 | 25762.524815 | 29025.319924 | 29036.296843 | Now we can evaluate the metrics and performance in the `test` set. ```python theme={null} evaluate( df = test, metrics=metrics, models=list(models_evaluation.keys()) ) ``` | | unique\_id | metric | lgbm | lasso | | - | ----------------- | ------ | ----------- | ----------- | | 0 | PJM\_Load\_hourly | mae | 1092.050817 | 899.979743 | | 1 | PJM\_Load\_hourly | rmse | 1340.422762 | 1163.695525 | | 2 | PJM\_Load\_hourly | mape | 0.033600 | 0.027688 | | 3 | PJM\_Load\_hourly | smape | 0.017137 | 0.013812 | We can see that the `lasso` regression performed slightly better than the `LightGBM` for the test set. Additionally, we can also plot the forecasts alongside their prediction intervals. For that we can use the `plot_series` method available in `utilsforecast.plotting`. We can plot one or many models at once alongside their confidence intervals. ```python theme={null} fig = plot_series( df_train, test, models=['lasso', 'lgbm'], plot_random=False, level=levels, max_insample_length=24 ) ``` ### Comparison with Prophet One of the most widely used models for time series forecasting is `Prophet`. This model is known for its ability to model different seasonalities (weekly, daily yearly). We will use this model as a benchmark to see if the `lgbm` alongside `MLForecast` adds value for this time series. ```python theme={null} from prophet import Prophet from time import time ``` ```text theme={null} Importing plotly failed. Interactive plots will not work. ``` ```python theme={null} # create prophet model prophet = Prophet(interval_width=0.9) init = time() prophet.fit(df_train) # produce forecasts future = prophet.make_future_dataframe(periods=len(df_last_24_hours), freq='H', include_history=False) forecast_prophet = prophet.predict(future) end = time() # data wrangling forecast_prophet = forecast_prophet[['ds', 'yhat', 'yhat_lower', 'yhat_upper']] forecast_prophet.columns = ['ds', 'Prophet', 'Prophet-lo-90', 'Prophet-hi-90'] forecast_prophet.insert(0, 'unique_id', 'PJM_Load_hourly') forecast_prophet.head() ``` | | unique\_id | ds | Prophet | Prophet-lo-90 | Prophet-hi-90 | | - | ----------------- | ------------------- | ------------ | ------------- | ------------- | | 0 | PJM\_Load\_hourly | 2001-12-31 01:00:00 | 25333.448442 | 20589.873559 | 30370.174820 | | 1 | PJM\_Load\_hourly | 2001-12-31 02:00:00 | 24039.925936 | 18927.503487 | 29234.930903 | | 2 | PJM\_Load\_hourly | 2001-12-31 03:00:00 | 23363.998793 | 18428.462513 | 28292.424622 | | 3 | PJM\_Load\_hourly | 2001-12-31 04:00:00 | 23371.799609 | 18206.273446 | 28181.023448 | | 4 | PJM\_Load\_hourly | 2001-12-31 05:00:00 | 24146.468610 | 19356.171497 | 29006.546759 | ```python theme={null} time_prophet = (end - init) print(f'Prophet Time: {time_prophet:.2f} seconds') ``` ```text theme={null} Prophet Time: 18.00 seconds ``` ```python theme={null} models_comparison ={ 'lgbm': lgb.LGBMRegressor(verbosity=-1) } mlf_comparison = MLForecast( models = models_comparison, freq='H', # our series have hourly frequency target_transforms=[Differences([24, 24*7])], lags=[1,12,24], lag_transforms={ 1: [ExpandingMean()], 24: [RollingMean(window_size=48)], }, date_features=['month', 'hour', 'dayofweek'] ) init = time() mlf_comparison.fit( df = df_train, prediction_intervals=PredictionIntervals(n_windows=4, h=24) ) levels = [90] forecasts_comparison = mlf_comparison.predict(24, level=levels) end = time() forecasts_comparison.head() ``` | | unique\_id | ds | lgbm | lgbm-lo-90 | lgbm-hi-90 | | - | ----------------- | ------------------- | ------------ | ------------ | ------------ | | 0 | PJM\_Load\_hourly | 2001-12-31 01:00:00 | 28847.573176 | 28567.603130 | 29127.543222 | | 1 | PJM\_Load\_hourly | 2001-12-31 02:00:00 | 27862.589195 | 27128.839888 | 28596.338503 | | 2 | PJM\_Load\_hourly | 2001-12-31 03:00:00 | 27044.418960 | 25688.230426 | 28400.607493 | | 3 | PJM\_Load\_hourly | 2001-12-31 04:00:00 | 26976.104125 | 25286.024722 | 28666.183529 | | 4 | PJM\_Load\_hourly | 2001-12-31 05:00:00 | 26694.246238 | 25051.548832 | 28336.943644 | ```python theme={null} time_lgbm = (end - init) print(f'LGBM Time: {time_lgbm:.2f} seconds') ``` ```text theme={null} LGBM Time: 0.86 seconds ``` ```python theme={null} metrics_comparison = df_last_24_hours.merge(forecasts_comparison, how='left', on=['unique_id', 'ds']).merge( forecast_prophet, how='left', on=['unique_id', 'ds']) metrics_comparison = evaluate( df = metrics_comparison, metrics=metrics, models=['Prophet', 'lgbm'] ) metrics_comparison.reset_index(drop=True).style.background_gradient(cmap='RdYlGn_r', axis=1) ``` |   | unique\_id | metric | Prophet | lgbm | | - | ----------------- | ------ | ----------- | ----------- | | 0 | PJM\_Load\_hourly | mae | 2266.561642 | 1092.050817 | | 1 | PJM\_Load\_hourly | rmse | 2701.302779 | 1340.422762 | | 2 | PJM\_Load\_hourly | mape | 0.073226 | 0.033600 | | 3 | PJM\_Load\_hourly | smape | 0.038320 | 0.017137 | As we can see `lgbm` had consistently better metrics than `prophet`. ```python theme={null} metrics_comparison['improvement'] = metrics_comparison['Prophet'] / metrics_comparison['lgbm'] metrics_comparison['improvement'] = metrics_comparison['improvement'].apply(lambda x: f'{x:.2f}') metrics_comparison.set_index('metric')[['improvement']] ``` | | improvement | | ------ | ----------- | | metric | | | mae | 2.08 | | rmse | 2.02 | | mape | 2.18 | | smape | 2.24 | ```python theme={null} print(f'lgbm with MLForecast has a speedup of {time_prophet/time_lgbm:.2f} compared with prophet') ``` ```text theme={null} lgbm with MLForecast has a speedup of 20.95 compared with prophet ``` We can see that `lgbm` with `MLForecast` was able to provide metrics at least twice as good as `Prophet` as seen in the column `improvement` above, and way faster. # Detect Demand Peaks | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/docs/tutorials/electricity_peak_forecasting.html > In this example we will show how to perform electricity load > forecasting on the ERCOT (Texas) market for detecting daily peaks. ## Introduction Predicting peaks in different markets is useful. In the electricity market, consuming electricity at peak demand is penalized with higher tariffs. When an individual or company consumes electricity when its most demanded, regulators call that a coincident peak (CP). In the Texas electricity market (ERCOT), the peak is the monthly 15-minute interval when the ERCOT Grid is at a point of highest capacity. The peak is caused by all consumers’ combined demand on the electrical grid. The coincident peak demand is an important factor used by ERCOT to determine final electricity consumption bills. ERCOT registers the CP demand of each client for 4 months, between June and September, and uses this to adjust electricity prices. Clients can therefore save on electricity bills by reducing the coincident peak demand. In this example we will train a `LightGBM` model on historic load data to forecast day-ahead peaks on September 2022. Multiple seasonality is traditionally present in low sampled electricity data. Demand exhibits daily and weekly seasonality, with clear patterns for specific hours of the day such as 6:00pm vs 3:00am or for specific days such as Sunday vs Friday. First, we will load ERCOT historic demand, then we will use the `MLForecast.cross_validation` method to fit the `LightGBM` model and forecast daily load during September. Finally, we show how to use the forecasts to detect the coincident peak. **Outline** 1. Install libraries 2. Load and explore the data 3. Fit LightGBM model and forecast 4. Peak detection > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Libraries We assume you have MLForecast already installed. Check this guide for instructions on [how to install MLForecast](../getting-started/install.html). Install the necessary packages using `pip install mlforecast`. Also we have to install `LightGBM` using `pip install lightgbm`. ## Load Data The input to MLForecast is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestamp ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. We will rename the First, read the 2022 historic total demand of the ERCOT market. We processed the original data (available [here](https://www.ercot.com/gridinfo/load/load_hist)), by adding the missing hour due to daylight saving time, parsing the date to datetime format, and filtering columns of interest. ```python theme={null} import numpy as np import pandas as pd from utilsforecast.plotting import plot_series ``` ```python theme={null} # Load data Y_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/ERCOT-clean.csv', parse_dates=['ds']) Y_df = Y_df.query("ds >= '2022-01-01' & ds <= '2022-10-01'") ``` ```python theme={null} fig = plot_series(Y_df) ``` We observe that the time series exhibits seasonal patterns. Moreover, the time series contains `6,552` observations, so it is necessary to use computationally efficient methods to deploy them in production. ## Fit and Forecast LightGBM model Import the `MLForecast` class and the models you need. ```python theme={null} import lightgbm as lgb from mlforecast import MLForecast from mlforecast.target_transforms import Differences ``` First, instantiate the model and define the parameters. > **Tip** > > In this example we are using the default parameters of the > `lgb.LGBMRegressor` model, but you can change them to improve the > forecasting performance. ```python theme={null} models = [ lgb.LGBMRegressor(verbosity=-1) # you can include more models here ] ``` We fit the model by instantiating a `MLForecast` object with the following required parameters: * `models`: a list of sklearn-like (fit and predict) models. * `freq`: a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `target_transforms`: Transformations to apply to the target before computing the features. These are restored at the forecasting step. * `lags`: Lags of the target to use as features. ```python theme={null} # Instantiate MLForecast class as mlf mlf = MLForecast( models=models, freq='H', target_transforms=[Differences([24])], lags=range(1, 25) ) ``` > **Tip** > > In this example, we are only using differences and lags to produce > features. See the [full > documentation](https://nixtla.github.io/mlforecast/forecast.html#mlforecast) > to see all available features. The `cross_validation` method allows the user to simulate multiple historic forecasts, greatly simplifying pipelines by replacing for loops with `fit` and `predict` methods. This method re-trains the model and forecast each window. See [this tutorial](https://nixtla.github.io/statsforecast/examples/getting_started_complete.html) for an animation of how the windows are defined. Use the `cross_validation` method to produce all the daily forecasts for September. To produce daily forecasts set the forecasting horizon `window_size` as 24. In this example we are simulating deploying the pipeline during September, so set the number of windows as 30 (one for each day). Finally, the step size between windows is 24 (equal to the `window_size`). This ensure to only produce one forecast per day. Additionally, * `id_col`: identifies each time series. * `time_col`: indetifies the temporal column of the time series. * `target_col`: identifies the column to model. ```python theme={null} crossvalidation_df = mlf.cross_validation( df=Y_df, h=24, n_windows=30, ) ``` ```python theme={null} crossvalidation_df.head() ``` | | unique\_id | ds | cutoff | y | LGBMRegressor | | - | ---------- | ------------------- | ------------------- | ------------ | ------------- | | 0 | ERCOT | 2022-09-01 00:00:00 | 2022-08-31 23:00:00 | 45482.471757 | 45685.265537 | | 1 | ERCOT | 2022-09-01 01:00:00 | 2022-08-31 23:00:00 | 43602.658043 | 43779.819515 | | 2 | ERCOT | 2022-09-01 02:00:00 | 2022-08-31 23:00:00 | 42284.817342 | 42672.470923 | | 3 | ERCOT | 2022-09-01 03:00:00 | 2022-08-31 23:00:00 | 41663.156771 | 42091.768192 | | 4 | ERCOT | 2022-09-01 04:00:00 | 2022-08-31 23:00:00 | 41710.621904 | 42481.403168 | > **Important** > > When using `cross_validation` make sure the forecasts are produced at > the desired timestamps. Check the `cutoff` column which specifices the > last timestamp before the forecasting window. ## Peak Detection Finally, we use the forecasts in `crossvaldation_df` to detect the daily hourly demand peaks. For each day, we set the detected peaks as the highest forecasts. In this case, we want to predict one peak (`npeaks`); depending on your setting and goals, this parameter might change. For example, the number of peaks can correspond to how many hours a battery can be discharged to reduce demand. ```python theme={null} npeaks = 1 # Number of peaks ``` For the ERCOT 4CP detection task we are interested in correctly predicting the highest monthly load. Next, we filter the day in September with the highest hourly demand and predict the peak. ```python theme={null} crossvalidation_df = crossvalidation_df.reset_index()[['ds','y','LGBMRegressor']] max_day = crossvalidation_df.iloc[crossvalidation_df['y'].argmax()].ds.day # Day with maximum load cv_df_day = crossvalidation_df.query('ds.dt.day == @max_day') max_hour = cv_df_day['y'].argmax() peaks = cv_df_day['LGBMRegressor'].argsort().iloc[-npeaks:].values # Predicted peaks ``` In the following plot we see how the LightGBM model is able to correctly detect the coincident peak for September 2022. ```python theme={null} import matplotlib.pyplot as plt ``` ```python theme={null} fig, ax = plt.subplots(figsize=(10, 5)) ax.axvline(cv_df_day.iloc[max_hour]['ds'], color='black', label='True Peak') ax.scatter(cv_df_day.iloc[peaks]['ds'], cv_df_day.iloc[peaks]['LGBMRegressor'], color='green', label=f'Predicted Top-{npeaks}') ax.plot(cv_df_day['ds'], cv_df_day['y'], label='y', color='blue') ax.plot(cv_df_day['ds'], cv_df_day['LGBMRegressor'], label='Forecast', color='red') ax.set(xlabel='Time', ylabel='Load (MW)') ax.grid() ax.legend() fig.savefig('../../figs/electricity_peak_forecasting__predicted_peak.png', bbox_inches='tight') plt.close() ``` > **Important** > > In this example we only include September. However, MLForecast and > LightGBM can correctly predict the peaks for the 4 months of 2022. You > can try this by increasing the `n_windows` parameter of > `cross_validation` or filtering the `Y_df` dataset. ## Next steps MLForecast and LightGBM in particular are good benchmarking models for peak detection. However, it might be useful to explore further and newer forecasting algorithms or perform hyperparameter optimization. # Incremental Forecast generation Source: https://nixtlaverse.nixtla.io/mlforecast/docs/tutorials/incremental_forecasting.html This tutorial provides a practical framework for developing **incremental forecasting systems** using `MLForecast`. It walks through the full process of building scalable time series pipelines-from baseline model training to monthly incremental updates-illustrating how `update()` enables efficient, real-time forecast refreshes without full retraining. The focus is on designing **sustainable, production-grade forecasting workflows** that balance speed, adaptability, and long-term accuracy. ## **Table of Contents** 1. Introduction 2. Why Incremental Forecasting Matters 3. Why You Should Not Retrain at Every Step 4. When Full Retraining Becomes Necessary 5. A Hybrid Cadence for Reliable Forecasting Pipelines 6. Model Design and Implementation Flow 7. Comparing Incremental Updates and Full Retraining 8. Visual Analysis of Forecast Behavior 9. Conclusion ## **Introduction** When a forecasting system goes live, the flow of data doesn’t stop. Each month - or even each week - new observations arrive, and decision-makers expect your forecasts to adjust in real time. Yet, retraining the entire model every time new data comes in, is often the default reaction. It seems simple, but it’s computationally expensive, time-consuming, and can introduce instability into production workflows. This is where incremental forecasting becomes essential. Instead of retraining from scratch, we can incrementally update the model to reflect the latest data while preserving its learned patterns and parameters. In `MLForecast`, the `update()` method provides this capability by allowing a trained forecasting object to absorb new observations without retraining the underlying estimator. It extends the historical window for each series, recalculates lag and date features, and ensures that subsequent forecasts are generated using the most recent actuals. ## **Why You Shouldn’t Retrain Every Time** Once your forecasting system is operational, new data becomes a constant. Each cycle - be it daily, weekly, or monthly - brings new observations that reflect the latest market conditions. The instinctive reaction is to retrain the entire model whenever new data arrives, under the assumption that a fresh model guarantees higher accuracy. In practice, however, **frequent retraining is neither efficient nor necessary**. Retraining a global model across thousands of time series is computationally expensive and can introduce instability into production pipelines. Each retrain recalculates lag features, re-splits data, re-fits hyperparameters, and may slightly shift model weights due to stochastic effects, creating subtle variations in forecast outputs that can confuse downstream systems or decision-makers. Moreover, most new observations tend to reinforce existing patterns rather than alter them drastically. The **incremental update** approach offers a more robust alternative. Instead of discarding the existing model, MLForecast’s `update()` method allows you to append new observations to the existing historical window. It recalculates lag, lag\_transforms, and date features while keeping the learned model parameters fixed. This ensures that your forecasts stay aligned with the most recent actuals without triggering a full retraining cycle. The table below summarizes the difference between Retraining the model and Incremental Learning. | **Concern** | **Retraining Every Month** | **Using `update()` (Incremental Learning)** | | ----------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------ | | **Compute Cost** | High (retraining models for all SKUs consumes significant compute) | Minimal (only updates incremental weights or new samples) | | **Speed** | Slow (full retraining can take hours for large datasets) | Fast (updates often complete in seconds or minutes, even for many SKUs) | | **Model Stability** | May introduce noise or instability between training runs | Maintains consistent learned structure; less susceptible to random noise | | **Versioning Overhead** | Multiple model artifacts; difficult to track and manage | Single stable model with incremental updates; simpler to manage | | **Operational Risk** | Errors can propagate during frequent retraining cycles | Controlled, gradual updates reduce sudden failures | ## **When to Retrain Anyway** Incremental updates are powerful, but they are not a permanent substitute for model retraining. Over time, even the most robust forecasting systems experience **concept drift** - subtle or abrupt changes in the underlying data-generating process. When this happens, the relationships your model once learned no longer represent reality. In other words, the past stops being a reliable guide to the future. Retraining becomes necessary when the environment changes in ways that cannot be captured by simply appending new observations. Some common triggers include: #### 1. Structural breaks in the data Events such as product rebranding, changes in packaging, or shifts in demand patterns can cause discontinuities in historical trends. These “structural breaks” disrupt the temporal consistency that incremental updates rely on. A retraining cycle helps the model recalibrate to the new baseline. **How to identify structural changes** | Method Category | Technique | How it Works | When to Use | | ----------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **Classical structural break detection** | CUSUM, Bai Perron tests, Chow test | Detects statistically significant breaks in mean, variance, trend, or regression relationships by comparing parameter stability across segments | When regime changes are suspected due to events such as rebranding, packaging changes, policy interventions, or known breakpoint shifts | | **Residual and error diagnostics** | Residual pattern analysis, rolling RMSE or RMSSE | Identifies abrupt structural changes by detecting persistent bias, variance jumps, new autocorrelation patterns, or sudden degradation in performance metrics | When the model begins underpredicting or overpredicting in a consistent pattern or when accuracy drops after a structural shift | | **Machine learning based structural detection** | Drift classification models | Uses classification to distinguish between pre-event and post-event data, revealing multidimensional structural differences | When breaks are subtle, involve multiple features, or are not well captured by classical statistical tests | | **Domain driven indicators** | Business event logs, operational change tracking | Links structural changes to real business events such as assortment updates, supply chain disruptions, pricing regime shifts, or major operational interventions | When the structural break is caused by an external or managerial action rather than intrinsic time-series behaviour | #### 2. Distributional or Seasonal Drift If the statistical properties of the series - mean, variance, or seasonal amplitudes - start deviating consistently from past patterns, your lag-based features become less predictive. **How to identify distributional or seasonal drift** | Method Category | Technique | What it Detects | When to Use | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | **Distributional drift detection** | KS test, PSI, Jensen Shannon divergence, KL divergence | Detects shifts in mean, variance, shape, or overall distribution differences between historical and recent windows | When monitoring for gradual distributional drift in production systems | | **Residual and error diagnostics** | Rolling RMSSE, windowed performance metrics, residual distribution comparison | Detects sustained performance degradation or changes in residual bias, variance, or autocorrelation | When accuracy declines or residual patterns indicate emerging drift | | **Seasonality drift detection** | STL or MSTL seasonal comparison, seasonal correlation checks, spectral density analysis, seasonal strength metrics | Detects changes in seasonal amplitude, timing, periodicity, or variance explained by seasonality | When intra year seasonality evolves due to behavior, climate, or market changes | | **Machine learning based drift detection** | Drift classification model | Detects subtle or multidimensional drift by classifying old vs new windows | When drift affects multiple features simultaneously and is not captured by univariate tests | #### 3. Changes in Exogenous Features When the relationship between the target and external drivers changes, the model’s learned dependencies no longer reflect how the real world behaves. These changes often arise from broader business or environmental shifts rather than from the time series itself. For example, sudden changes in import tariffs can make certain products more expensive and reduce sensitivity to discounts, altering long-standing price–demand relationships. The COVID-19 pandemic is another well known example where mobility restrictions, work-from-home adoption, and shifts in consumer priorities changed the way promotions, holidays, and even weather patterns influenced demand. Fuel price spikes can change commuting behaviour and affect categories like ready-to-eat foods, travel accessories, or home improvement. Unexpected supply shortages can change how customers respond to stockouts or substitute products. Even store refurbishments, new competitor entries, or changes in brand positioning can shift the effectiveness of promotions or alter the response to price changes. In all these scenarios, the external features have not simply drifted in value; the way they influence demand has changed. Models relying on outdated relationships need retraining to correctly learn the new dependency structure. #### 4. Model degradation in Monitoring Metrics A steady rise in forecast error such as RMSSE or MAPE across a meaningful portion of the portfolio is one of the clearest signals that incremental updates are no longer sufficient. In practice, this degradation is detected through rolling or windowed error tracking, residual stability checks, horizon specific error monitoring, and cohort level breakdowns, all of which were discussed in earlier sections. When these monitoring signals begin to trend upward consistently, or when residuals show persistent bias or variance inflation, it indicates that the model is drifting away from the underlying data generating process. At this point, incremental updates can no longer correct the misalignment and a full retraining cycle becomes necessary to restore performance stability. #### 5. Major Business or Market Shifts Major business or market shifts can reshape demand patterns so abruptly that previously learned temporal relationships no longer hold. External shocks such as sudden supply chain disruptions, unexpected policy changes, or rapid demand surges create new regimes that the model has never seen before. Examples include widespread stockouts during global logistics delays, regulatory changes affecting product availability, or short-term spikes in demand triggered by festivals, weather anomalies, or viral social trends. As discussed earlier, these shifts often operate outside the time series itself and override the stability that incremental updates rely on. When such events realign consumer behaviour or operational constraints, a full retraining cycle becomes necessary to ensure the model adapts to the new environment. ## **A Hybrid Cadence for Sustainable Forecasting** Retraining and updating are not competing strategies - they’re complementary. In a well-engineered forecasting pipeline, the two work together to balance **adaptability**, **efficiency**, and **stability**. This balanced cadence ensures your model evolves with data drift while avoiding unnecessary computational overhead. A **hybrid cadence** combines three operational layers: #### 1. Routine Incremental Updates (Short-Term Adaptation) Use MLForecast’s `update()` method every time new data arrives - typically weekly or monthly. This keeps forecasts current by recalculating lag and date features using the latest observations, without retraining the model. * **Objective:** Maintain freshness of forecasts * **Cost:** Minimal (light computation) * **When to use:** After each data ingestion cycle #### 2. Scheduled Retraining (Periodic Refresh) Perform full model retraining at regular intervals - for instance, quarterly or semi-annually. This refreshes feature relationships, captures gradual drift, and resets model parameters to reflect long-term trends. * **Objective:** Recalibrate the model to evolving seasonal or macro patterns * **Cost:** Moderate to high (training time and resource usage) * **When to use:** On a fixed calendar schedule or after major seasonal transitions #### 3. Drift-Triggered Retraining (Event-Based Correction) Deploy monitoring scripts that track forecast accuracy (e.g., RMSSE, MAPE, WAPE) and detect statistical drift using tools such as the Kolmogorov–Smirnov test or rolling error windows. If accuracy degrades beyond a defined threshold, initiate an unscheduled retrain. * **Objective:** Respond to sudden or unanticipated changes * **Cost:** High but justified by regained accuracy * **When to use:** When metrics indicate model degradation or feature distribution shifts #### Quantitative Validation To evaluate the effectiveness of this cadence, compare update-only vs. retrain strategies using MLForecast’s `cross_validation()`: * `refit=False` simulates incremental updates (trained once, updated continuously) * `refit=True` simulates retraining at each historical window If `refit=True` consistently outperforms `refit=False`, it signals that drift is significant and retraining yields real gains. #### Why This Matters This cadence matters because forecasting systems fail silently when they rely on only one mechanism of adaptation. Incremental updates alone cannot correct for long-term drift, and full retraining alone cannot deliver the responsiveness modern pipelines require. By combining both, you create a system that stays fresh in the short term, stable in the long term, and resilient when unexpected changes occur. It ensures that the model not only keeps pace with new data but also remains aligned with the deeper structural patterns that drive forecasting accuracy. In practice this means fewer operational surprises, fewer degradations that go unnoticed, and forecasts that remain dependable even as the business and the environment around it evolve. In essence, **incremental updates keep your model agile; retraining keeps it honest.** ## **Code Implementation Overview** #### **Installing `mlforecast` and Required Libraries** Before we dive into incremental forecasting, let’s set up the environment by installing all necessary dependencies. We’ll use `mlforecast`, along with common Python libraries for data handling and visualization. You can open it in colab in this [link](https://colab.research.google.com/github/Nixtla/mlforecast/blob/main/nbs/docs/tutorials/incremental_forecasting.ipynb). > 💡 *Tip:* If you’re running this in Google Colab or a fresh > environment, it’s a good idea to restart the kernel after installation > to ensure all dependencies are properly loaded. ```python theme={null} !pip install mlforecast datasetsforecast s3fs xgboost -q ``` ```python theme={null} import warnings warnings.filterwarnings("ignore") ``` ```python theme={null} import xgboost as xgb import pandas as pd from mlforecast import MLForecast from datasetsforecast.m3 import M3 import pandas as pd import seaborn as sns import matplotlib.pyplot as plt from mlforecast.lag_transforms import (ExpandingMean, ExpandingStd, ExpandingMin, ExpandingMax, RollingMean, RollingStd, RollingMin, RollingMax,ExponentiallyWeightedMean) from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ## **Loading the M3 Dataset** For this demonstration, we’ll use the **M3 forecasting competition dataset**, a widely used benchmark for evaluating time series models.\ Specifically, we’ll focus on the **`M1` series** from the **Monthly (M) group**. The M3 dataset provides multiple time series across different frequencies -*Yearly, Quarterly, Monthly, and Others*.\ By selecting one unique identifier (`unique_id = 'M1'`), we can visualize and test how incremental forecasting behaves on a single, interpretable time series. > 📘 *Note:* The dataset is loaded using `datasetsforecast` package, > which automatically structures the data in the expected format for > MLForecast - columns `unique_id`, `ds` (date), and `y` (target value). ```python theme={null} # Load M3 data (e.g., monthly frequency) Y_df, X_df, S_df = M3.load(directory='./', group='Monthly') # Filter for the specified unique_id select = ['M1'] df_select = Y_df[Y_df['unique_id'].isin(select)].copy() # Convert 'Date' column to datetime in the original filtered dataframe df_select['ds'] = pd.to_datetime(df_select['ds']) # Sort by unique_id and Date to ensure correct slicing df_select = df_select.sort_values(by=['unique_id', 'ds']).reset_index(drop=True) print("Preview of df_select") print("Top 5 rows:") display(df_select.head()) print("\nShape of df_select") print(f"Rows: {df_select.shape[0]}") print(f"Columns: {df_select.shape[1]}") ``` ```text theme={null} Preview of df_select Top 5 rows: Shape of df_select Rows: 68 Columns: 3 ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ------ | | 0 | M1 | 1990-01-31 | 2640.0 | | 1 | M1 | 1990-02-28 | 2640.0 | | 2 | M1 | 1990-03-31 | 2160.0 | | 3 | M1 | 1990-04-30 | 4200.0 | | 4 | M1 | 1990-05-31 | 3360.0 | #### **Defining the Simulation Scenario** To replicate a **real-world production forecasting setup**, we’ll simulate how forecasts evolve as new data arrives over time, without retraining the model. This helps us understand how `update()` method can efficiently keep forecasts current in a live environment. Here’s how we’ll structure the simulation: * Data **from 1991-06-30 to 1994-05-31** serves as the **baseline training period (Month 0)** - the last available data before the model is deployed. * We then simulate **three consecutive production updates**, where new observations gradually arrive: * **Month 1:** Includes actuals for *1994-06-30* * **Month 2:** Includes actuals for *1994-07-31* * **Month 3:** Includes actuals for *1994-08-31* At each step, the model will be **incrementally updated** using the `update()` method, allowing it to absorb new observations, recompute lags and date features, and produce refreshed forecasts without undergoing a full retraining cycle. ```python theme={null} # Select the last 51 data points from df_select data = df_select.tail(51) # From these 51 points: # - Use the first 36 data points for initial model training (Month 0) # - Reserve the next 3 data points as sequential "new data arrivals" train_month_zero = data.head(36) new_data_all = data.iloc[36:39] # Split the 3 new months into separate DataFrames to simulate data arriving one month at a time new_data_month_one = new_data_all.iloc[0:1] new_data_month_two = new_data_all.iloc[1:2] new_data_month_three = new_data_all.iloc[2:3] # Show the original training and new-data splits print("Preview - Train DataFrame (Month 0 - initial training window):") display(train_month_zero.head(5)) print("="*60) print("\nNew Data Month One (to arrive after Month 0):") display(new_data_month_one) # Construct cumulative training sets by concatenation for showing the full training example # - train_month_one = train_month_zero + new_data_month_one # - train_month_two = train_month_one + new_data_month_two # - train_month_three= train_month_two + new_data_month_three # We concatenate in chronological order so time index remains increasing. train_month_one = pd.concat([train_month_zero, new_data_month_one], ignore_index=True) train_month_two = pd.concat([train_month_one, new_data_month_two], ignore_index=True) train_month_three = pd.concat([train_month_two, new_data_month_three], ignore_index=True) print("="*60) print("\nPreview - train_month_one (last rows):") display(train_month_one.tail(5)) # Create rolling test windows for evaluation test_month_zero = data.iloc[36:48] test_month_one = data.iloc[37:49] test_month_two = data.iloc[38:50] test_month_three = data.iloc[39:51] # Also display the test windows used for evaluation (as before) print("="*60) print("\nPreview - Test Window - Month 0 (after initial training):") display(test_month_zero.head(5)) ``` ```text theme={null} Preview - Train DataFrame (Month 0 - initial training window): ============================================================ New Data Month One (to arrive after Month 0): ============================================================ Preview - train_month_one (last rows): ============================================================ Preview - Test Window - Month 0 (after initial training): ``` | | unique\_id | ds | y | | -- | ---------- | ---------- | ------ | | 17 | M1 | 1991-06-30 | 2520.0 | | 18 | M1 | 1991-07-31 | 9000.0 | | 19 | M1 | 1991-08-31 | 2640.0 | | 20 | M1 | 1991-09-30 | 3120.0 | | 21 | M1 | 1991-10-31 | 2880.0 | | | unique\_id | ds | y | | -- | ---------- | ---------- | ------ | | 53 | M1 | 1994-06-30 | 1920.0 | | | unique\_id | ds | y | | -- | ---------- | ---------- | ------ | | 32 | M1 | 1994-02-28 | 2400.0 | | 33 | M1 | 1994-03-31 | 2280.0 | | 34 | M1 | 1994-04-30 | 480.0 | | 35 | M1 | 1994-05-31 | 5040.0 | | 36 | M1 | 1994-06-30 | 1920.0 | | | unique\_id | ds | y | | -- | ---------- | ---------- | ------ | | 53 | M1 | 1994-06-30 | 1920.0 | | 54 | M1 | 1994-07-31 | 840.0 | | 55 | M1 | 1994-08-31 | 2520.0 | | 56 | M1 | 1994-09-30 | 1560.0 | | 57 | M1 | 1994-10-31 | 1440.0 | #### **Model specification - XGBoost with lags and lag\_transforms** For this demonstration we use XGBoost Regressor together with a small set of lag features and lag\_transforms. The goal is to keep the model simple and interpretable while preserving enough temporal information to produce reliable incremental forecasts. In the next block we define the complete forecasting setup for Month Zero. We create an MLForecast object that includes the XGBoost Regressor, the time frequency of the series and a structured set of lag features and lag based statistical transforms. The same block also defines a compact evaluation function that uses `utilsforecast` to merge predictions with the test set and compute **MAE** and **RMSE**. This keeps the evaluation process consistent for all forecasting stages that follow. ```python theme={null} # Load your data (must have columns: unique_id, ds, y) df = train_month_zero # Initialize the MLForecast object with model, frequency and feature settings fcst = MLForecast( models=xgb.XGBRegressor(verbosity=0, random_state=5), freq='ME', lags=[1, 2, 3, 4, 6, 12], # added more lags lag_transforms={ 1: [ExpandingMean(), ExpandingStd(), ExpandingMin(), ExpandingMax(), ExponentiallyWeightedMean(alpha=0.3)], 3: [RollingMean(window_size=3),RollingMean(window_size=3),RollingStd(window_size=3)], 6: [RollingMean(window_size=3),RollingMin(window_size=3),RollingMax(window_size=3)] }, date_features=['month', 'quarter', 'year', 'week']) # Fit the model using the Month Zero training data fcst.fit(df) # At this stage, forecasts are produced using data available up to Month 0. predictions_month_zero = fcst.predict(h=12) # Function for Evaluation using MAE and RMSE def evaluate_forecast(test_month, predictions, train_data): """ Wrapper function to evaluate forecasts using MAE and RMSE """ result = evaluate(test_month.merge(predictions), metrics=[ufl.mae, ufl.rmse], train_df=train_data) return result evaluation_month_zero = evaluate_forecast(test_month_zero, predictions_month_zero, train_month_zero) ``` ### **Incremental Learning for Month 1, 2 and 3** #### Updating the Model for Month One Now that we have the actuals for **Month One (June 1994)**, we’ll simulate how a forecasting system incorporates this new data without retraining. Instead of rebuilding the model from scratch, we’ll use the `update()` method to **incrementally refresh** the forecasting object.\ This operation allows the model to: * Absorb the latest observation into its internal history * Recompute lag and date-based features * Generate forecasts that reflect the most recent trend Importantly, the underlying model parameters remain fixed, we are simply extending the historical window that the model bases its predictions on. This is the essence of *incremental forecasting*: fast, efficient, and adaptive to streaming data. ```python theme={null} # Update the trained model with the latest observation fcst.update(new_data_month_one) # Produce a new 12-month-ahead forecast using the extended data window. predictions_month_one = fcst.predict(h=12) # Evaluation at Incremental Month One evaluation_incremental_month_one = evaluate_forecast(test_month_one, predictions_month_one, train_month_one) ``` #### Updating the Model for Month Two Next, we simulate the arrival of **Month Two (July 1994)** data. By this point, our model has already been incrementally updated with the June actuals. Now, we extend the historical window once more by incorporating the July observation. ```python theme={null} # Update the forecasting object with the July 1995 data fcst.update(new_data_month_two) # Generate new forecasts (12 months ahead) after Month Two update predictions_month_two = fcst.predict(h=12) # Evaluation at Incremental Month Two evaluation_incremental_month_two = evaluate_forecast(test_month_two, predictions_month_two, train_month_two) ``` #### Updating the Model for Month Three Finally, we simulate the arrival of **Month Three (August 1994)** actuals. By this stage, the model has already incorporated data from June and July through successive incremental updates. Now, we’ll perform one more `update()` to include the August observation. ```python theme={null} # Update the model with Month Three data fcst.update(new_data_month_three) # Generate forecasts after Month Three update predictions_month_three = fcst.predict(h=12) # Evaluation at Incremental Month Three evaluation_incremental_month_three = evaluate_forecast(test_month_three, predictions_month_three, train_month_three) ``` ### **Full Retrain for Month 1, 2 and 3** The following function reconstructs the entire MLForecast pipeline and trains a fresh model on the supplied dataset. It defines a new XGBoost regressor, rebuilds all lag features, regenerates lag based statistical transforms and recreates the date features for every retraining cycle. This function is used when we want to evaluate how the model performs with full retraining after Month 1, Month 2 and Month 3. ```python theme={null} def run_mlforecast_model(input_df, forecast_name, horizon=12): """ Trains a fresh MLForecast model on the given dataset and generates a new forecast for the specified horizon. Used during full retraining steps to compare model performance across expanded training windows. """ fcst = MLForecast( models=xgb.XGBRegressor(verbosity=0, random_state=5), freq='ME', lags=[1, 2, 3, 4, 6, 12], # added more lags lag_transforms={ 1: [ExpandingMean(), ExpandingStd(), ExpandingMin(), ExpandingMax(), ExponentiallyWeightedMean(alpha=0.3)], 3: [RollingMean(window_size=3),RollingMean(window_size=3),RollingStd(window_size=3)], 6: [RollingMean(window_size=3),RollingMin(window_size=3),RollingMax(window_size=3)] }, date_features=['month', 'quarter', 'year', 'week'], ) # Fit the newly constructed model on the provided training window fcst.fit(input_df) # Produce the forecast for the specified horizon predictions = fcst.predict(h=horizon) return predictions ``` #### Full retrain for Month One ```python theme={null} full_retrain_one = run_mlforecast_model(train_month_one, forecast_name = "full_retrain_one", horizon=12) evaluation_fr_month_one = evaluate_forecast(test_month_one, full_retrain_one, train_month_one) ``` #### Full retrain for Month Two ```python theme={null} full_retrain_two = run_mlforecast_model(train_month_two, forecast_name = "full_retrain_two", horizon=12) evaluation_fr_month_two = evaluate_forecast(test_month_two, full_retrain_two, train_month_two) ``` #### Full retrain for Month Three ```python theme={null} full_retrain_three = run_mlforecast_model(train_month_three, forecast_name = "full_retrain_three", horizon=12) evaluation_fr_month_three = evaluate_forecast(test_month_three, full_retrain_three, train_month_three) ``` ## **Displaying the Forecast for incremental updates and full retraining** ```python theme={null} # Prepare Actuals Data actuals = data.rename(columns={'y': 'Actuals'}) # Prepare Prediction Dataframes predictions_M0 = predictions_month_zero.rename(columns={'XGBRegressor': 'M0_fcst'}) predictions_M1 = predictions_month_one.rename(columns={'XGBRegressor': 'update_M1_fcst'}) predictions_M2 = predictions_month_two.rename(columns={'XGBRegressor': 'update_M2_fcst'}) predictions_M3 = predictions_month_three.rename(columns={'XGBRegressor': 'update_M3_fcst'}) full_retrain_M1 = full_retrain_one.rename(columns={'XGBRegressor': 'fr_M1_fcst'}) full_retrain_M2 = full_retrain_two.rename(columns={'XGBRegressor': 'fr_M2_fcst'}) full_retrain_M3 = full_retrain_three.rename(columns={'XGBRegressor': 'fr_M3_fcst'}) # Merge Dataframes horizontally merged_df = actuals prediction_dfs = [predictions_M0,predictions_M1, predictions_M2, predictions_M3, full_retrain_M1, full_retrain_M2, full_retrain_M3] for pred_df in prediction_dfs: merged_df = pd.merge(merged_df,pred_df, on=['unique_id', 'ds'], how='outer') print("Forecasts for the data:") merged_df.tail(15) ``` ```text theme={null} Forecasts for the data: ``` | | unique\_id | ds | Actuals | M0\_fcst | update\_M1\_fcst | update\_M2\_fcst | update\_M3\_fcst | fr\_M1\_fcst | fr\_M2\_fcst | fr\_M3\_fcst | | -- | ---------- | ---------- | ------- | ----------- | ---------------- | ---------------- | ---------------- | ------------ | ------------ | ------------ | | 36 | M1 | 1994-06-30 | 1920.0 | 3864.938232 | NaN | NaN | NaN | NaN | NaN | NaN | | 37 | M1 | 1994-07-31 | 840.0 | 2594.932129 | 2935.675293 | NaN | NaN | 2326.694824 | NaN | NaN | | 38 | M1 | 1994-08-31 | 2520.0 | 4164.596680 | 4542.910156 | 4541.260742 | NaN | 3381.296875 | 4850.554199 | NaN | | 39 | M1 | 1994-09-30 | 1560.0 | 1275.051147 | 1397.846802 | 1955.686035 | 1878.714355 | 1786.434082 | 2493.714600 | 2078.830322 | | 40 | M1 | 1994-10-31 | 1440.0 | 1587.772827 | 2979.571533 | 3855.898438 | 3675.791504 | 2989.618164 | 3918.535889 | 3702.415039 | | 41 | M1 | 1994-11-30 | 240.0 | 3405.029053 | 1300.912109 | 4274.687988 | 3670.577393 | 2768.753418 | 4103.104492 | 2165.639160 | | 42 | M1 | 1994-12-31 | 1800.0 | 1239.843140 | 1272.664429 | 2168.222412 | 2514.473389 | 923.883606 | 2992.595947 | 1841.551758 | | 43 | M1 | 1995-01-31 | 4680.0 | 2959.135498 | 3896.721191 | 2978.796875 | 3009.882568 | 2853.434082 | 1643.795288 | 2048.090332 | | 44 | M1 | 1995-02-28 | 1800.0 | 3824.433594 | 2101.060303 | 1859.390747 | 1828.250122 | 1785.165283 | 1919.282837 | 2137.860596 | | 45 | M1 | 1995-03-31 | 1680.0 | 2100.476807 | 2890.057617 | 2211.707520 | 2211.707520 | 2805.169922 | 2153.157715 | 3000.055664 | | 46 | M1 | 1995-04-30 | 3720.0 | 3765.023926 | 4625.206543 | 4431.138184 | 4431.138184 | 4142.056152 | 550.080627 | 2833.239746 | | 47 | M1 | 1995-05-31 | 2160.0 | 994.927246 | 1047.208374 | 1171.909912 | 1204.782959 | 1166.692383 | 4042.854492 | 2641.430664 | | 48 | M1 | 1995-06-30 | 480.0 | NaN | 3646.509521 | 3382.160645 | 3382.160645 | 3694.346191 | 2470.466553 | 2698.386230 | | 49 | M1 | 1995-07-31 | 2040.0 | NaN | NaN | 4582.800781 | 4582.800781 | NaN | 2140.027588 | 1828.845093 | | 50 | M1 | 1995-08-31 | 1440.0 | NaN | NaN | NaN | 1782.028687 | NaN | NaN | 1667.283569 | ## **MAE and RMSE comparison across Incremental Updates and Full Retraining** The table below compares the MAE and RMSE produced by the incremental forecasting approach and the full retraining approach for Month One, Month Two and Month Three. This comparison helps illustrate how forecast accuracy changes when the model is updated versus fully retrained on expanded training windows. ```python theme={null} # Map each DF pair to the month name mapping = { "Month One": (evaluation_incremental_month_one, evaluation_fr_month_one), "Month Two": (evaluation_incremental_month_two, evaluation_fr_month_two), "Month Three": (evaluation_incremental_month_three, evaluation_fr_month_three), } #Generate final dataframe final_df = pd.DataFrame([{ "unique_id": inc.loc[0, "unique_id"], "Month": m, "Incremental Model_MAE": inc.loc[inc.metric == "mae", inc.columns[-1]].item(), "Full Retrain Model_MAE": fr.loc[fr.metric == "mae", fr.columns[-1]].item(), "Incremental Model_RMSE": inc.loc[inc.metric == "rmse", inc.columns[-1]].item(), "Full Retrain Model_RMSE": fr.loc[fr.metric == "rmse", fr.columns[-1]].item(), } for m, (inc, fr) in mapping.items()]).round(4) print("Evaluation Metrics for the Data:") display(final_df) ``` ```text theme={null} Evaluation Metrics for the Data: ``` | | unique\_id | Month | Incremental Model\_MAE | Full Retrain Model\_MAE | Incremental Model\_RMSE | Full Retrain Model\_RMSE | | - | ---------- | ----------- | ---------------------- | ----------------------- | ----------------------- | ------------------------ | | 0 | M1 | Month One | 1240.6219 | 1260.4329 | 1487.0861 | 1545.5916 | | 1 | M1 | Month Two | 1556.0206 | 1797.5349 | 1958.8191 | 2156.6650 | | 2 | M1 | Month Three | 1365.2481 | 1088.6065 | 1752.9972 | 1411.7591 | ## **Visualizing Actuals and Forecasts for Incremental Updates and Full Retraining** To understand how the forecasting behavior evolves over time, we now visualize the actual series together with the forecasted values generated at each stage for both incremental updates and full retraining. This includes predictions from Month 0, Month 1, Month 2 and Month 3 for both approaches. ```python theme={null} # COLORS & MARKERS COLOR_ACTUAL, COLOR_UPDATE, COLOR_RETRAIN = "#1f77b4", "#FF9933", "#40A240" MARK_UPDATE, MARK_RETRAIN = "D", "^" # HELPERS def norm(df, name): """Rename the last prediction column to name and keep (unique_id, ds).""" col = [c for c in df.columns if c not in ["unique_id","ds","y"]][-1] return df.rename(columns={col:name})[["unique_id","ds",name]] def _norm_out(df, out_col): """Normalize forecast df by renaming its last prediction column to out_col.""" if df is None or df.empty: return None nd = norm(df, out_col); nd["ds"] = pd.to_datetime(nd["ds"]); return nd def build_series(srcs, out_col): """Stitch multiple forecast runs, keeping the latest prediction for each ds.""" parts = [ _norm_out(df, out_col) for df in srcs if _norm_out(df, out_col) is not None ] if not parts: return None df = pd.concat(parts, ignore_index=True) return (df.query("unique_id == @UNIQUE_ID")[["ds", out_col]] .sort_values("ds").drop_duplicates("ds", keep="last").reset_index(drop=True)) def extract_step(df, label, typ): """Extract the first available forecast point for marker plotting.""" if df is None or df.empty: return None col = [c for c in df.columns if c not in ["unique_id","ds","y"]][-1] d = df[["ds", col]].rename(columns={col:label}); d["ds"] = pd.to_datetime(d["ds"]); d["type"] = typ; return d # BUILD FORECASTS UNIQUE_ID = "M1" inc_df = build_series([predictions_month_one.head(1), predictions_month_two.head(1), predictions_month_three], "Incremental") full_df = build_series([full_retrain_one.head(1), full_retrain_two.head(1), full_retrain_three], "FullRetrain") # Step markers update_steps = [extract_step(predictions_month_one.head(1),"Incremental","update"), extract_step(predictions_month_two.head(1),"Incremental","update"), extract_step(predictions_month_three,"Incremental","update")] retrain_steps = [extract_step(full_retrain_one.head(1),"FullRetrain","retrain"), extract_step(full_retrain_two.head(1),"FullRetrain","retrain"), extract_step(full_retrain_three,"FullRetrain","retrain")] update_steps, retrain_steps = [x for x in update_steps if x is not None], [x for x in retrain_steps if x is not None] # ACTUALS actuals = (data.query("unique_id == @UNIQUE_ID")[["ds","y"]] .rename(columns={"y":"Actual"}) .assign(ds=lambda x: pd.to_datetime(x["ds"])) .sort_values("ds").reset_index(drop=True)) # MERGE plot_df = actuals.copy() if inc_df is not None: plot_df = plot_df.merge(inc_df, on="ds", how="outer") if full_df is not None: plot_df = plot_df.merge(full_df, on="ds", how="outer") plot_df = plot_df.sort_values("ds").reset_index(drop=True) # PLOT plt.figure(figsize=(14,6)) plt.plot(plot_df["ds"], plot_df["Actual"], color=COLOR_ACTUAL, lw=2.2, marker='o', markersize=6, markerfacecolor=COLOR_ACTUAL, markeredgecolor=COLOR_ACTUAL, label="Actuals") if "Incremental" in plot_df: plt.plot(plot_df["ds"], plot_df["Incremental"], color=COLOR_UPDATE, lw=2.0, label="Incremental (Update)") if "FullRetrain" in plot_df: plt.plot(plot_df["ds"], plot_df["FullRetrain"], color=COLOR_RETRAIN, lw=2.0, ls="--", label="Full Retrain") for s in update_steps: plt.scatter(s["ds"], s["Incremental"], color=COLOR_UPDATE, marker=MARK_UPDATE, s=90, edgecolor="black", label=None) for s in retrain_steps: plt.scatter(s["ds"], s["FullRetrain"], color=COLOR_RETRAIN, marker=MARK_RETRAIN, s=100, edgecolor="black", label=None) plt.title("Actuals vs Updates vs Full Retrains", fontsize=20) plt.grid(alpha=0.25) plt.xlabel("Date", fontsize=14); plt.ylabel("Value", fontsize=14) plt.legend(loc="center left", bbox_to_anchor=(1.02,0.5)) plt.tight_layout(); plt.show() ``` ## **Comparative analysis of Incremental Updates and Full Retraining** The plots and RMSE comparison table together provide a complete picture of how the two forecasting strategies behave as new data becomes available. Both methods process the same series, but their update mechanisms differ. Incremental forecasting updates only the data window while preserving the learned model parameters. Full retraining rebuilds the entire model on each expanded dataset. The following points summarise the observed behaviour. #### 1. **Responsiveness to new observations** * Incremental updates shift the forecasts quickly toward the newest actual points. This is visible in the plot where each successive update pulls the forecast curves closer to the recent downward and upward movements in Month 1, 2 and 3. * Full retraining is also responsive, but sometimes overshoots or undershoots depending on how the newly retrained model interprets the expanded historical window. The variance introduced in the retrained curves is visible in the colour coded lines for FullRetrain M1, M2 and M3. #### 2. **Stability and continuity of the forecast path** * The incremental method produces smoother transitions, since the model parameters do not change. Only the lagged features derived from the newly appended data shift the forecasts. This preserves the character of the initial model and avoids abrupt structural changes. * Full retraining reoptimises the model with every cycle. This can cause sudden changes in the shape of the predictions. #### 3. **Accuracy across the three cycles** * For Month One and Month Two, the incremental approach produces lower MAE and RMSE than the retrained model. This suggests that the original model was already well tuned and preserving its parameters provided better generalisation. * For Month Three, the trend reverses and the fully retrained model performs better. This indicates that by the third update the model benefits from incorporating the longer training history and reoptimising its parameters. #### 4. **Practical interpretation** * Incremental forecasting is more stable, less computationally heavy and adapts quickly to new information. It is suitable for real time or high frequency update environments. * Full retraining can provide benefits once enough new data accumulates, especially if the underlying pattern has shifted. However, it is more sensitive to small data variations and may introduce unnecessary volatility when used too frequently. ## **Conclusion** This tutorial shows that incremental forecasting with MLForecast provides a practical and efficient way to manage evolving time series. By combining lag based feature engineering with `update()` mechanisms, the approach keeps forecasts aligned with incoming data while avoiding the cost of frequent full model rebuilds. The results highlight how incremental updates maintain stability and continuity, making them suitable for production settings where new observations arrive regularly. Full retraining still has value when enough new information accumulates, but incremental updates offer a reliable and scalable foundation for ongoing forecasting operations. ## **References** 1. Nixtla Team. (2024). *[MLForecast](https://nixtlaverse.nixtla.io/mlforecast): Scalable Machine Learning for Time Series Forecasting.* 2. Makridakis, S., & Hibon, M. (2000). *The M3-Competition: Results, Conclusions and Implications.*\ *International Journal of Forecasting*, 16(4), 451–476.\ DOI: [10.1016/S0169-2070(00)00057-1](https://doi.org/10.1016/S0169-2070\(00\)00057-1) 3. Chaudhuri, S., (2025). *A Practical Guide to Incremental Updates and Transfer Learning for Scalable New-Product Forecasting using MLForecast*. Available at: [Article by Satyajit Chaudhuri on Medium](https://medium.com/gitconnected/a-practical-guide-to-incremental-updates-and-transfer-learning-for-scalable-new-product-forecasting-b0c3916ebf78) # M4 Competition Source: https://nixtlaverse.nixtla.io/mlforecast/docs/tutorials/m4.html This shows an example with just 4 series of the M4 dataset. If you want to run it yourself on all of them, you can refer to [this notebook](https://www.kaggle.com/code/lemuz90/m4-competition). ```python theme={null} import random import lightgbm as lgb import numpy as np import pandas as pd from sklearn.linear_model import LinearRegression from utilsforecast.plotting import plot_series from mlforecast import MLForecast from mlforecast.lag_transforms import ( ExponentiallyWeightedMean, RollingMean, ) from mlforecast.lgb_cv import LightGBMCV from mlforecast.target_transforms import Differences from mlforecast.utils import PredictionIntervals ``` ```python theme={null} df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') ids = df['unique_id'].unique() random.seed(0) sample_ids = random.choices(ids, k=4) sample_df = df[df['unique_id'].isin(sample_ids)] sample_df ``` | | unique\_id | ds | y | | ------ | ---------- | ---- | ---- | | 86796 | H196 | 1 | 11.8 | | 86797 | H196 | 2 | 11.4 | | 86798 | H196 | 3 | 11.1 | | 86799 | H196 | 4 | 10.8 | | 86800 | H196 | 5 | 10.6 | | ... | ... | ... | ... | | 325235 | H413 | 1004 | 99.0 | | 325236 | H413 | 1005 | 88.0 | | 325237 | H413 | 1006 | 47.0 | | 325238 | H413 | 1007 | 41.0 | | 325239 | H413 | 1008 | 34.0 | We now split this data into train and validation. ```python theme={null} horizon = 48 valid = sample_df.groupby('unique_id').tail(horizon) train = sample_df.drop(valid.index) train.shape, valid.shape ``` ```text theme={null} ((3840, 3), (192, 3)) ``` ## Creating the forecaster ```python theme={null} fcst = MLForecast( models=lgb.LGBMRegressor(random_state=0, verbosity=-1), freq=1, lags=[24 * (i+1) for i in range(7)], lag_transforms={ 48: [ExponentiallyWeightedMean(alpha=0.3)], }, num_threads=1, target_transforms=[Differences([24])], ) ``` ```python theme={null} fcst ``` ```text theme={null} MLForecast(models=[LGBMRegressor], freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168', 'exponentially_weighted_mean_lag48_alpha0.3'], date_features=[], num_threads=1) ``` Once we have this setup we can compute the features and fit the model. ## Fitting and predicting ```python theme={null} fcst = MLForecast( models=lgb.LGBMRegressor(random_state=0, verbosity=-1), freq=1, lags=[24 * (i+1) for i in range(7)], lag_transforms={ 48: [ExponentiallyWeightedMean(alpha=0.3)], }, num_threads=1, target_transforms=[Differences([24])], ) ``` ```python theme={null} train2 = train.copy() train2['weight'] = np.random.default_rng(seed=0).random(train2.shape[0]) fcst.fit(train2, weight_col='weight', as_numpy=True).predict(5) ``` | | unique\_id | ds | LGBMRegressor | | -- | ---------- | --- | ------------- | | 0 | H196 | 961 | 16.079737 | | 1 | H196 | 962 | 15.679737 | | 2 | H196 | 963 | 15.279737 | | 3 | H196 | 964 | 14.979737 | | 4 | H196 | 965 | 14.679737 | | 5 | H256 | 961 | 13.279737 | | 6 | H256 | 962 | 12.679737 | | 7 | H256 | 963 | 12.379737 | | 8 | H256 | 964 | 12.079737 | | 9 | H256 | 965 | 11.879737 | | 10 | H381 | 961 | 56.939977 | | 11 | H381 | 962 | 40.314608 | | 12 | H381 | 963 | 33.859013 | | 13 | H381 | 964 | 15.498139 | | 14 | H381 | 965 | 25.722674 | | 15 | H413 | 961 | 25.131194 | | 16 | H413 | 962 | 19.177421 | | 17 | H413 | 963 | 21.250829 | | 18 | H413 | 964 | 18.743132 | | 19 | H413 | 965 | 16.027263 | ```python theme={null} fcst.cross_validation(train2, n_windows=2, h=5, weight_col='weight', as_numpy=True) ``` | | unique\_id | ds | cutoff | y | LGBMRegressor | | -- | ---------- | --- | ------ | ----- | ------------- | | 0 | H196 | 951 | 950 | 24.4 | 24.288850 | | 1 | H196 | 952 | 950 | 24.3 | 24.188850 | | 2 | H196 | 953 | 950 | 23.8 | 23.688850 | | 3 | H196 | 954 | 950 | 22.8 | 22.688850 | | 4 | H196 | 955 | 950 | 21.2 | 21.088850 | | 5 | H256 | 951 | 950 | 19.5 | 19.688850 | | 6 | H256 | 952 | 950 | 19.4 | 19.488850 | | 7 | H256 | 953 | 950 | 18.9 | 19.088850 | | 8 | H256 | 954 | 950 | 18.3 | 18.388850 | | 9 | H256 | 955 | 950 | 17.0 | 17.088850 | | 10 | H381 | 951 | 950 | 182.0 | 208.327270 | | 11 | H381 | 952 | 950 | 222.0 | 247.768326 | | 12 | H381 | 953 | 950 | 288.0 | 277.965997 | | 13 | H381 | 954 | 950 | 264.0 | 321.532857 | | 14 | H381 | 955 | 950 | 191.0 | 206.316903 | | 15 | H413 | 951 | 950 | 77.0 | 60.972692 | | 16 | H413 | 952 | 950 | 91.0 | 54.936494 | | 17 | H413 | 953 | 950 | 76.0 | 73.949203 | | 18 | H413 | 954 | 950 | 68.0 | 67.087417 | | 19 | H413 | 955 | 950 | 68.0 | 75.896022 | | 20 | H196 | 956 | 955 | 19.3 | 19.287891 | | 21 | H196 | 957 | 955 | 18.2 | 18.187891 | | 22 | H196 | 958 | 955 | 17.5 | 17.487891 | | 23 | H196 | 959 | 955 | 16.9 | 16.887891 | | 24 | H196 | 960 | 955 | 16.5 | 16.487891 | | 25 | H256 | 956 | 955 | 15.5 | 15.687891 | | 26 | H256 | 957 | 955 | 14.7 | 14.787891 | | 27 | H256 | 958 | 955 | 14.1 | 14.287891 | | 28 | H256 | 959 | 955 | 13.6 | 13.787891 | | 29 | H256 | 960 | 955 | 13.2 | 13.387891 | | 30 | H381 | 956 | 955 | 130.0 | 124.117828 | | 31 | H381 | 957 | 955 | 113.0 | 119.180350 | | 32 | H381 | 958 | 955 | 94.0 | 105.356552 | | 33 | H381 | 959 | 955 | 192.0 | 127.095338 | | 34 | H381 | 960 | 955 | 87.0 | 119.875754 | | 35 | H413 | 956 | 955 | 59.0 | 67.993133 | | 36 | H413 | 957 | 955 | 58.0 | 69.869815 | | 37 | H413 | 958 | 955 | 53.0 | 34.717960 | | 38 | H413 | 959 | 955 | 38.0 | 47.665581 | | 39 | H413 | 960 | 955 | 46.0 | 45.940137 | ```python theme={null} fcst.fit(train, fitted=True); ``` ```python theme={null} expected_future = fcst.make_future_dataframe(h=1) expected_future ``` | | unique\_id | ds | | - | ---------- | --- | | 0 | H196 | 961 | | 1 | H256 | 961 | | 2 | H381 | 961 | | 3 | H413 | 961 | ```python theme={null} missing_future = fcst.get_missing_future(h=1, X_df=expected_future.head(2)) pd.testing.assert_frame_equal( missing_future, expected_future.tail(2).reset_index(drop=True) ) ``` ```python theme={null} fcst.forecast_fitted_values() ``` | | unique\_id | ds | y | LGBMRegressor | | ---- | ---------- | --- | ---- | ------------- | | 0 | H196 | 193 | 12.7 | 12.671271 | | 1 | H196 | 194 | 12.3 | 12.271271 | | 2 | H196 | 195 | 11.9 | 11.871271 | | 3 | H196 | 196 | 11.7 | 11.671271 | | 4 | H196 | 197 | 11.4 | 11.471271 | | ... | ... | ... | ... | ... | | 3067 | H413 | 956 | 59.0 | 68.280574 | | 3068 | H413 | 957 | 58.0 | 70.427570 | | 3069 | H413 | 958 | 53.0 | 44.767965 | | 3070 | H413 | 959 | 38.0 | 48.691257 | | 3071 | H413 | 960 | 46.0 | 46.652238 | ```python theme={null} fcst.forecast_fitted_values(level=[90]) ``` | | unique\_id | ds | y | LGBMRegressor | LGBMRegressor-lo-90 | LGBMRegressor-hi-90 | | ---- | ---------- | --- | ---- | ------------- | ------------------- | ------------------- | | 0 | H196 | 193 | 12.7 | 12.671271 | 12.540634 | 12.801909 | | 1 | H196 | 194 | 12.3 | 12.271271 | 12.140634 | 12.401909 | | 2 | H196 | 195 | 11.9 | 11.871271 | 11.740634 | 12.001909 | | 3 | H196 | 196 | 11.7 | 11.671271 | 11.540634 | 11.801909 | | 4 | H196 | 197 | 11.4 | 11.471271 | 11.340634 | 11.601909 | | ... | ... | ... | ... | ... | ... | ... | | 3067 | H413 | 956 | 59.0 | 68.280574 | 58.846640 | 77.714509 | | 3068 | H413 | 957 | 58.0 | 70.427570 | 60.993636 | 79.861504 | | 3069 | H413 | 958 | 53.0 | 44.767965 | 35.334031 | 54.201899 | | 3070 | H413 | 959 | 38.0 | 48.691257 | 39.257323 | 58.125191 | | 3071 | H413 | 960 | 46.0 | 46.652238 | 37.218304 | 56.086172 | Once we’ve run this we’re ready to compute our predictions. ```python theme={null} predictions = fcst.predict(horizon) ``` We can see at a couple of results. ```python theme={null} results = valid.merge(predictions, on=['unique_id', 'ds']) fig = plot_series(forecasts_df=results) ``` ```python theme={null} fig ``` ### Prediction intervals With [`MLForecast`](https://Nixtla.github.io/mlforecast/forecast.html#mlforecast), you can generate prediction intervals using Conformal Prediction. To configure Conformal Prediction, you need to pass an instance of the [`PredictionIntervals`](https://Nixtla.github.io/mlforecast/utils.html#predictionintervals) class to the `prediction_intervals` argument of the `fit` method. The class takes three parameters: `n_windows`, `h` and `method`. * `n_windows` represents the number of cross-validation windows used to calibrate the intervals * `h` is the forecast horizon * `method` can be `conformal_distribution` or `conformal_error`; `conformal_distribution` (default) creates forecasts paths based on the cross-validation errors and calculate quantiles using those paths, on the other hand `conformal_error` calculates the error quantiles to produce prediction intervals. The strategy will adjust the intervals for each horizon step, resulting in different widths for each step. Please note that a minimum of 2 cross-validation windows must be used. ```python theme={null} fcst.fit( train, prediction_intervals=PredictionIntervals(n_windows=3, h=48) ) ``` ```text theme={null} MLForecast(models=[LGBMRegressor], freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168', 'exponentially_weighted_mean_lag48_alpha0.3'], date_features=[], num_threads=1) ``` After that, you just have to include your desired confidence levels to the `predict` method using the `level` argument. Levels must lie between 0 and 100. ```python theme={null} predictions_w_intervals = fcst.predict(48, level=[50, 80, 95]) predictions_w_intervals.head() ``` | | unique\_id | ds | LGBMRegressor | LGBMRegressor-lo-95 | LGBMRegressor-lo-80 | LGBMRegressor-lo-50 | LGBMRegressor-hi-50 | LGBMRegressor-hi-80 | LGBMRegressor-hi-95 | | - | ---------- | --- | ------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | 0 | H196 | 961 | 16.071271 | 15.958042 | 15.971271 | 16.005091 | 16.137452 | 16.171271 | 16.184501 | | 1 | H196 | 962 | 15.671271 | 15.553632 | 15.553632 | 15.578632 | 15.763911 | 15.788911 | 15.788911 | | 2 | H196 | 963 | 15.271271 | 15.153632 | 15.153632 | 15.162452 | 15.380091 | 15.388911 | 15.388911 | | 3 | H196 | 964 | 14.971271 | 14.858042 | 14.871271 | 14.905091 | 15.037452 | 15.071271 | 15.084501 | | 4 | H196 | 965 | 14.671271 | 14.553632 | 14.553632 | 14.562452 | 14.780091 | 14.788911 | 14.788911 | Let’s explore the generated intervals. ```python theme={null} results = valid.merge(predictions_w_intervals, on=['unique_id', 'ds']) fig = plot_series(forecasts_df=results, level=[50, 80, 95]) fig ``` If you want to reduce the computational time and produce intervals with the same width for the whole forecast horizon, simple pass `h=1` to the [`PredictionIntervals`](https://Nixtla.github.io/mlforecast/utils.html#predictionintervals) class. The caveat of this strategy is that in some cases, variance of the absolute residuals maybe be small (even zero), so the intervals may be too narrow. ```python theme={null} fcst.fit( train, prediction_intervals=PredictionIntervals(n_windows=3, h=1) ); ``` ```python theme={null} predictions_w_intervals_ws_1 = fcst.predict(48, level=[80, 90, 95]) ``` Let’s explore the generated intervals. ```python theme={null} results = valid.merge(predictions_w_intervals_ws_1, on=['unique_id', 'ds']) fig = plot_series(forecasts_df=results, level=[90]) fig ``` ### Forecast using a pretrained model MLForecast allows you to use a pretrained model to generate forecasts for a new dataset. Simply provide a pandas dataframe containing the new observations as the value for the `new_df` argument when calling the `predict` method. The dataframe should have the same structure as the one used to fit the model, including any features and time series data. The function will then use the pretrained model to generate forecasts for the new observations. This allows you to easily apply a pretrained model to a new dataset and generate forecasts without the need to retrain the model. ```python theme={null} ercot_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/ERCOT-clean.csv') # we have to convert the ds column to integers # since MLForecast was trained with that structure ercot_df['ds'] = np.arange(1, len(ercot_df) + 1) # use the `new_df` argument to pass the ercot dataset ercot_fcsts = fcst.predict(horizon, new_df=ercot_df) fig = plot_series(ercot_df, ercot_fcsts, max_insample_length=48 * 2) fig ``` ### Preprocess If you want to take a look at the data that will be used to train the models you can call `Forecast.preprocess`. ```python theme={null} prep_df = fcst.preprocess(train) prep_df ``` | | unique\_id | ds | y | lag24 | lag48 | lag72 | lag96 | lag120 | lag144 | lag168 | exponentially\_weighted\_mean\_lag48\_alpha0.3 | | ------ | ---------- | --- | ---- | ----- | ----- | ----- | ----- | ------ | ------ | ------ | ---------------------------------------------- | | 86988 | H196 | 193 | 0.1 | 0.0 | 0.0 | 0.0 | 0.3 | 0.1 | 0.1 | 0.3 | 0.002810 | | 86989 | H196 | 194 | 0.1 | -0.1 | 0.1 | 0.0 | 0.3 | 0.1 | 0.1 | 0.3 | 0.031967 | | 86990 | H196 | 195 | 0.1 | -0.1 | 0.1 | 0.0 | 0.3 | 0.1 | 0.2 | 0.1 | 0.052377 | | 86991 | H196 | 196 | 0.1 | 0.0 | 0.0 | 0.0 | 0.3 | 0.2 | 0.1 | 0.2 | 0.036664 | | 86992 | H196 | 197 | 0.0 | 0.0 | 0.0 | 0.1 | 0.2 | 0.2 | 0.1 | 0.2 | 0.025665 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 325187 | H413 | 956 | 0.0 | 10.0 | 1.0 | 6.0 | -53.0 | 44.0 | -21.0 | 21.0 | 7.963225 | | 325188 | H413 | 957 | 9.0 | 10.0 | 10.0 | -7.0 | -46.0 | 27.0 | -19.0 | 24.0 | 8.574257 | | 325189 | H413 | 958 | 16.0 | 8.0 | 5.0 | -9.0 | -36.0 | 32.0 | -13.0 | 8.0 | 7.501980 | | 325190 | H413 | 959 | -3.0 | 17.0 | -7.0 | 2.0 | -31.0 | 22.0 | 5.0 | -2.0 | 3.151386 | | 325191 | H413 | 960 | 15.0 | 11.0 | -6.0 | -5.0 | -17.0 | 22.0 | -18.0 | 10.0 | 0.405970 | If we do this we then have to call `Forecast.fit_models`, since this only stores the series information. ```python theme={null} X, y = prep_df.drop(columns=['unique_id', 'ds', 'y']), prep_df['y'] fcst.fit_models(X, y) ``` ```text theme={null} MLForecast(models=[LGBMRegressor], freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168', 'exponentially_weighted_mean_lag48_alpha0.3'], date_features=[], num_threads=1) ``` ```python theme={null} fcst ``` ```text theme={null} MLForecast(models=[LGBMRegressor], freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168', 'exponentially_weighted_mean_lag48_alpha0.3'], date_features=[], num_threads=1) ``` ```python theme={null} predictions2 = fcst.predict(horizon) pd.testing.assert_frame_equal(predictions, predictions2) ``` If we would like to know how good our forecast will be for a specific model and set of features then we can perform cross validation. What cross validation does is take our data and split it in two parts, where the first part is used for training and the second one for validation. Since the data is time dependant we usually take the last *x* observations from our data as the validation set. This process is implemented in [`MLForecast.cross_validation`](https://Nixtla.github.io/mlforecast/forecast.html#mlforecast.cross_validation), which takes our data and performs the process described above for `n_windows` times where each window has `h` validation samples in it. For example, if we have 100 samples and we want to perform 2 backtests each of size 14, the splits will be as follows: 1. Train: 1 to 72. Validation: 73 to 86. 2. Train: 1 to 86. Validation: 87 to 100. You can control the size between each cross validation window using the `step_size` argument. For example, if we have 100 samples and we want to perform 2 backtests each of size 14 and move one step ahead in each fold (`step_size=1`), the splits will be as follows: 1. Train: 1 to 85. Validation: 86 to 99. 2. Train: 1 to 86. Validation: 87 to 100. You can also perform cross validation without refitting your models for each window by setting `refit=False`. This allows you to evaluate the performance of your models using multiple window sizes without having to retrain them each time. ```python theme={null} fcst = MLForecast( models=lgb.LGBMRegressor(random_state=0, verbosity=-1), freq=1, lags=[24 * (i+1) for i in range(7)], lag_transforms={ 1: [RollingMean(window_size=24)], 24: [RollingMean(window_size=24)], 48: [ExponentiallyWeightedMean(alpha=0.3)], }, num_threads=1, target_transforms=[Differences([24])], ) cv_results = fcst.cross_validation( train, n_windows=2, h=horizon, step_size=horizon, fitted=True, ) cv_results ``` | | unique\_id | ds | cutoff | y | LGBMRegressor | | --- | ---------- | --- | ------ | ---- | ------------- | | 0 | H196 | 865 | 864 | 15.5 | 15.373393 | | 1 | H196 | 866 | 864 | 15.1 | 14.973393 | | 2 | H196 | 867 | 864 | 14.8 | 14.673393 | | 3 | H196 | 868 | 864 | 14.4 | 14.373393 | | 4 | H196 | 869 | 864 | 14.2 | 14.073393 | | ... | ... | ... | ... | ... | ... | | 379 | H413 | 956 | 912 | 59.0 | 64.284167 | | 380 | H413 | 957 | 912 | 58.0 | 64.830429 | | 381 | H413 | 958 | 912 | 53.0 | 40.726851 | | 382 | H413 | 959 | 912 | 38.0 | 42.739657 | | 383 | H413 | 960 | 912 | 46.0 | 52.802769 | Since we set `fitted=True` we can access the predictions for the training sets as well with the `cross_validation_fitted_values` method. ```python theme={null} fcst.cross_validation_fitted_values() ``` | | unique\_id | ds | fold | y | LGBMRegressor | | ---- | ---------- | --- | ---- | ---- | ------------- | | 0 | H196 | 193 | 0 | 12.7 | 12.673393 | | 1 | H196 | 194 | 0 | 12.3 | 12.273393 | | 2 | H196 | 195 | 0 | 11.9 | 11.873393 | | 3 | H196 | 196 | 0 | 11.7 | 11.673393 | | 4 | H196 | 197 | 0 | 11.4 | 11.473393 | | ... | ... | ... | ... | ... | ... | | 5563 | H413 | 908 | 1 | 49.0 | 50.620196 | | 5564 | H413 | 909 | 1 | 39.0 | 35.972331 | | 5565 | H413 | 910 | 1 | 29.0 | 29.359678 | | 5566 | H413 | 911 | 1 | 24.0 | 25.784563 | | 5567 | H413 | 912 | 1 | 20.0 | 23.168413 | We can also compute prediction intervals by passing a configuration to `prediction_intervals` as well as values for the width through `levels`. ```python theme={null} cv_results_intervals = fcst.cross_validation( train, n_windows=2, h=horizon, step_size=horizon, prediction_intervals=PredictionIntervals(h=horizon), level=[80, 90] ) cv_results_intervals ``` | | unique\_id | ds | cutoff | y | LGBMRegressor | LGBMRegressor-lo-90 | LGBMRegressor-lo-80 | LGBMRegressor-hi-80 | LGBMRegressor-hi-90 | | --- | ---------- | --- | ------ | ---- | ------------- | ------------------- | ------------------- | ------------------- | ------------------- | | 0 | H196 | 865 | 864 | 15.5 | 15.373393 | 15.311379 | 15.316528 | 15.430258 | 15.435407 | | 1 | H196 | 866 | 864 | 15.1 | 14.973393 | 14.940556 | 14.940556 | 15.006230 | 15.006230 | | 2 | H196 | 867 | 864 | 14.8 | 14.673393 | 14.606230 | 14.606230 | 14.740556 | 14.740556 | | 3 | H196 | 868 | 864 | 14.4 | 14.373393 | 14.306230 | 14.306230 | 14.440556 | 14.440556 | | 4 | H196 | 869 | 864 | 14.2 | 14.073393 | 14.006230 | 14.006230 | 14.140556 | 14.140556 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 379 | H413 | 956 | 912 | 59.0 | 64.284167 | 29.890099 | 34.371545 | 94.196788 | 98.678234 | | 380 | H413 | 957 | 912 | 58.0 | 64.830429 | 56.874572 | 57.827689 | 71.833169 | 72.786285 | | 381 | H413 | 958 | 912 | 53.0 | 40.726851 | 35.296195 | 35.846206 | 45.607495 | 46.157506 | | 382 | H413 | 959 | 912 | 38.0 | 42.739657 | 35.292153 | 35.807640 | 49.671674 | 50.187161 | | 383 | H413 | 960 | 912 | 46.0 | 52.802769 | 42.465597 | 43.895670 | 61.709869 | 63.139941 | The `refit` argument allows us to control if we want to retrain the models in every window. It can either be: * A boolean: True will retrain on every window and False only on the first one. * A positive integer: The models will be trained on the first window and then every `refit` windows. ```python theme={null} fcst = MLForecast( models=LinearRegression(), freq=1, lags=[1, 24], ) for refit, expected_models in zip([True, False, 2], [4, 1, 2]): fcst.cross_validation( train, n_windows=4, h=horizon, refit=refit, ) assert len(fcst.cv_models_) == expected_models ``` ```python theme={null} fig = plot_series(forecasts_df=cv_results.drop(columns='cutoff')) fig ``` ```python theme={null} fig = plot_series(forecasts_df=cv_results_intervals.drop(columns='cutoff'), level=[90]) fig ``` ### Using LightGBMCV to tune your forecasts Once you’ve found a set of features and parameters that work for your problem you can build a forecast object from it using [`MLForecast.from_cv`](https://Nixtla.github.io/mlforecast/forecast.html#mlforecast.from_cv), which takes the trained [`LightGBMCV`](https://Nixtla.github.io/mlforecast/lgb_cv.html#lightgbmcv) object and builds an [`MLForecast`](https://Nixtla.github.io/mlforecast/forecast.html#mlforecast) object that will use the same features and parameters. Then you can call fit and predict as you normally would. ```python theme={null} cv = LightGBMCV( freq=1, lags=[24 * (i+1) for i in range(7)], lag_transforms={ 48: [ExponentiallyWeightedMean(alpha=0.3)], }, num_threads=1, target_transforms=[Differences([24])] ) hist = cv.fit( train, n_windows=2, h=horizon, params={'verbosity': -1}, ) ``` ```text theme={null} [10] mape: 0.118569 [20] mape: 0.111506 [30] mape: 0.107314 [40] mape: 0.106089 [50] mape: 0.106630 Early stopping at round 50 Using best iteration: 40 ``` ```python theme={null} fcst = MLForecast.from_cv(cv) assert cv.best_iteration_ == fcst.models['LGBMRegressor'].n_estimators ``` # Prediction intervals Source: https://nixtlaverse.nixtla.io/mlforecast/docs/tutorials/prediction_intervals_in_forecasting_models.html The objective of the following article is to provide a step-by-step guide on building `Prediction intervals in forecasting models` using `mlforecast`. During this walkthrough, we will become familiar with the main `MlForecast` class and some relevant methods such as `MLForecast.fit`, `MLForecast.predict` and `MLForecast.cross_validation` in other. Let’s start!!! # Table of contents 1. [Introduction](#introduction) 2. [Forecasts and prediction intervals](#forecasts-and-prediction-intervals) 3. [Installing mlforecast](#installing-mlforecast) 4. [Loading libraries and data](#loading-libraries-and-data) 5. [Explore Data with the plot method](#explore-data-with-the-plot-method) 6. [Split the data into training and testing](#split-the-data-into-training-and-testing) 7. [Modeling with mlforecast](#modeling-with-mlforecast) 8. [References](#references) # Introduction The target of our prediction is something unknown (otherwise we wouldn’t be making a prediction), so we can think of it as a random variable. For example, the total sales for the next month could have different possible values, and we won’t know what the exact value will be until we get the actual sales at the end of the month. Until next month’s sales are known, this is a random amount. By the time the next month draws near, we usually have a pretty good idea of possible sales values. However, if we are forecasting sales for the same month next year, the possible values can vary much more. In most forecasting cases, the variability associated with what we are forecasting reduces as we get closer to the event. In other words, the further back in time we make the prediction, the more uncertainty there is. We can imagine many possible future scenarios, each yielding a different value for what we are trying to forecast. When we obtain a forecast, we are estimating the middle of the range of possible values the random variable could take. Often, a forecast is accompanied by a prediction interval giving a range of values the random variable could take with relatively high probability. For example, a 95% prediction interval contains a range of values which should include the actual future value with probability 95%. Rather than plotting individual possible futures , we usually show these prediction intervals instead. When we generate a forecast, we usually produce a single value known as the point forecast. This value, however, doesn’t tell us anything about the uncertainty associated with the forecast. To have a measure of this uncertainty, we need prediction intervals. A prediction interval is a range of values that the forecast can take with a given probability. Hence, a 95% prediction interval should contain a range of values that include the actual future value with probability 95%. Probabilistic forecasting aims to generate the full forecast distribution. Point forecasting, on the other hand, usually returns the mean or the median or said distribution. However, in real-world scenarios, it is better to forecast not only the most probable future outcome, but many alternative outcomes as well. The problem is that some timeseries models provide forecast distributions, but some other ones only provide point forecasts. How can we then estimate the uncertainty of predictions? # Forecasts and prediction intervals There are at least four sources of uncertainty in forecasting using time series models: 1. The random error term; 2. The parameter estimates; 3. The choice of model for the historical data; 4. The continuation of the historical data generating process into the future. When we produce prediction intervals for time series models, we generally only take into account the first of these sources of uncertainty. It would be possible to account for 2 and 3 using simulations, but that is almost never done because it would take too much time to compute. As computing speeds increase, it might become a viable approach in the future. Even if we ignore the model uncertainty and the DGP uncertainty (sources 3 and 4), and just try to allow for parameter uncertainty as well as the random error term (sources 1 and 2), there are no closed form solutions apart from some simple special cases. see full article [Rob J Hyndman](https://robjhyndman.com/hyndsight/narrow-pi/) ## Forecast distributions We use forecast distributions to express the uncertainty in our predictions. These probability distributions describe the probability of observing different future values using the fitted model. The point forecast corresponds to the mean of this distribution. Most time series models generate forecasts that follow a normal distribution, which implies that we assume that possible future values follow a normal distribution. However, later in this section we will look at some alternatives to normal distributions. ### Importance of Confidence Interval Prediction in Time Series: 1. Uncertainty Estimation: The confidence interval provides a measure of the uncertainty associated with time series predictions. It enables variability and the range of possible future values to be quantified, which is essential for making informed decisions. 2. Precision evaluation: By having a confidence interval, the precision of the predictions can be evaluated. If the interval is narrow, it indicates that the forecast is more accurate and reliable. On the other hand, if the interval is wide, it indicates greater uncertainty and less precision in the predictions. 3. Risk management: The confidence interval helps in risk management by providing information about possible future scenarios. It allows identifying the ranges in which the real values could be located and making decisions based on those possible scenarios. 4. Effective communication: The confidence interval is a useful tool for communicating predictions clearly and accurately. It allows the variability and uncertainty associated with the predictions to be conveyed to the stakeholders, avoiding a wrong or overly optimistic interpretation of the results. Therefore, confidence interval prediction in time series is essential to understand and manage uncertainty, assess the accuracy of predictions, and make informed decisions based on possible future scenarios. ## Prediction intervals A prediction interval gives us a range in which we expect $y_t$ to lie with a specified probability. For example, if we assume that the distribution of future observations follows a normal distribution, a 95% prediction interval for the forecast of step h would be represented by the range $\hat{y}_{T+h|T} \pm 1.96 \hat\sigma_h,$ Where $\hat\sigma_h$ is an estimate of the standard deviation of the h -step forecast distribution. More generally, a prediction interval can be written as $\hat{y}_{T+h|T} \pm c \hat\sigma_h$ In this context, the term “multiplier c” is associated with the probability of coverage. In this article, intervals of 80% and 95% are typically calculated, but any other percentage can be used. The table below shows the values of c corresponding to different coverage probabilities, assuming a normal forecast distribution. | Percentage | Multiplier | | ---------- | ---------- | | 50 | 0.67 | | 55 | 0.76 | | 60 | 0.84 | | 65 | 0.93 | | 70 | 1.04 | | 75 | 1.15 | | 80 | 1.28 | | 85 | 1.44 | | 90 | 1.64 | | 95 | 1.96 | | 96 | 2.05 | | 97 | 2.17 | | 98 | 2.33 | | 99 | 2.58 | Prediction intervals are valuable because they reflect the uncertainty in the predictions. If we only generate point forecasts, we cannot assess how accurate those forecasts are. However, by providing prediction intervals, the amount of uncertainty associated with each forecast becomes apparent. For this reason, point forecasts may lack significant value without the inclusion of corresponding forecast intervals. ## One-step prediction intervals When making a prediction for a future step, it is possible to estimate the standard deviation of the forecast distribution using the standard deviation of the residuals, which is calculated by where $K$ is the number of parameters estimated in the forecasting method, and $M$ is the number of missing values in the residuals. (For example, $M=1$ for a naive forecast, because we can’t forecast the first observation.) ## Multi-step prediction intervals A typical feature of forecast intervals is that they tend to increase in length as the forecast horizon lengthens. As we move further out in time, there is greater uncertainty associated with the prediction, resulting in wider prediction intervals. In general, σh tends to increase as h increases (although there are some nonlinear forecasting methods that do not follow this property). To generate a prediction interval, it is necessary to have an estimate of σh. As mentioned above, for one-step forecasts (h=1), equation (1) provides a good estimate of the standard deviation of the forecast, σ1. However, for multi-step forecasts, a more complex calculation method is required. These calculations assume that the residuals are uncorrelated with each other. ## Benchmark methods For the four benchmark methods, it is possible to mathematically derive the forecast standard deviation under the assumption of uncorrelated residuals. If $\hat{\sigma}_h$ denotes the standard deviation of the $h$ -step forecast distribution, and $\hat{\sigma}$ is the residual standard deviation given by (1), then we can use the expressions shown in next Table. Note that when $h=1$ and $T$ is large, these all give the same approximate value $\hat{\sigma}$. | Method | h-step forecast standard deviation | | ------------------------ | ------------------------------------------ | | Mean forecasts | $\hat\sigma_h = \hat\sigma\sqrt{1 + 1/T}$ | | Naïve forecasts | $\hat\sigma_h = \hat\sigma\sqrt{h}$ | | Seasonal naïve forecasts | $\hat\sigma_h = \hat\sigma\sqrt{k+1}$ | | Drift forecasts | $\hat\sigma_h = \hat\sigma\sqrt{h(1+h/T)}$ | Note that when $h=1$ and $T$ is large, these all give the same approximate value $\hat{\sigma}$. ## Prediction intervals from bootstrapped residuals When a normal distribution for the residuals is an unreasonable assumption, one alternative is to use bootstrapping, which only assumes that the residuals are uncorrelated with constant variance. We will illustrate the procedure using a naïve forecasting method. A one-step forecast error is defined as $e_t = y_t - \hat{y}_{t|t-1}$. For a naïve forecasting method, $\hat{y}_{t|t-1} = y_{t-1}$, so we can rewrite this as $y_t = y_{t-1} + e_t.$ Assuming future errors will be similar to past errors, when $t>T$ we can replace $e_{t}$ by sampling from the collection of errors we have seen in the past (i.e., the residuals). So we can simulate the next observation of a time series using $y^*_{T+1} = y_{T} + e^*_{T+1}$ where $e^*_{T+1}$ is a randomly sampled error from the past, and $y^*_{T+1}$ is the possible future value that would arise if that particular error value occurred. We use We use a \* to indicate that this is not the observed $y_{T+1}$ value, but one possible future that could occur. Adding the new simulated observation to our data set, we can repeat the process to obtain $y^*_{T+2} = y_{T+1}^* + e^*_{T+2},$ where $e^*_{T+2}$ is another draw from the collection of residuals. Continuing in this way, we can simulate an entire set of future values for our time series. ## Conformal Prediction Multi-quantile losses and statistical models can provide provide prediction intervals, but the problem is that these are uncalibrated, meaning that the actual frequency of observations falling within the interval does not align with the confidence level associated with it. For example, a calibrated 95% prediction interval should contain the true value 95% of the time in repeated sampling. An uncalibrated 95% prediction interval, on the other hand, might contain the true value only 80% of the time, or perhaps 99% of the time. In the first case, the interval is too narrow and underestimates the uncertainty, while in the second case, it is too wide and overestimates the uncertainty. Statistical methods also assume normality. Here, we talk about another method called conformal prediction that doesn’t require any distributional assumptions. Conformal prediction intervals use cross-validation on a point forecaster model to generate the intervals. This means that no prior probabilities are needed, and the output is well-calibrated. No additional training is needed, and the model is treated as a black box. The approach is compatible with any model [mlforecast](https://github.com/nixtla/mlforecast) now supports Conformal Prediction on all available models. # Installing mlforecast * using pip: `pip install mlforecast` * using with conda: `conda install -c conda-forge mlforecast` # Loading libraries and data ```python theme={null} # Handling and processing of Data # ============================================================================== import numpy as np import pandas as pd import scipy.stats as stats # Handling and processing of Data for Date (time) # ============================================================================== import datetime import time from datetime import datetime, timedelta # # ============================================================================== from statsmodels.tsa.stattools import adfuller import statsmodels.api as sm import statsmodels.tsa.api as smt from statsmodels.tsa.seasonal import seasonal_decompose # # ============================================================================== from utilsforecast.plotting import plot_series ``` ```python theme={null} import xgboost as xgb from mlforecast import MLForecast from mlforecast.lag_transforms import ExpandingMean, ExponentiallyWeightedMean, RollingMean from mlforecast.target_transforms import Differences from mlforecast.utils import PredictionIntervals ``` ```python theme={null} # Plot # ============================================================================== import matplotlib.pyplot as plt import matplotlib.ticker as ticker from statsmodels.graphics.tsaplots import plot_acf, plot_pacf ``` ## Read Data ```python theme={null} data_url = "https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/nyc_taxi.csv" df = pd.read_csv(data_url, parse_dates=["timestamp"]) df.head() ``` | | timestamp | value | | - | ------------------- | ----- | | 0 | 2014-07-01 00:00:00 | 10844 | | 1 | 2014-07-01 00:30:00 | 8127 | | 2 | 2014-07-01 01:00:00 | 6210 | | 3 | 2014-07-01 01:30:00 | 4656 | | 4 | 2014-07-01 02:00:00 | 3820 | The input to MlForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"] = "1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | ----- | ---------- | | 0 | 2014-07-01 00:00:00 | 10844 | 1 | | 1 | 2014-07-01 00:30:00 | 8127 | 1 | | 2 | 2014-07-01 01:00:00 | 6210 | 1 | | 3 | 2014-07-01 01:30:00 | 4656 | 1 | | 4 | 2014-07-01 02:00:00 | 3820 | 1 | ```python theme={null} df.info() ``` ```text theme={null} RangeIndex: 10320 entries, 0 to 10319 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 ds 10320 non-null datetime64[ns] 1 y 10320 non-null int64 2 unique_id 10320 non-null object dtypes: datetime64[ns](1), int64(1), object(1) memory usage: 242.0+ KB ``` # Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints 8 random series from the dataset and is useful for basic EDA. ```python theme={null} fig = plot_series(df) ``` ## The Augmented Dickey-Fuller Test The Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis. Augmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis. * Null Hypothesis: Time Series is non-stationary. It gives a time-dependent trend. * Alternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time. * ADF or t Statistic \< critical values: Reject the null hypothesis, time series is stationary. * ADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary. ```python theme={null} def augmented_dickey_fuller_test(series , column_name): print (f'Dickey-Fuller test results for columns: {column_name}') dftest = adfuller(series, autolag='AIC') dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used']) for key,value in dftest[4].items(): dfoutput['Critical Value (%s)'%key] = value print (dfoutput) if dftest[1] <= 0.05: print("Conclusion:====>") print("Reject the null hypothesis") print("The data is stationary") else: print("Conclusion:====>") print("The null hypothesis cannot be rejected") print("The data is not stationary") ``` ```python theme={null} augmented_dickey_fuller_test(df["y"],'Ads') ``` ```text theme={null} Dickey-Fuller test results for columns: Ads Test Statistic -1.076452e+01 p-value 2.472132e-19 No Lags Used 3.900000e+01 Number of observations used 1.028000e+04 Critical Value (1%) -3.430986e+00 Critical Value (5%) -2.861821e+00 Critical Value (10%) -2.566920e+00 dtype: float64 Conclusion:====> Reject the null hypothesis The data is stationary ``` ## Autocorrelation plots ### Autocorrelation Function **Definition 1.** Let $\{x_t;1 ≤ t ≤ n\}$ be a time series sample of size n from $\{X_t\}$. 1. $\bar x = \sum_{t=1}^n \frac{x_t}{n}$ is called the sample mean of $\{X_t\}$. 2. $c_k =\sum_{t=1}^{n−k} (x_{t+k}- \bar x)(x_t−\bar x)/n$ is known as the sample autocovariance function of $\{X_t\}$. 3. $r_k = c_k /c_0$ is said to be the sample autocorrelation function of $\{X_t\}$. Note the following remarks about this definition: * Like most literature, this guide uses ACF to denote the sample autocorrelation function as well as the autocorrelation function. What is denoted by ACF can easily be identified in context. * Clearly c0 is the sample variance of $\{X_t\}$. Besides, $r_0 = c_0/c_0 = 1$ and for any integer $k, |r_k| ≤ 1$. * When we compute the ACF of any sample series with a fixed length $n$, we cannot put too much confidence in the values of $r_k$ for large k’s, since fewer pairs of $(x_{t +k }, x_t )$ are available for calculating $r_k$ as $k$ is large. One rule of thumb is not to estimate $r_k$ for $k > n/3$, and another is $n ≥ 50, k ≤ n/4$. In any case, it is always a good idea to be careful. * We also compute the ACF of a nonstationary time series sample by Definition 1. In this case, however, the ACF or $r_k$ very slowly or hardly tapers off as $k$ increases. * Plotting the ACF $(r_k)$ against lag $k$ is easy but very helpful in analyzing time series sample. Such an ACF plot is known as a correlogram. * If $\{X_t\}$ is stationary with $E(X_t)=0$ and $\rho_k =0$ for all $k \neq 0$, that is, it is a white noise series, then the sampling distribution of $r_k$ is asymptotically normal with the mean 0 and the variance of $1/n$. Hence, there is about 95% chance that $r_k$ falls in the interval $[−1.96/√n, 1.96/√n]$. Now we can give a summary that (1) if the time series plot of a time series clearly shows a trend or/and seasonality, it is surely nonstationary; (2) if the ACF $r_k$ very slowly or hardly tapers off as lag $k$ increases, the time series should also be nonstationary. ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); # Grafico plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.savefig("../../figs/prediction_intervals_in_forecasting_models__autocorrelation.png", bbox_inches='tight') plt.close(); ``` ## Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* multiplicative Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = \text{level} + \text{trend} + \text{seasonality} + \text{noise}$ ## Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = \text{level} * \text{trend} * \text{seasonality} * \text{noise}$ ### Additive ```python theme={null} a = seasonal_decompose(df["y"], model = "additive", period=24).plot() a.savefig('../../figs/prediction_intervals_in_forecasting_models__seasonal_decompose_aditive.png', bbox_inches='tight') plt.close() ``` ### Multiplicative ```python theme={null} b = seasonal_decompose(df["y"], model = "Multiplicative", period=24).plot() b.savefig('../../figs/prediction_intervals_in_forecasting_models__seasonal_decompose_multiplicative.png', bbox_inches='tight') plt.close(); ``` # Split the data into training and testing Let’s divide our data into sets 1. Data to train our model. 2. Data to test our model For the test data we will use the last 500 hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2015-01-21 13:30:00'] test = df[df.ds>'2015-01-21 13:30:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((9820, 3), (500, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} fig = plot_series(train,test) ``` # Modeling with mlforecast ## Building Model We define the model that we want to use, for our example we are going to use the `XGBoost model`. ```python theme={null} model1 = [xgb.XGBRegressor()] ``` We can use the `MLForecast.preprocess` method to explore different transformations. If it is true that the series we are working with is a stationary series see (Dickey fuller test), however for the sake of practice and instruction in this guide, we will apply the difference to our series, we will do this using the `target_transforms parameter` and calling the diff function like: `mlforecast.target_transforms.Differences` ```python theme={null} mlf = MLForecast(models=model1, freq='30min', target_transforms=[Differences([1])], ) ``` It is important to take into account when we use the parameter `target_transforms=[Differences([1])]` in case the series is stationary we can use a difference, or in the case that the series is not stationary, we can use more than one difference so that the series is constant over time, that is, that it is constant in mean and in variance. ```python theme={null} prep = mlf.preprocess(df) prep ``` | | ds | y | unique\_id | | ----- | ------------------- | ------- | ---------- | | 1 | 2014-07-01 00:30:00 | -2717.0 | 1 | | 2 | 2014-07-01 01:00:00 | -1917.0 | 1 | | 3 | 2014-07-01 01:30:00 | -1554.0 | 1 | | 4 | 2014-07-01 02:00:00 | -836.0 | 1 | | 5 | 2014-07-01 02:30:00 | -947.0 | 1 | | ... | ... | ... | ... | | 10315 | 2015-01-31 21:30:00 | 951.0 | 1 | | 10316 | 2015-01-31 22:00:00 | 1051.0 | 1 | | 10317 | 2015-01-31 22:30:00 | 1588.0 | 1 | | 10318 | 2015-01-31 23:00:00 | -718.0 | 1 | | 10319 | 2015-01-31 23:30:00 | -303.0 | 1 | This has subtracted the lag 1 from each value, we can see what our series look like now. ```python theme={null} fig = plot_series(prep) ``` ## Adding features ### Lags Looks like the seasonality is gone, we can now try adding some lag features. ```python theme={null} mlf = MLForecast(models=model1, freq='30min', lags=[1,24], target_transforms=[Differences([1])], ) ``` ```python theme={null} prep = mlf.preprocess(df) prep ``` | | ds | y | unique\_id | lag1 | lag24 | | ----- | ------------------- | ------ | ---------- | ------ | ------- | | 25 | 2014-07-01 12:30:00 | -22.0 | 1 | 445.0 | -2717.0 | | 26 | 2014-07-01 13:00:00 | -708.0 | 1 | -22.0 | -1917.0 | | 27 | 2014-07-01 13:30:00 | 1281.0 | 1 | -708.0 | -1554.0 | | 28 | 2014-07-01 14:00:00 | 87.0 | 1 | 1281.0 | -836.0 | | 29 | 2014-07-01 14:30:00 | 1045.0 | 1 | 87.0 | -947.0 | | ... | ... | ... | ... | ... | ... | | 10315 | 2015-01-31 21:30:00 | 951.0 | 1 | 428.0 | 4642.0 | | 10316 | 2015-01-31 22:00:00 | 1051.0 | 1 | 951.0 | -519.0 | | 10317 | 2015-01-31 22:30:00 | 1588.0 | 1 | 1051.0 | 2411.0 | | 10318 | 2015-01-31 23:00:00 | -718.0 | 1 | 1588.0 | 214.0 | | 10319 | 2015-01-31 23:30:00 | -303.0 | 1 | -718.0 | 2595.0 | ```python theme={null} prep.drop(columns=['unique_id', 'ds']).corr()['y'] ``` ```text theme={null} y 1.000000 lag1 0.663082 lag24 0.155366 Name: y, dtype: float64 ``` ### Lag transforms Lag transforms are defined as a dictionary where the keys are the lags and the values are lists of the transformations that we want to apply to that lag. You can refer to the [lag transformations guide](../how-to-guides/lag_transforms_guide.html) for more details. ```python theme={null} mlf = MLForecast(models=model1, freq='30min', lags=[1,24], lag_transforms={1: [ExpandingMean()], 24: [RollingMean(window_size=7)]}, target_transforms=[Differences([1])], ) ``` ```python theme={null} prep = mlf.preprocess(df) prep ``` | | ds | y | unique\_id | lag1 | lag24 | expanding\_mean\_lag1 | rolling\_mean\_lag24\_window\_size7 | | ----- | ------------------- | ------- | ---------- | ------- | ------ | --------------------- | ----------------------------------- | | 31 | 2014-07-01 15:30:00 | -836.0 | 1 | -1211.0 | -305.0 | 284.533325 | -1254.285767 | | 32 | 2014-07-01 16:00:00 | -2316.0 | 1 | -836.0 | 157.0 | 248.387100 | -843.714294 | | 33 | 2014-07-01 16:30:00 | -1215.0 | 1 | -2316.0 | -63.0 | 168.250000 | -578.857117 | | 34 | 2014-07-01 17:00:00 | 2190.0 | 1 | -1215.0 | 357.0 | 126.333336 | -305.857147 | | 35 | 2014-07-01 17:30:00 | 2322.0 | 1 | 2190.0 | 1849.0 | 187.029419 | 77.714287 | | ... | ... | ... | ... | ... | ... | ... | ... | | 10315 | 2015-01-31 21:30:00 | 951.0 | 1 | 428.0 | 4642.0 | 1.248303 | 2064.285645 | | 10316 | 2015-01-31 22:00:00 | 1051.0 | 1 | 951.0 | -519.0 | 1.340378 | 1873.428589 | | 10317 | 2015-01-31 22:30:00 | 1588.0 | 1 | 1051.0 | 2411.0 | 1.442129 | 2179.000000 | | 10318 | 2015-01-31 23:00:00 | -718.0 | 1 | 1588.0 | 214.0 | 1.595910 | 1888.714233 | | 10319 | 2015-01-31 23:30:00 | -303.0 | 1 | -718.0 | 2595.0 | 1.526168 | 2071.714355 | You can see that both approaches get to the same result, you can use whichever one you feel most comfortable with. ## Date features If your time column is made of timestamps then it might make sense to extract features like week, dayofweek, quarter, etc. You can do that by passing a list of strings with pandas time/date components. You can also pass functions that will take the time column as input, as we’ll show here. ```python theme={null} mlf = MLForecast(models=model1, freq='30min', lags=[1,24], lag_transforms={1: [ExpandingMean()], 24: [RollingMean(window_size=7)]}, target_transforms=[Differences([1])], date_features=["year", "month", "day", "hour"]) # Seasonal data ``` ```python theme={null} prep = mlf.preprocess(df) prep ``` | | ds | y | unique\_id | lag1 | lag24 | expanding\_mean\_lag1 | rolling\_mean\_lag24\_window\_size7 | year | month | day | hour | | ----- | ------------------- | ------- | ---------- | ------- | ------ | --------------------- | ----------------------------------- | ---- | ----- | --- | ---- | | 31 | 2014-07-01 15:30:00 | -836.0 | 1 | -1211.0 | -305.0 | 284.533325 | -1254.285767 | 2014 | 7 | 1 | 15 | | 32 | 2014-07-01 16:00:00 | -2316.0 | 1 | -836.0 | 157.0 | 248.387100 | -843.714294 | 2014 | 7 | 1 | 16 | | 33 | 2014-07-01 16:30:00 | -1215.0 | 1 | -2316.0 | -63.0 | 168.250000 | -578.857117 | 2014 | 7 | 1 | 16 | | 34 | 2014-07-01 17:00:00 | 2190.0 | 1 | -1215.0 | 357.0 | 126.333336 | -305.857147 | 2014 | 7 | 1 | 17 | | 35 | 2014-07-01 17:30:00 | 2322.0 | 1 | 2190.0 | 1849.0 | 187.029419 | 77.714287 | 2014 | 7 | 1 | 17 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 10315 | 2015-01-31 21:30:00 | 951.0 | 1 | 428.0 | 4642.0 | 1.248303 | 2064.285645 | 2015 | 1 | 31 | 21 | | 10316 | 2015-01-31 22:00:00 | 1051.0 | 1 | 951.0 | -519.0 | 1.340378 | 1873.428589 | 2015 | 1 | 31 | 22 | | 10317 | 2015-01-31 22:30:00 | 1588.0 | 1 | 1051.0 | 2411.0 | 1.442129 | 2179.000000 | 2015 | 1 | 31 | 22 | | 10318 | 2015-01-31 23:00:00 | -718.0 | 1 | 1588.0 | 214.0 | 1.595910 | 1888.714233 | 2015 | 1 | 31 | 23 | | 10319 | 2015-01-31 23:30:00 | -303.0 | 1 | -718.0 | 2595.0 | 1.526168 | 2071.714355 | 2015 | 1 | 31 | 23 | ## Fit the Model ```python theme={null} # fit the models mlf.fit(df, fitted=True, prediction_intervals=PredictionIntervals(n_windows=5, h=30, method="conformal_distribution" ) ) ``` ```text theme={null} MLForecast(models=[XGBRegressor], freq=30min, lag_features=['lag1', 'lag24', 'expanding_mean_lag1', 'rolling_mean_lag24_window_size7'], date_features=['year', 'month', 'day', 'hour'], num_threads=1) ``` Let’s see the results of our model in this case the `XGBoost model`. We can observe it with the following instruction: Let us now visualize the fitted values of our models. ```python theme={null} result = mlf.forecast_fitted_values() result = result.set_index("unique_id") result ``` | | ds | y | XGBRegressor | | ---------- | ------------------- | ------- | ------------ | | unique\_id | | | | | 1 | 2014-07-01 15:30:00 | 18544.0 | 18441.443359 | | 1 | 2014-07-01 16:00:00 | 16228.0 | 16391.152344 | | 1 | 2014-07-01 16:30:00 | 15013.0 | 15260.714844 | | 1 | 2014-07-01 17:00:00 | 17203.0 | 17066.148438 | | 1 | 2014-07-01 17:30:00 | 19525.0 | 19714.404297 | | ... | ... | ... | ... | | 1 | 2015-01-31 21:30:00 | 24670.0 | 24488.646484 | | 1 | 2015-01-31 22:00:00 | 25721.0 | 25868.865234 | | 1 | 2015-01-31 22:30:00 | 27309.0 | 27290.125000 | | 1 | 2015-01-31 23:00:00 | 26591.0 | 27123.226562 | | 1 | 2015-01-31 23:30:00 | 26288.0 | 26241.205078 | ```python theme={null} from statsmodels.stats.diagnostic import normal_ad from scipy import stats ``` ```python theme={null} sw_result = stats.shapiro(result["XGBRegressor"]) ad_result = normal_ad(np.array(result["XGBRegressor"]), axis=0) dag_result = stats.normaltest(result["XGBRegressor"], axis=0, nan_policy='propagate') ``` It’s important to note that we can only use this method if we assume that the residuals of our validation predictions are normally distributed. To see if this is the case, we will use a PP-plot and test its normality with the Anderson-Darling, Kolmogorov-Smirnov, and D’Agostino $K^2$ tests. The PP-plot(Probability-to-Probability) plots the data sample against the normal distribution plot in such a way that if normally distributed, the data points will form a straight line. The three normality tests determine how likely a data sample is from a normally distributed population using p-values. The null hypothesis for each test is that “the sample came from a normally distributed population”. This means that if the resulting p-values are below a chosen alpha value, then the null hypothesis is rejected. Thus there is evidence to suggest that the data comes from a non-normal distribution. For this article, we will use an Alpha value of 0.01. ```python theme={null} result=mlf.forecast_fitted_values() fig, axs = plt.subplots(nrows=2, ncols=2) # plot[1,1] result["XGBRegressor"].plot(ax=axs[0,0]) axs[0,0].set_title("Residuals model"); # plot axs[0,1].hist(result["XGBRegressor"], density=True,bins=50, alpha=0.5 ) axs[0,1].set_title("Density plot - Residual"); # plot stats.probplot(result["XGBRegressor"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') axs[1,0].annotate("SW p-val: {:.4f}".format(sw_result[1]), xy=(0.05,0.9), xycoords='axes fraction', fontsize=15, bbox=dict(boxstyle="round", fc="none", ec="gray", pad=0.6)) axs[1,0].annotate("AD p-val: {:.4f}".format(ad_result[1]), xy=(0.05,0.8), xycoords='axes fraction', fontsize=15, bbox=dict(boxstyle="round", fc="none", ec="gray", pad=0.6)) axs[1,0].annotate("DAG p-val: {:.4f}".format(dag_result[1]), xy=(0.05,0.7), xycoords='axes fraction', fontsize=15, bbox=dict(boxstyle="round", fc="none", ec="gray", pad=0.6)) # plot plot_acf(result["XGBRegressor"], lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.savefig("../../figs/prediction_intervals_in_forecasting_models__plot_residual_model.png", bbox_inches='tight') plt.close(); ``` ## Predict method with prediction intervals To generate forecasts use the predict method. ```python theme={null} forecast_df = mlf.predict(h=30, level=[80,95]) forecast_df.head() ``` | | unique\_id | ds | XGBRegressor | XGBRegressor-lo-95 | XGBRegressor-lo-80 | XGBRegressor-hi-80 | XGBRegressor-hi-95 | | - | ---------- | ------------------- | ------------ | ------------------ | ------------------ | ------------------ | ------------------ | | 0 | 1 | 2015-02-01 00:00:00 | 26320.298828 | 25559.884241 | 25680.228369 | 26960.369287 | 27080.713416 | | 1 | 1 | 2015-02-01 00:30:00 | 26446.472656 | 24130.429614 | 25195.461621 | 27697.483691 | 28762.515698 | | 2 | 1 | 2015-02-01 01:00:00 | 24909.970703 | 23094.950537 | 23579.583398 | 26240.358008 | 26724.990869 | | 3 | 1 | 2015-02-01 01:30:00 | 24405.402344 | 21548.628296 | 22006.662598 | 26804.142090 | 27262.176392 | | 4 | 1 | 2015-02-01 02:00:00 | 22292.390625 | 20666.736963 | 21130.215430 | 23454.565820 | 23918.044287 | ## Plot prediction intervals Now let’s visualize the result of our forecast and the historical data of our time series, also let’s draw the confidence interval that we have obtained when making the prediction with 95% confidence. ```python theme={null} fig = plot_series(df, forecast_df, level=[80,95], max_insample_length=200,engine="matplotlib") fig.get_axes()[0].set_title("Prediction intervals") fig.savefig('../../figs/prediction_intervals_in_forecasting_models__plot_forecasting_intervals.png', bbox_inches='tight') ``` The confidence interval is a range of values that has a high probability of containing the true value of a variable. In machine learning time series models, the confidence interval is used to estimate the uncertainty in the predictions. One of the main benefits of using the confidence interval is that it allows users to understand the accuracy of the predictions. For example, if the confidence interval is very wide, it means that the prediction is less accurate. Conversely, if the confidence interval is very narrow, it means that the prediction is more accurate. Another benefit of the confidence interval is that it helps users make informed decisions. For example, if a prediction is within the confidence interval, it means that it is likely to come true. Conversely, if a prediction is outside the confidence interval, it means that it is less likely to come true. In general, the confidence interval is an important tool for machine learning time series models. It helps users understand the accuracy of the forecasts and make informed decisions. # References 1. Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python. 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla Parameters for Mlforecast](https://nixtlaverse.nixtla.io/mlforecast/forecast.html) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, Time series cross-validation”.](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Transfer Conformal Prediction Intervals Source: https://nixtlaverse.nixtla.io/mlforecast/docs/tutorials/transfer_conformal_prediction.html This tutorial evaluates the **conformal prediction transfer methods** available in `mlforecast`. We train a model on one domain (M4 Monthly **Macro** series) and then generate calibrated prediction intervals for a different domain (M4 Monthly **Finance** series) — without retraining the model. ## Why transfer? Standard conformal prediction intervals are calibrated on the same distribution as training data. When you apply a pretrained model to **new, unseen series** from a potentially different domain, the source conformity scores may be miscalibrated for the target domain. Transfer conformal methods attempt to correct for this shift using different strategies: | Method | Strategy | Needs CV on target? | | ------------------------ | --------------------------------------------------------------------- | ------------------- | | `recalibrate` | Re-run cross-validation on target data | Yes | | `scale_aligned` | Rescale source errors by target/source scale ratio (from *y* history) | No | | `error_scaled` | Rescale source errors by target/source prediction error ratio | Yes | | `weighted_conformal` | Reweight source errors via density-ratio estimation (covariate shift) | No | | `scale_aligned_weighted` | Combine scale alignment with density-ratio weighting | No | We evaluate each method’s **empirical coverage** — the fraction of test observations that fall inside the predicted interval — and compare it to the nominal level. ## Setup ```python theme={null} import warnings warnings.filterwarnings('ignore') import lightgbm as lgb import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.preprocessing import FunctionTransformer from mlforecast import MLForecast from mlforecast.lag_transforms import ( ExpandingMean, ExponentiallyWeightedMean, RollingMean, RollingStd, SeasonalRollingMean, ) from mlforecast.target_transforms import Differences, GlobalSklearnTransformer from mlforecast.utils import PredictionIntervals ``` ## Load M4 Monthly Data The M4 Monthly dataset contains 48,000 monthly time series across 6 categories. We use it to create a cross-domain transfer scenario: * **Source domain**: `Macro` category — macroeconomic time series * **Target domain**: `Finance` category — financial time series The forecast horizon for M4 Monthly is **h = 18** months. ```python theme={null} HORIZON = 18 DATA_DIR = '../../../data' def read_and_melt(file): df = pd.read_csv(file) df.columns = ['unique_id'] + list(range(1, df.shape[1])) df = pd.melt(df, id_vars=['unique_id'], var_name='ds', value_name='y') df = df.dropna() df['ds'] = df['ds'].astype(int) return df # Load train/test splits (M4 stores them separately) m4_train = read_and_melt(f'{DATA_DIR}/m4/datasets/Monthly-train.csv') m4_test = read_and_melt(f'{DATA_DIR}/m4/datasets/Monthly-test.csv') # Adjust test ds so it continues from where train ends last_train_ds = m4_train.groupby('unique_id')['ds'].max().reset_index() last_train_ds.columns = ['unique_id', 'last_ds'] m4_test = m4_test.merge(last_train_ds, on='unique_id') m4_test['ds'] = m4_test['ds'] + m4_test['last_ds'] m4_test = m4_test.drop(columns='last_ds') print(f"Train: {m4_train.shape}, Test: {m4_test.shape}") print(f"Training series count: {m4_train['unique_id'].nunique()}") print(f"Test periods per series: {m4_test.groupby('unique_id')['ds'].count().unique().tolist()}") ``` ```text theme={null} Train: (10382411, 3), Test: (864000, 3) Training series count: 48000 Test periods per series: [18] ``` ```python theme={null} # Load category labels from M4 info m4_info = pd.read_csv(f'{DATA_DIR}/m4/datasets/M4-info.csv', usecols=['M4id', 'category']) m4_info = m4_info[m4_info['M4id'].str.startswith('M')].rename(columns={'M4id': 'unique_id'}) print("M4 Monthly category counts:") print(m4_info['category'].value_counts()) ``` ```text theme={null} M4 Monthly category counts: category Finance 10987 Micro 10975 Industry 10017 Macro 10016 Demographic 5728 Other 277 Name: count, dtype: int64 ``` ## Create Source and Target Domains ```python theme={null} rng = np.random.default_rng(42) # Source domain: sample from Macro category macro_ids = m4_info[m4_info['category'] == 'Macro']['unique_id'].values source_ids = rng.choice(macro_ids, size=800, replace=False) # Target domain: sample from Finance category (disjoint from source) finance_ids = m4_info[m4_info['category'] == 'Finance']['unique_id'].values target_ids = rng.choice(finance_ids, size=200, replace=False) # Source: use all training data (no test split needed for fitting) source_train = m4_train[m4_train['unique_id'].isin(source_ids)].copy() # Target: training data goes into new_df; test data is our evaluation ground truth target_train = m4_train[m4_train['unique_id'].isin(target_ids)].copy() target_test = m4_test[m4_test['unique_id'].isin(target_ids)].copy() print(f"Source domain: {source_train['unique_id'].nunique()} Macro series") print(f" Train obs: {len(source_train):,}") print(f" Length range: {source_train.groupby('unique_id')['ds'].count().agg(['min','max']).to_dict()}") print() print(f"Target domain: {target_train['unique_id'].nunique()} Finance series") print(f" Train obs: {len(target_train):,}") print(f" Test obs: {len(target_test):,} ({HORIZON} steps per series)") ``` ```text theme={null} Source domain: 800 Macro series Train obs: 186,308 Length range: {'min': 62, 'max': 1230} Target domain: 200 Finance series Train obs: 38,794 Test obs: 3,600 (18 steps per series) ``` ## Explore the Domains Let’s visualize a few series from each domain to get a sense of the distribution shift. ```python theme={null} fig, axes = plt.subplots(2, 3, figsize=(15, 6)) sample_source = source_ids[:3] sample_target = target_ids[:3] for ax, uid in zip(axes[0], sample_source): s = source_train[source_train['unique_id'] == uid] ax.plot(s['ds'], s['y']) ax.set_title(f'Source (Macro): {uid}') ax.set_xlabel('Period') for ax, uid in zip(axes[1], sample_target): s = target_train[target_train['unique_id'] == uid] ax.plot(s['ds'], s['y'], color='orange') ax.set_title(f'Target (Finance): {uid}') ax.set_xlabel('Period') plt.tight_layout() plt.suptitle('Source vs Target Domain Series', y=1.02, fontsize=14) plt.show() ``` ## Fit MLForecast on Source Domain ### Feature engineering for cross-domain transfer Because the model is trained on one domain and applied to another, the features must be **scale-invariant**. Tree models cannot extrapolate: if a Finance series has level changes larger than anything seen in the Macro training data, its predictions get clamped to the training range, producing large, skewed errors that no conformal correction can fully repair. We therefore model **log-returns** instead of raw differences: * `GlobalSklearnTransformer(FunctionTransformer(np.log1p, np.expm1))` followed by `Differences([1])` — the model sees relative changes, which live on a comparable scale in both domains. Back-transformed intervals scale multiplicatively with each series’ level. * Volatility and trend features (`RollingStd`, `ExponentiallyWeightedMean`, `SeasonalRollingMean`) — these sharpen the point forecasts and, importantly, give the density-ratio estimator meaningful covariates: on the raw scale, lag features mostly encode the scale difference between domains rather than the dynamics. ### Prediction interval configuration We fit on Macro series using `PredictionIntervals` with: - `method='weighted_conformal_error'` — stores lag features in the conformity score dataframe, enabling density-ratio estimation (DRE) for the `weighted_conformal` and `scale_aligned_weighted` transfer methods. - `scale_estimator='mad'` — stores per-series scale estimates (MAD of first differences), enabling the `scale_aligned` and `scale_aligned_weighted` transfer methods. Using this single fit configuration unlocks **all five** transfer methods. ```python theme={null} mlf = MLForecast( models=lgb.LGBMRegressor(n_estimators=100, verbosity=-1, random_state=0), freq=1, lags=[1, 2, 3, 4, 6, 12], lag_transforms={ 1: [ ExpandingMean(), RollingMean(window_size=3), RollingStd(window_size=12, min_samples=6), ExponentiallyWeightedMean(alpha=0.3), ], 12: [SeasonalRollingMean(season_length=12, window_size=2, min_samples=1)], }, target_transforms=[ GlobalSklearnTransformer(FunctionTransformer(func=np.log1p, inverse_func=np.expm1)), Differences([1]), ], num_threads=1, ) mlf.fit( source_train, prediction_intervals=PredictionIntervals( n_windows=2, h=HORIZON, method='weighted_conformal_error', scale_estimator='mad', ), ) ``` ```text theme={null} MLForecast(models=[LGBMRegressor], freq=1, lag_features=['lag1', 'lag2', 'lag3', 'lag4', 'lag6', 'lag12', 'expanding_mean_lag1', 'rolling_mean_lag1_window_size3', 'rolling_std_lag1_window_size12_min_samples6', 'exponentially_weighted_mean_lag1_alpha0.3', 'seasonal_rolling_mean_lag12_season_length12_window_size2_min_samples1'], date_features=[], num_threads=1) ``` ## Evaluate Transfer Methods For each transfer method, we call `mlf.predict()` with: - `new_df=target_train` — the target domain training history (Finance series) - `level=[80, 90, 95]` — the requested coverage levels - `transfer_conformal=method` — which transfer strategy to use We then merge predictions with `target_test` and compute the **empirical coverage** at each level. ```python theme={null} LEVELS = [80, 90, 95] MODEL = 'LGBMRegressor' transfer_methods = [ 'recalibrate', 'scale_aligned', 'error_scaled', 'weighted_conformal', 'scale_aligned_weighted', ] coverage_results = {} for method in transfer_methods: print(f"Running '{method}'...", end=' ', flush=True) preds = mlf.predict( h=HORIZON, level=LEVELS, new_df=target_train, transfer_conformal=method, ) merged = target_test.merge(preds, on=['unique_id', 'ds']) cov = {} for lv in LEVELS: lo_col = f'{MODEL}-lo-{lv}' hi_col = f'{MODEL}-hi-{lv}' covered = (merged['y'] >= merged[lo_col]) & (merged['y'] <= merged[hi_col]) cov[lv] = float(covered.mean()) coverage_results[method] = cov print(f"done. Coverage: { {k: f'{v:.1%}' for k,v in cov.items()} }") print("\nAll methods evaluated.") ``` ```text theme={null} Running 'recalibrate'... done. Coverage: {80: '76.6%', 90: '85.9%', 95: '92.4%'} Running 'scale_aligned'... done. Coverage: {80: '75.6%', 90: '87.2%', 95: '92.9%'} Running 'error_scaled'... done. Coverage: {80: '76.3%', 90: '86.3%', 95: '91.7%'} Running 'weighted_conformal'... done. Coverage: {80: '80.7%', 90: '89.4%', 95: '94.5%'} Running 'scale_aligned_weighted'... done. Coverage: {80: '79.7%', 90: '90.5%', 95: '95.0%'} All methods evaluated. ``` ## Results: Nominal vs Empirical Coverage A well-calibrated method should have empirical coverage close to nominal. We show the results as a summary table and a bar chart. ```python theme={null} # Build results dataframe rows = [] for method, cov_dict in coverage_results.items(): for lv, empirical in cov_dict.items(): rows.append({ 'Method': method, 'Nominal Level': f'{lv}%', 'Nominal': lv / 100, 'Empirical': empirical, 'Gap': empirical - lv / 100, }) results_df = pd.DataFrame(rows) # Pivot for display pivot = results_df.pivot(index='Method', columns='Nominal Level', values='Empirical') pivot = pivot[['80%', '90%', '95%']] pivot.columns.name = 'Empirical Coverage @' display_df = (pivot * 100).round(1).astype(str) + '%' print("Empirical Coverage by Transfer Method (nominal levels: 80%, 90%, 95%)") print("=" * 70) print(display_df.to_string()) ``` ```text theme={null} Empirical Coverage by Transfer Method (nominal levels: 80%, 90%, 95%) ====================================================================== Empirical Coverage @ 80% 90% 95% Method error_scaled 76.3% 86.3% 91.7% recalibrate 76.6% 85.9% 92.4% scale_aligned 75.6% 87.2% 92.9% scale_aligned_weighted 79.7% 90.5% 95.0% weighted_conformal 80.7% 89.4% 94.5% ``` ```python theme={null} # Bar chart: empirical vs nominal coverage fig, axes = plt.subplots(1, 3, figsize=(15, 5), sharey=False) method_labels = [ 'recalibrate', 'scale_aligned', 'error_scaled', 'weighted\nconformal', 'scale_aligned\nweighted', ] x = np.arange(len(transfer_methods)) for ax, lv in zip(axes, LEVELS): empirical_vals = [coverage_results[m][lv] * 100 for m in transfer_methods] bars = ax.bar(x, empirical_vals, width=0.6, alpha=0.8, label='Empirical') ax.axhline(lv, color='red', linewidth=2, linestyle='--', label=f'Nominal {lv}%') ax.set_xticks(x) ax.set_xticklabels(method_labels, fontsize=9) ax.set_title(f'Coverage @ {lv}% Nominal', fontsize=12) ax.set_ylabel('Empirical Coverage (%)') ax.legend() # Add value labels on bars for bar, val in zip(bars, empirical_vals): ax.text( bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.3, f'{val:.1f}%', ha='center', va='bottom', fontsize=8 ) y_min = min(min(empirical_vals), lv) - 5 y_max = max(max(empirical_vals), lv) + 5 ax.set_ylim(y_min, y_max) plt.suptitle( 'Transfer Conformal Coverage: Source=Macro, Target=Finance (M4 Monthly)', fontsize=13, y=1.02 ) plt.tight_layout() plt.show() ``` ## Coverage Gap Analysis ```python theme={null} # Signed gap: positive = over-coverage, negative = under-coverage gap_pivot = results_df.pivot(index='Method', columns='Nominal Level', values='Gap')[['80%', '90%', '95%']] gap_display = (gap_pivot * 100).round(2) print("Coverage gap (Empirical − Nominal) in percentage points:") print(" Positive = over-coverage (wider intervals than needed)") print(" Negative = under-coverage (intervals too narrow)") print() print(gap_display.to_string()) # Best method: smallest mean absolute gap across all levels mean_abs_gap = gap_pivot.abs().mean(axis=1).sort_values() print("\nMethods ranked by mean |gap| across all levels:") for method, gap in mean_abs_gap.items(): print(f" {method}: {gap*100:.2f} pp") ``` ```text theme={null} Coverage gap (Empirical − Nominal) in percentage points: Positive = over-coverage (wider intervals than needed) Negative = under-coverage (intervals too narrow) Nominal Level 80% 90% 95% Method error_scaled -3.72 -3.72 -3.33 recalibrate -3.36 -4.14 -2.61 scale_aligned -4.44 -2.83 -2.06 scale_aligned_weighted -0.31 0.47 0.03 weighted_conformal 0.69 -0.64 -0.53 Methods ranked by mean |gap| across all levels: scale_aligned_weighted: 0.27 pp weighted_conformal: 0.62 pp scale_aligned: 3.11 pp recalibrate: 3.37 pp error_scaled: 3.59 pp ``` ## Interval Width Analysis Beyond coverage, we also care about interval sharpness. Narrower intervals (lower width) are better, as long as coverage is maintained. ```python theme={null} width_results = {} for method in transfer_methods: preds = mlf.predict( h=HORIZON, level=LEVELS, new_df=target_train, transfer_conformal=method, ) widths = {} for lv in LEVELS: lo_col = f'{MODEL}-lo-{lv}' hi_col = f'{MODEL}-hi-{lv}' width = (preds[hi_col] - preds[lo_col]).mean() widths[lv] = float(width) width_results[method] = widths width_df = pd.DataFrame(width_results).T width_df.columns = [f'{lv}%' for lv in LEVELS] print("Mean interval width by method and level:") print(width_df.round(2).to_string()) ``` ```text theme={null} Mean interval width by method and level: 80% 90% 95% recalibrate 1596.68 3038.93 5199.22 scale_aligned 3410.02 5759.49 9812.96 error_scaled 1469.70 2755.49 4186.24 weighted_conformal 1883.55 3512.86 5472.55 scale_aligned_weighted 4012.52 7362.65 13545.20 ``` ```python theme={null} # Scatter plot: width vs coverage gap (ideal: small gap, narrow width) fig, axes = plt.subplots(1, 3, figsize=(15, 4)) colors = plt.cm.tab10(np.linspace(0, 0.5, len(transfer_methods))) for ax, lv in zip(axes, LEVELS): for i, method in enumerate(transfer_methods): gap = abs(coverage_results[method][lv] - lv / 100) * 100 width = width_results[method][lv] ax.scatter(width, gap, color=colors[i], s=120, label=method, zorder=3) ax.annotate( method.replace('_', '\n'), (width, gap), fontsize=7, ha='center', va='bottom' ) ax.set_xlabel('Mean Interval Width') ax.set_ylabel('|Coverage Gap| (pp)') ax.set_title(f'{lv}% Level: Width vs |Gap|') ax.set_ylim(bottom=0) ax.grid(True, alpha=0.3) plt.suptitle('Sharpness vs Calibration Trade-off', fontsize=13, y=1.02) plt.tight_layout() plt.show() ``` ## Visual Inspection: Interval Examples Let’s visually inspect the intervals produced by each method for a few target-domain series. ```python theme={null} PLOT_LEVEL = 90 EXAMPLE_IDS = target_ids[:4].tolist() # Collect predictions for all methods all_preds = {} for method in transfer_methods: preds = mlf.predict( h=HORIZON, level=[PLOT_LEVEL], new_df=target_train[target_train['unique_id'].isin(EXAMPLE_IDS)], transfer_conformal=method, ) all_preds[method] = preds ``` ```python theme={null} fig, axes = plt.subplots( len(EXAMPLE_IDS), len(transfer_methods), figsize=(4 * len(transfer_methods), 3 * len(EXAMPLE_IDS)), sharex='row' ) lo_col = f'{MODEL}-lo-{PLOT_LEVEL}' hi_col = f'{MODEL}-hi-{PLOT_LEVEL}' for row, uid in enumerate(EXAMPLE_IDS): hist = target_train[target_train['unique_id'] == uid].tail(36) test = target_test[target_test['unique_id'] == uid] for col, method in enumerate(transfer_methods): ax = axes[row][col] pred = all_preds[method][all_preds[method]['unique_id'] == uid] ax.plot(hist['ds'], hist['y'], color='black', linewidth=1) ax.plot(pred['ds'], pred[MODEL], color='blue', linewidth=1.5, label='Forecast') ax.fill_between( pred['ds'], pred[lo_col], pred[hi_col], alpha=0.3, color='blue', label=f'{PLOT_LEVEL}% PI' ) ax.scatter(test['ds'], test['y'], color='red', s=20, zorder=5, label='Actuals') if row == 0: ax.set_title(method.replace('_', '\n'), fontsize=9) if col == 0: ax.set_ylabel(f'{uid}', fontsize=9) ax.tick_params(labelsize=7) # Single legend at top handles, labels = axes[0][0].get_legend_handles_labels() fig.legend(handles, labels, loc='upper right', fontsize=9) plt.suptitle( f'Prediction Intervals ({PLOT_LEVEL}% level) — Finance Target Series', fontsize=12, y=1.01 ) plt.tight_layout() plt.show() ``` ## Summary The table and charts above show how each transfer method calibrates prediction intervals when moving from a Macro source domain to a Finance target domain in M4 Monthly data. **Key takeaways:** * **Feature engineering matters as much as the transfer method.** Modeling log-returns (`log1p` + `Differences([1])`) instead of raw differences makes the features scale-invariant across domains, removes the systematic point-forecast bias, and is what allows the weighted methods to reach near-nominal coverage. With raw differences, every method under-covers by several percentage points. * `recalibrate` runs cross-validation on the target data — it tends to be the most directly calibrated but requires running CV (computationally equivalent to retraining). * `scale_aligned` uses the scale of the *y* signal (MAD of differences) to align source residuals — zero-shot, no CV needed. * `error_scaled` runs CV on the target data to estimate prediction error magnitude — a middle ground between full recalibration and scale alignment. * `weighted_conformal` uses density-ratio estimation to reweight source conformity scores — handles covariate shift without needing target labels during calibration. * `scale_aligned_weighted` combines scale alignment with DRE weighting — the most sophisticated zero-shot method. * The residual under-coverage of the non-weighted methods comes from pooling conformity scores across heterogeneous series: pooled intervals are too wide for calm series and too narrow for volatile ones. This is precisely the failure mode the weighted/scale-aligned variants are designed to mitigate. The right method to use depends on your constraints: - If you can run CV on the target: `recalibrate` or `error_scaled` - If you need zero-shot transfer: `scale_aligned` or `scale_aligned_weighted` - If covariate shift is the main concern: `weighted_conformal` or `scale_aligned_weighted` # Feature engineering | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/feature_engineering.html Compute transformations on exogenous regressors ```python theme={null} import numpy as np import pandas as pd from nbdev import show_doc from mlforecast.lag_transforms import ExpandingMean from mlforecast.utils import generate_daily_series ``` ## Setup ```python theme={null} rng = np.random.RandomState(0) series = generate_daily_series(100, equal_ends=True) starts_ends = series.groupby( 'unique_id', observed=True, as_index=False )['ds'].agg(['min', 'max']) prices = [] for r in starts_ends.itertuples(): dates = pd.date_range(r.min, r.max + 14 * pd.offsets.Day()) df = pd.DataFrame({'ds': dates, 'price': rng.rand(dates.size)}) df['unique_id'] = r.Index prices.append(df) prices = pd.concat(prices) prices['price2'] = prices['price'] * rng.rand(prices.shape[0]) prices.head() ``` | | ds | price | unique\_id | price2 | | - | ---------- | -------- | ---------- | -------- | | 0 | 2000-10-05 | 0.548814 | 0 | 0.345011 | | 1 | 2000-10-06 | 0.715189 | 0 | 0.445598 | | 2 | 2000-10-07 | 0.602763 | 0 | 0.165147 | | 3 | 2000-10-08 | 0.544883 | 0 | 0.041373 | | 4 | 2000-10-09 | 0.423655 | 0 | 0.391577 | *** ### `transform_exog` ```python theme={null} transform_exog(df, lags=None, lag_transforms=None, id_col='unique_id', time_col='ds', num_threads=1) ``` Compute lag features for dynamic exogenous regressors. **Parameters:** | Name | Type | Description | Default | | ---------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | Dataframe with ids, times and values for the exogenous regressors. | *required* | | `lags` | list of int | Lags of the target to use as features. Defaults to None. | None | | `lag_transforms` | dict of int to list of functions | Mapping of target lags to their transformations. Defaults to None. | None | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `num_threads` | [int](#int) | Number of threads to use when computing the features. Use -1 to use all available CPU cores. Defaults to 1. | 1 | **Returns:** | Type | Description | | --------------------------------------- | --------------------------------------------- | | pandas or polars DataFrame | Original DataFrame with the computed features | ```python theme={null} transformed = transform_exog( prices, lags=[1, 2], lag_transforms={1: [ExpandingMean()]} ) transformed.head() ``` | | ds | price | unique\_id | price2 | price\_lag1 | price\_lag2 | price\_expanding\_mean\_lag1 | price2\_lag1 | price2\_lag2 | price2\_expanding\_mean\_lag1 | | - | ---------- | -------- | ---------- | -------- | ----------- | ----------- | ---------------------------- | ------------ | ------------ | ----------------------------- | | 0 | 2000-10-05 | 0.548814 | 0 | 0.345011 | NaN | NaN | NaN | NaN | NaN | NaN | | 1 | 2000-10-06 | 0.715189 | 0 | 0.445598 | 0.548814 | NaN | 0.548814 | 0.345011 | NaN | 0.345011 | | 2 | 2000-10-07 | 0.602763 | 0 | 0.165147 | 0.715189 | 0.548814 | 0.632001 | 0.445598 | 0.345011 | 0.395304 | | 3 | 2000-10-08 | 0.544883 | 0 | 0.041373 | 0.602763 | 0.715189 | 0.622255 | 0.165147 | 0.445598 | 0.318585 | | 4 | 2000-10-09 | 0.423655 | 0 | 0.391577 | 0.544883 | 0.602763 | 0.602912 | 0.041373 | 0.165147 | 0.249282 | ```python theme={null} import polars as pl ``` ```python theme={null} prices_pl = pl.from_pandas(prices) transformed_pl = transform_exog( prices_pl, lags=[1, 2], lag_transforms={1: [ExpandingMean()]}, num_threads=2, ) transformed_pl.head() ``` | ds | price | unique\_id | price2 | price\_lag1 | price\_lag2 | price\_expanding\_mean\_lag1 | price2\_lag1 | price2\_lag2 | price2\_expanding\_mean\_lag1 | | ------------------- | -------- | ---------- | -------- | ----------- | ----------- | ---------------------------- | ------------ | ------------ | ----------------------------- | | datetime\[ns] | f64 | i64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | | 2000-10-05 00:00:00 | 0.548814 | 0 | 0.345011 | NaN | NaN | NaN | NaN | NaN | NaN | | 2000-10-06 00:00:00 | 0.715189 | 0 | 0.445598 | 0.548814 | NaN | 0.548814 | 0.345011 | NaN | 0.345011 | | 2000-10-07 00:00:00 | 0.602763 | 0 | 0.165147 | 0.715189 | 0.548814 | 0.632001 | 0.445598 | 0.345011 | 0.395304 | | 2000-10-08 00:00:00 | 0.544883 | 0 | 0.041373 | 0.602763 | 0.715189 | 0.622255 | 0.165147 | 0.445598 | 0.318585 | | 2000-10-09 00:00:00 | 0.423655 | 0 | 0.391577 | 0.544883 | 0.602763 | 0.602912 | 0.041373 | 0.165147 | 0.249282 | # MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/forecast.html Full pipeline encapsulation ## ### `MLForecast` ```python theme={null} MLForecast(models, freq, lags=None, lag_transforms=None, date_features=None, num_threads=1, target_transforms=None, lag_transforms_namer=None, date_features_as_dummies=False, drop_auxiliary_columns=True) ``` Forecasting pipeline **Parameters:** | Name | Type | Description | Default | | -------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `models` | regressor or list of regressors | Models that will be trained and used to compute the forecasts. | *required* | | `freq` | [str](#str) or [int](#int) or [BaseOffset](#pandas.offsets.BaseOffset) | Pandas offset, pandas offset alias, e.g. 'D', 'W-THU' or integer denoting the frequency of the series. | *required* | | `lags` | list of int | Lags of the target to use as features. Defaults to None. | None | | `lag_transforms` | dict of int to list of functions | Mapping of target lags to their transformations. Defaults to None. | None | | `date_features` | list of str or callable | Features computed from the dates. Can be pandas date attributes or functions that will take the dates as input. Defaults to None. | None | | `num_threads` | [int](#int) | Number of threads to use when computing the features. Use -1 to use all available CPU cores. Defaults to 1. | 1 | | `target_transforms` | list of transformers | Transformations that will be applied to the target before computing the features and restored after the forecasting step. Defaults to None. | None | | `lag_transforms_namer` | [callable](#callable) | Function that takes a transformation (either function or class), a lag and extra arguments and produces a name. Defaults to None. | None | | `date_features_as_dummies` | [bool](#bool) | If True, string date features with a known finite range (e.g. 'dayofweek', 'month') are expanded into binary indicator columns named '\_' instead of being kept as ordinal integers. Defaults to False. | False | | `drop_auxiliary_columns` | bool or list of str | Controls which columns used solely for grouping are excluded from the model feature matrix. True (default) drops all columns referenced in any groupby transform. False keeps all columns. A list of strings drops only the named columns explicitly. Changed in v1.0.4: default changed from False (keep all columns) to True (auto-drop groupby columns). | True | #### `MLForecast.fit` ```python theme={null} fit(df, id_col='unique_id', time_col='ds', target_col='y', static_features=None, dropna=True, keep_last_n=None, max_horizon=None, horizons=None, horizon_features=None, horizon_feature_templates=None, prediction_intervals=None, fitted=False, as_numpy=False, weight_col=None, models_fit_kwargs=None, validate_data=True, cache_train_df=True) ``` Apply the feature engineering and train the models. **Parameters:** | Name | Type | Description | Default | | --------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | Series data in long format. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `static_features` | list of str | Names of the features that are static and will be repeated when forecasting. If `None`, will consider all columns (except id\_col and time\_col) as static. Defaults to None. | None | | `dropna` | [bool](#bool) | Drop rows with missing values produced by the transformations. Defaults to True. | True | | `keep_last_n` | [int](#int) | Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it. Pooled lag transforms (global\_/groupby/partition\_by) with a window wider than this keep that wider window instead, since their shared aggregates have no per-series buffer to trim below it. Defaults to None. | None | | `max_horizon` | [int](#int) | Train this many models, where each model will predict a specific horizon. Defaults to None. | None | | `horizons` | list of int | Train models only for specific horizons (1-indexed). For example, `horizons=[7, 14]` trains models only for steps 7 and 14. Mutually exclusive with max\_horizon. Defaults to None. | None | | `horizon_features` | dict of int to list of str | Explicit mapping of 1-indexed horizons to dynamic exogenous columns. Only supported when using `max_horizon` or `horizons`. Defaults to None. | None | | `horizon_feature_templates` | list of str | Template patterns for horizon-specific dynamic exogenous features. Each template must include exactly one '' placeholder (1-indexed), for example: \['feature\_h']. Acts as shorthand for `horizon_features` and is only supported when using `max_horizon` or `horizons`. Defaults to None. | None | | `prediction_intervals` | [PredictionIntervals](#mlforecast.conformal_prediction.PredictionIntervals) | Configuration to calibrate prediction intervals (Conformal Prediction). Defaults to None. | None | | `fitted` | [bool](#bool) | Save in-sample predictions. Defaults to False. | False | | `as_numpy` | [bool](#bool) | Cast features to numpy array. Defaults to True. | False | | `weight_col` | [str](#str) | Column that contains the sample weights. Defaults to None. | None | | `models_fit_kwargs` | [dict](#dict) | Keyword arguments for each model's fit method. Defaults to None. | None | | `validate_data` | [bool](#bool) | Run data quality validations before fitting. Warns about missing dates and raises on duplicate rows. Defaults to True. | True | | `cache_train_df` | [bool](#bool) | Cache a copy of the training data when `fitted=True` so `forecast_fitted_values(h>1)` can be called later for recursive models without passing `train_df`. Disable this to avoid the memory overhead and pass `train_df` directly to `forecast_fitted_values` when needed. Defaults to True. | True | **Returns:** | Name | Type | Description | | ------------ | ---------------------------------------------------------- | ------------------------------------------------------ | | `MLForecast` | [MLForecast](#mlforecast.forecast.MLForecast) | Forecast object with series values and trained models. | #### `MLForecast.save` ```python theme={null} save(path) ``` Save forecast object **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------------------- | ----------------------------------------- | ---------- | | `path` | [str](#str) or [Path](#pathlib.Path) | Directory where artifacts will be stored. | *required* | #### `MLForecast.load` ```python theme={null} load(path) ``` Load forecast object **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------------------- | ------------------------------- | ---------- | | `path` | [str](#str) or [Path](#pathlib.Path) | Directory with saved artifacts. | *required* | #### `MLForecast.update` ```python theme={null} update(df, validate_new_data=False) ``` Update the values of the stored series. **Parameters:** | Name | Type | Description | Default | | ------------------- | --------------------------------------- | --------------------------------------------------------- | ------------------ | | `df` | pandas or polars DataFrame | Dataframe with new observations. | *required* | | `validate_new_data` | [bool](#bool) | If True, validate continuity, start dates, and frequency. | False | #### `MLForecast.make_future_dataframe` ```python theme={null} make_future_dataframe(h) ``` Create a dataframe with all ids and future times in the forecasting horizon. **Parameters:** | Name | Type | Description | Default | | ---- | ------------------------ | ----------------------------- | ---------- | | `h` | [int](#int) | Number of periods to predict. | *required* | **Returns:** | Type | Description | | --------------------------------------------------------- | ------------------------------------------------------------------------ | | [DataFrame](#utilsforecast.compat.DataFrame) | pandas or polars DataFrame: DataFrame with expected ids and future times | #### `MLForecast.get_missing_future` ```python theme={null} get_missing_future(h, X_df) ``` Get the missing id and time combinations in `X_df`. **Parameters:** | Name | Type | Description | Default | | ------ | --------------------------------------- | -------------------------------------------------------------------------------------------- | ---------- | | `h` | [int](#int) | Number of periods to predict. | *required* | | `X_df` | pandas or polars DataFrame | Dataframe with the future exogenous features. Should have the id column and the time column. | *required* | **Returns:** | Type | Description | | --------------------------------------------------- | ------------------------------------------------------------------------------------------ | | [DFType](#utilsforecast.compat.DFType) | pandas or polars DataFrame: DataFrame with expected ids and future times missing in `X_df` | #### `MLForecast.predict` ```python theme={null} predict(h, before_predict_callback=None, after_predict_callback=None, new_df=None, level=None, X_df=None, ids=None, transfer_conformal=None) ``` Compute the predictions for the next `h` steps. **Parameters:** | Name | Type | Description | Default | | | | ------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Number of periods to predict. | *required* | | | | `before_predict_callback` | [callable](#callable) | Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index. Defaults to None. | None | | | | `after_predict_callback` | [callable](#callable) | Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index. Defaults to None. | None | | | | `new_df` | pandas or polars DataFrame | Series data of new observations for which forecasts are to be generated. This dataframe should have the same structure as the one used to fit the model, including any features and time series data. If `new_df` is not None, the method will generate forecasts for the new observations. Defaults to None. | None | | | | `level` | list of ints or floats | Confidence levels between 0 and 100 for prediction intervals. Defaults to None. | None | | | | `X_df` | pandas or polars DataFrame | Dataframe with the future exogenous features. Should have the id column and the time column. Defaults to None. | None | | | | `ids` | list of str | List with subset of ids seen during training for which the forecasts should be computed. Defaults to None. | None | | | | `transfer_conformal` | [str](#str) or [TransferConformal](#mlforecast.conformal_prediction.TransferConformal) | Strategy for adapting source conformal scores to the target domain when both `new_df` and `level` are provided. A plain string is shorthand for `TransferConformal(method=)`. Supported methods:
- `'recalibrate'`: Re-runs cross-validation on `new_df` and replaces source calibration scores with target scores. No special fit requirements. - `'error_scaled'`: Scales source residuals by the ratio of target to source prediction-error standard deviation, estimated via CV on `new_df`. No special fit requirements beyond `prediction_intervals`. - `'scale_aligned'`: Zero-shot — scales source residuals by the ratio of target to source y-history variance without running CV. Requires the source model to have been fit with \`\`PredictionIntervals(scale\_estimator='mad' | 'std')`. - `'scale\_aligned\_weighted'`: Combines scale alignment with density-ratio reweighting (DRE). Requires both `scale\_estimator` at fit time and a weighted conformal method (`weighted\_conformal\_error`or`weighted\_conformal\_distribution`). - `'weighted\_conformal'`: Reweights source residuals via density-ratio estimation using model covariates. Requires the source model to have been fit with `PredictionIntervals(method='weighted\_conformal\_error' | 'weighted\_conformal\_distribution')`so that source features are stored.
Defaults to`None`(equivalent to`'recalibrate'`when`new\_df`and`level\`\` are both provided). | None | **Returns:** | Type | Description | | --------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | [DFType](#utilsforecast.compat.DFType) | pandas or polars DataFrame: Predictions for each serie and timestep, with one column per model. | #### `MLForecast.preprocess` ```python theme={null} preprocess(df, id_col='unique_id', time_col='ds', target_col='y', static_features=None, dropna=True, keep_last_n=None, max_horizon=None, horizons=None, horizon_features=None, horizon_feature_templates=None, return_X_y=False, as_numpy=False, weight_col=None, validate_data=True) ``` Add the features to `data`. **Parameters:** | Name | Type | Description | Default | | --------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas DataFrame | Series data in long format. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `static_features` | list of str | Names of the features that are static and will be repeated when forecasting. Defaults to None. | None | | `dropna` | [bool](#bool) | Drop rows with missing values produced by the transformations. Defaults to True. | True | | `keep_last_n` | [int](#int) | Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it. Pooled lag transforms (global\_/groupby/partition\_by) with a window wider than this keep that wider window instead, since their shared aggregates have no per-series buffer to trim below it. Defaults to None. | None | | `max_horizon` | [int](#int) | Train this many models, where each model will predict a specific horizon. Defaults to None. | None | | `horizons` | list of int | Train models only for specific horizons (1-indexed). Mutually exclusive with max\_horizon. Defaults to None. | None | | `horizon_features` | dict of int to list of str | Explicit mapping of 1-indexed horizons to dynamic exogenous columns. Only supported when using `max_horizon` or `horizons`. Defaults to None. | None | | `horizon_feature_templates` | list of str | Template patterns for horizon-specific dynamic exogenous features. Each template must include exactly one '' placeholder (1-indexed), for example: \['feature\_h']. Acts as shorthand for `horizon_features` and is only supported when using `max_horizon` or `horizons`. Defaults to None. | None | | `return_X_y` | [bool](#bool) | Return a tuple with the features and the target. If False will return a single dataframe. Defaults to False. | False | | `as_numpy` | [bool](#bool) | Cast features to numpy array. Only works for `return_X_y=True`. Defaults to True. | False | | `weight_col` | [str](#str) | Column that contains the sample weights. Defaults to None. | None | | `validate_data` | [bool](#bool) | Run data quality validations before preprocessing. Warns about missing dates and raises on duplicate rows. Defaults to True. | True | **Returns:** | Type | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | | [Union](#typing.Union)\[[DFType](#utilsforecast.compat.DFType), [Tuple](#typing.Tuple)\[[DFType](#utilsforecast.compat.DFType), [ndarray](#numpy.ndarray)]] | DataFrame or tuple of pandas Dataframe and a numpy array: `df` plus added features and target(s). | #### `MLForecast.fit_models` ```python theme={null} fit_models(X=None, y=None, models_fit_kwargs=None, generator_factory=None) ``` Manually train models. Use this if you called `MLForecast.preprocess` beforehand. **Parameters:** | Name | Type | Description | Default | | ------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------- | ----------------- | | `X` | pandas or polars DataFrame or numpy array | Features (for recursive forecasting). | None | | `y` | numpy array | Target (for recursive forecasting). | None | | `models_fit_kwargs` | [dict](#dict) | Keyword arguments for each model's fit method. | None | | `generator_factory` | [callable](#callable) | Factory function that returns an iterator yielding (h, X\_h, y\_h) tuples per horizon. | None | **Returns:** | Name | Type | Description | | ------------ | ---------------------------------------------------------- | ------------------------------------ | | `MLForecast` | [MLForecast](#mlforecast.forecast.MLForecast) | Forecast object with trained models. | #### `MLForecast.cross_validation` ```python theme={null} cross_validation(df, n_windows, h, id_col='unique_id', time_col='ds', target_col='y', step_size=None, static_features=None, dropna=True, keep_last_n=None, refit=True, max_horizon=None, horizons=None, horizon_features=None, horizon_feature_templates=None, before_predict_callback=None, after_predict_callback=None, prediction_intervals=None, level=None, input_size=None, fitted=False, as_numpy=False, weight_col=None, validate_data=True) ``` Perform time series cross validation. Creates `n_windows` splits where each window has `h` test periods, trains the models, computes the predictions and merges the actuals. **Parameters:** | Name | Type | Description | Default | | --------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | Series data in long format. | *required* | | `n_windows` | [int](#int) | Number of windows to evaluate. | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `step_size` | [int](#int) | Step size between each cross validation window. If None it will be equal to `h`. Defaults to None. | None | | `static_features` | list of str | Names of the features that are static and will be repeated when forecasting. Defaults to None. | None | | `dropna` | [bool](#bool) | Drop rows with missing values produced by the transformations. Defaults to True. | True | | `keep_last_n` | [int](#int) | Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it. Pooled lag transforms (global\_/groupby/partition\_by) with a window wider than this keep that wider window instead, since their shared aggregates have no per-series buffer to trim below it. Defaults to None. | None | | `max_horizon` | [int](#int) | Train this many models, where each model will predict a specific horizon. Defaults to None. | None | | `horizons` | list of int | Train models only for specific horizons (1-indexed). Mutually exclusive with max\_horizon. Defaults to None. | None | | `horizon_features` | dict of int to list of str | Explicit mapping of 1-indexed horizons to dynamic exogenous columns. Only supported when using `max_horizon` or `horizons`. Defaults to None. | None | | `horizon_feature_templates` | list of str | Template patterns for horizon-specific dynamic exogenous features. Each template must include exactly one '' placeholder (1-indexed), for example: \['feature\_h']. Acts as shorthand for `horizon_features` and is only supported when using `max_horizon` or `horizons`. Defaults to None. | None | | `refit` | [bool](#bool) or [int](#int) | Retrain model for each cross validation window. If False, the models are trained at the beginning and then used to predict each window. If positive int, the models are retrained every `refit` windows. Defaults to True. | True | | `before_predict_callback` | [callable](#callable) | Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index. Defaults to None. | None | | `after_predict_callback` | [callable](#callable) | Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index. Defaults to None. | None | | `prediction_intervals` | [PredictionIntervals](#mlforecast.conformal_prediction.PredictionIntervals) | Configuration to calibrate prediction intervals (Conformal Prediction). Defaults to None. | None | | `level` | list of ints or floats | Confidence levels between 0 and 100 for prediction intervals. Defaults to None. | None | | `input_size` | [int](#int) | Maximum training samples per serie in each window. If None, will use an expanding window. Defaults to None. | None | | `fitted` | [bool](#bool) | Store the in-sample predictions. Defaults to False. | False | | `as_numpy` | [bool](#bool) | Cast features to numpy array. Defaults to True. | False | | `weight_col` | [str](#str) | Column that contains the sample weights. Defaults to None. | None | | `validate_data` | [bool](#bool) | Run data quality validations on the full dataset before cross-validation. Warns about missing dates and raises on duplicate rows. Defaults to True. | True | **Returns:** | Type | Description | | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | [DFType](#utilsforecast.compat.DFType) | pandas or polars DataFrame: Predictions for each window with the series id, timestamp, last train date, target value and predictions from each model. | #### `MLForecast.from_cv` ```python theme={null} from_cv(cv) ``` # Grouped Array Source: https://nixtlaverse.nixtla.io/mlforecast/grouped_array Something abou `Grouped Array` ## ### `GroupedArray` ```python theme={null} GroupedArray(data, indptr) ``` Array made up of different groups. Can be thought of (and iterated) as a list of arrays. All the data is stored in a single 1d array `data`. The indices for the group boundaries are stored in another 1d array `indptr`. #### `GroupedArray.append` ```python theme={null} append(new_data) ``` Appends each element of `new_data` to each existing group. Returns a copy. #### `GroupedArray.append_several` ```python theme={null} append_several(new_sizes, new_values, new_groups) ``` #### `GroupedArray.apply_multithreaded_transforms` ```python theme={null} apply_multithreaded_transforms(transforms, num_threads, updates_only=False) ``` Apply the transformations using multithreading. If `updates_only` then only the updates are returned. #### `GroupedArray.apply_transforms` ```python theme={null} apply_transforms(transforms, updates_only=False) ``` Apply the transformations using the main process. If `updates_only` then only the updates are returned. #### `GroupedArray.data` ```python theme={null} data = data ``` #### `GroupedArray.expand_target` ```python theme={null} expand_target(max_horizon) ``` #### `GroupedArray.indptr` ```python theme={null} indptr = indptr ``` #### `GroupedArray.n_groups` ```python theme={null} n_groups = len(indptr) - 1 ``` #### `GroupedArray.take` ```python theme={null} take(idxs) ``` #### `GroupedArray.take_from_groups` ```python theme={null} take_from_groups(idx) ``` Takes `idx` from each group in the array. ```python theme={null} import copy from fastcore.test import test_eq, test_fail ``` ```python theme={null} # The `GroupedArray` is used internally for storing the series values and performing transformations. data = np.arange(10, dtype=np.float32) indptr = np.array([0, 2, 10]) # group 1: [0, 1], group 2: [2..9] ga = GroupedArray(data, indptr) test_eq(len(ga), 2) test_eq(str(ga), 'GroupedArray(ndata=10, n_groups=2)') ``` ```python theme={null} # Iterate through the groups ga_iter = iter(ga) np.testing.assert_equal(next(ga_iter), np.array([0, 1])) np.testing.assert_equal(next(ga_iter), np.arange(2, 10)) ``` ```python theme={null} # Take the last two observations from every group last_2 = ga.take_from_groups(slice(-2, None)) np.testing.assert_equal(last_2.data, np.array([0, 1, 8, 9])) np.testing.assert_equal(last_2.indptr, np.array([0, 2, 4])) ``` ```python theme={null} # Take the last four observations from every group. Note that since group 1 only has two elements, only these are returned. last_4 = ga.take_from_groups(slice(-4, None)) np.testing.assert_equal(last_4.data, np.array([0, 1, 6, 7, 8, 9])) np.testing.assert_equal(last_4.indptr, np.array([0, 2, 6])) ``` ```python theme={null} # Select a specific subset of groups indptr = np.array([0, 2, 4, 7, 10]) ga2 = GroupedArray(data, indptr) subset = ga2.take([0, 2]) np.testing.assert_allclose(subset[0].data, ga2[0].data) np.testing.assert_allclose(subset[1].data, ga2[2].data) ``` ```python theme={null} # The groups are [0, 1], [2, ..., 9]. expand_target(2) should take rolling pairs of them and fill with nans when there aren't enough np.testing.assert_equal( ga.expand_target(2), np.array([ [0, 1], [1, np.nan], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, np.nan] ]) ) ``` ```python theme={null} # append combined = ga.append(np.array([-1, -2])) np.testing.assert_equal( combined.data, np.hstack([ga.data[:2], np.array([-1]), ga.data[2:], np.array([-2])]), ) # try to append new values that don't match the number of groups test_fail(lambda: ga.append(np.array([1., 2., 3.])), contains='`new_data` must be of size 2') ``` ```python theme={null} # __setitem__ new_vals = np.array([10, 11]) ga[0] = new_vals np.testing.assert_equal(ga.data, np.append(new_vals, np.arange(2, 10))) ``` ```python theme={null} ga_copy = copy.copy(ga) ga_copy.data[0] = 900 assert ga.data[0] == 10 assert ga.indptr is ga_copy.indptr ``` # Machine Learning 🤖 Forecast Source: https://nixtlaverse.nixtla.io/mlforecast/index.html Scalable machine learning for time series forecasting **mlforecast** is a framework to perform time series forecasting using machine learning models, with the option to scale to massive amounts of data using remote clusters. ## Install ### PyPI `pip install mlforecast` ### conda-forge `conda install -c conda-forge mlforecast` For more detailed instructions you can refer to the [installation page](./docs/getting-started/install.html). ## Quick Start 1. **Get Started with this [quick guide](./docs/getting-started/quick_start_local.html).** 2. **Follow this [end-to-end walkthrough](./docs/getting-started/end_to_end_walkthrough.html) for best practices.** ### Videos * [Overview](https://www.youtube.com/live/EnhyJx8l2LE) ### Sample notebooks * [m5](https://www.kaggle.com/code/lemuz90/m5-mlforecast-eval) * [m5-polars](https://www.kaggle.com/code/lemuz90/m5-mlforecast-eval-polars) * [m4](https://www.kaggle.com/code/lemuz90/m4-competition) * [m4-cv](https://www.kaggle.com/code/lemuz90/m4-competition-cv) * [favorita](https://www.kaggle.com/code/lemuz90/mlforecast-favorita) * [VN1](https://colab.research.google.com/drive/1UdhCAk49k6HgMezG-U_1ETnAB5pYvZk9) ## Why? Current Python alternatives for machine learning models are slow, inaccurate and don’t scale well. So we created a library that can be used to forecast in production environments. [`MLForecast`](./forecast.html#mlforecast) includes efficient feature engineering to train any machine learning model (with `fit` and `predict` methods such as [`sklearn`](https://scikit-learn.org/stable/)) to fit millions of time series. ## Features * Fastest implementations of feature engineering for time series forecasting in Python. * Out-of-the-box compatibility with pandas, polars, spark, dask, and ray. * Probabilistic Forecasting with Conformal Prediction. * Support for exogenous variables and static covariates. * Familiar `sklearn` syntax: `.fit` and `.predict`. Missing something? Please open an issue or write us in [![Slack](https://img.shields.io/badge/Slack-4A154B?\&logo=slack\&logoColor=white.png)](https://join.slack.com/t/nixtlaworkspace/shared_invite/zt-135dssye9-fWTzMpv2WBthq8NK0Yvu6A) ## Examples and Guides 📚 [End to End Walkthrough](./docs/getting-started/end_to_end_walkthrough.html): model training, evaluation and selection for multiple time series. 🔎 [Probabilistic Forecasting](./docs/tutorials/prediction_intervals_in_forecasting_models.html): use Conformal Prediction to produce prediciton intervals. 👩‍🔬 [Cross Validation](./docs/how-to-guides/cross_validation.html): robust model’s performance evaluation. 🔁 [M5: Reuse CV Splits + Global/Grouped Rolling Means](./docs/how-to-guides/hyperparameter_optimization.html): optimize with cached CV windows while tuning global and grouped rolling features in one workflow. 🔌 [Predict Demand Peaks](./docs/tutorials/electricity_peak_forecasting.html): electricity load forecasting for detecting daily peaks and reducing electric bills. 📈 [Transfer Learning](./docs/how-to-guides/transfer_learning.html): pretrain a model using a set of time series and then predict another one using that pretrained model. 🌡️ [Distributed Training](./docs/getting-started/quick_start_distributed.html): use a Dask, Ray or Spark cluster to train models at scale. ## How to use The following provides a very basic overview, for a more detailed description see the [documentation](./). ### Data setup Store your time series in a pandas dataframe in long format, that is, each row represents an observation for a specific serie and timestamp. ```python theme={null} from mlforecast.utils import generate_daily_series series = generate_daily_series( n_series=20, max_length=100, n_static_features=1, static_as_categorical=False, with_trend=True ) series.head() ``` | | unique\_id | ds | y | static\_0 | | - | ---------- | ---------- | ---------- | --------- | | 0 | id\_00 | 2000-01-01 | 17.519167 | 72 | | 1 | id\_00 | 2000-01-02 | 87.799695 | 72 | | 2 | id\_00 | 2000-01-03 | 177.442975 | 72 | | 3 | id\_00 | 2000-01-04 | 232.704110 | 72 | | 4 | id\_00 | 2000-01-05 | 317.510474 | 72 | > Note: The unique\_id serves as an identifier for each distinct time > series in your dataset. If you are using only single time series from > your dataset, set this column to a constant value. ### Models Next define your models, each one will be trained on all series. These can be any regressor that follows the scikit-learn API. ```python theme={null} import lightgbm as lgb from sklearn.linear_model import LinearRegression ``` ```python theme={null} models = [ lgb.LGBMRegressor(random_state=0, verbosity=-1), LinearRegression(), ] ``` ### Forecast object Now instantiate an [`MLForecast`](./forecast.html#mlforecast) object with the models and the features that you want to use. The features can be lags, transformations on the lags and date features. You can also define transformations to apply to the target before fitting, which will be restored when predicting. ```python theme={null} from mlforecast import MLForecast from mlforecast.lag_transforms import ExpandingMean, RollingMean from mlforecast.target_transforms import Differences ``` ```python theme={null} fcst = MLForecast( models=models, freq='D', lags=[7, 14], lag_transforms={ 1: [ExpandingMean()], 7: [RollingMean(window_size=28)] }, date_features=['dayofweek'], target_transforms=[Differences([1])], ) ``` ### Training To compute the features and train the models call `fit` on your `Forecast` object. ```python theme={null} fcst.fit(series) ``` ``` MLForecast(models=[LGBMRegressor, LinearRegression], freq=D, lag_features=['lag7', 'lag14', 'expanding_mean_lag1', 'rolling_mean_lag7_window_size28'], date_features=['dayofweek'], num_threads=1) ``` ### Predicting To get the forecasts for the next `n` days call `predict(n)` on the forecast object. This will automatically handle the updates required by the features using a recursive strategy. ```python theme={null} predictions = fcst.predict(14) predictions ``` | | unique\_id | ds | LGBMRegressor | LinearRegression | | --- | ---------- | ---------- | ------------- | ---------------- | | 0 | id\_00 | 2000-04-04 | 299.923771 | 311.432371 | | 1 | id\_00 | 2000-04-05 | 365.424147 | 379.466214 | | 2 | id\_00 | 2000-04-06 | 432.562441 | 460.234028 | | 3 | id\_00 | 2000-04-07 | 495.628000 | 524.278924 | | 4 | id\_00 | 2000-04-08 | 60.786223 | 79.828767 | | ... | ... | ... | ... | ... | | 275 | id\_19 | 2000-03-23 | 36.266780 | 28.333215 | | 276 | id\_19 | 2000-03-24 | 44.370984 | 33.368228 | | 277 | id\_19 | 2000-03-25 | 50.746222 | 38.613001 | | 278 | id\_19 | 2000-03-26 | 58.906524 | 43.447398 | | 279 | id\_19 | 2000-03-27 | 63.073949 | 48.666783 |

280 rows × 4 columns

### Visualize results ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} fig = plot_series(series, predictions, max_ids=4, plot_random=False) ``` ![](https://raw.githubusercontent.com/Nixtla/mlforecast/main/nbs/figs/index.png) ## How to contribute See [CONTRIBUTING.md](https://github.com/Nixtla/mlforecast/blob/main/CONTRIBUTING.md). # Lag transforms Source: https://nixtlaverse.nixtla.io/mlforecast/lag_transforms.html Built-in lag transformations ## The `mlforecast.lag_transforms` module provides built-in **lag transformations**: statistics computed over lagged values of the target that are used as features by the forecasting model. You pass them to `MLForecast` through the `lag_transforms` argument, a dict whose keys are the lags to apply the transformation to and whose values are lists of transformation instances. ```python theme={null} from mlforecast import MLForecast from mlforecast.lag_transforms import ExpandingStd, RollingMean fcst = MLForecast( models=[...], freq='D', lag_transforms={ 1: [ExpandingStd()], 7: [RollingMean(window_size=7), RollingMean(window_size=28)], }, ) ``` The transforms fall into four families, each with several variants: * **Rolling** — `RollingMean`, `RollingStd`, `RollingMin`, `RollingMax`, `RollingQuantile`: fixed-window statistics over the lagged target. * **Seasonal rolling** — `SeasonalRollingMean`, `SeasonalRollingStd`, `SeasonalRollingMin`, `SeasonalRollingMax`, `SeasonalRollingQuantile`: rolling statistics computed across same-position observations in successive seasons (e.g. last 4 Mondays). * **Expanding** — `ExpandingMean`, `ExpandingStd`, `ExpandingMin`, `ExpandingMax`, `ExpandingQuantile`: statistics over all observations up to the lag. * **Exponentially weighted** — `ExponentiallyWeightedMean`: a weighted mean that emphasises recent observations. Two combinators let you build richer features from these primitives: **`Offset`** applies a transformation at a shifted lag, and **`Combine`** joins two transformations with a binary operator (for example a ratio of two rolling means at different windows). The basic usage is per-series — each transformation is computed independently for every series. The next section describes how to instead compute these statistics **across multiple series at once**. For a worked walkthrough of all of the above, including the `Combine` / `Offset` combinators and how to plug in custom numba-based transforms, see the [Lag transformations](docs/how-to-guides/lag_transforms_guide.html) how-to guide. ## Pooled mode: `global_`, `groupby`, and `partition_by` Every built-in rolling, expanding, seasonal-rolling, and exponentially weighted transform accepts three pooling parameters that let you compute the statistic across **multiple series at once**: * **`global_: bool`** — when `True`, the statistic is computed across **all series** aggregated by timestamp. Every series receives the same feature value at each timestamp. * **`groupby: Sequence[str]`** — column names to group by before computing the statistic. Columns must be declared as static features when calling `fit` / `preprocess`. Series in the same group share the feature value at each timestamp; series in different groups get different values. * **`partition_by: Sequence[str]`** — column names to partition further along a **dynamic** (time-varying) key, such as `promo` or `regime`. Each unique combination of partition values gets its own bucket. Composes with `global_` (cross-series aggregates within each partition), with `groupby` (group aggregates within each partition), or stands alone (per-(id, partition) buckets — *local* mode). Partition columns must be supplied via `X_df` at prediction. `global_` and `groupby` are **mutually exclusive** on the same transform. `partition_by` composes with either one or stands alone. All pooled modes require every series to **end at the same timestamp**, including local `partition_by`. **RANGE semantics.** Pooled transforms use SQL-style `RANGE BETWEEN ... PRECEDING` windows over actual timestamps, not row positions. Series with staggered starts simply do not contribute to the window until they have observations — no synthetic zeros are injected. Pooled mode assumes a **continuous, gap-free time grid** within each series; combining `validate_data=False` with a pooled transform raises a `UserWarning`. For `partition_by`, ordinals come from the **parent calendar** (global or group scope for nonlocal modes, per-id for local mode), so a partition bucket with gaps still preserves RANGE window semantics across those gaps rather than collapsing to row-based behavior. **`min_samples` divergence.** In local (per-series) mode, `min_samples` is capped at `window_size` by `coreforecast`. In pooled mode, `min_samples` counts **total non-NaN observations across all series** in the bucket within the rolling window, with no capping. This makes it useful as a coverage threshold: `RollingMean(window_size=1, min_samples=2, groupby=["brand"])` produces a non-null value only at timestamps where at least two series in the brand contribute observations. See the [Pooled lag transforms](docs/how-to-guides/pooled_lag_transforms.html) how-to guide for end-to-end examples. ### `RollingQuantile` ```python theme={null} RollingQuantile(p, window_size, min_samples=None, global_=False, groupby=None, partition_by=None, time_agg=None, **kwargs) ``` Bases: [\_RollingBase](#mlforecast.lag_transforms._RollingBase) Rolling quantile.
Note In pooled modes (`global_`/`groupby`/`partition_by`) this transform has no aggregate-cache fast path: it falls back to a row-level pass whose cost grows with `unique timestamps x bucket rows` at fit, and aggregates are rebuilt at every recursive prediction step. Can be slow on large panels.
### `RollingMax` Bases: [\_RollingBase](#mlforecast.lag_transforms._RollingBase) Rolling statistic ### `RollingMin` Bases: [\_RollingBase](#mlforecast.lag_transforms._RollingBase) Rolling statistic ### `RollingStd` Bases: [\_RollingBase](#mlforecast.lag_transforms._RollingBase) Rolling statistic ### `RollingMean` Bases: [\_RollingBase](#mlforecast.lag_transforms._RollingBase) Rolling statistic ### `SeasonalRollingQuantile` ```python theme={null} SeasonalRollingQuantile(p, season_length, window_size, min_samples=None, global_=False, groupby=None, partition_by=None, time_agg=None, **kwargs) ``` Bases: [\_Seasonal\_RollingBase](#mlforecast.lag_transforms._Seasonal_RollingBase) Rolling statistic over seasonal periods
Note In pooled modes (`global_`/`groupby`/`partition_by`) seasonal rolling transforms have no aggregate-cache fast path: they fall back to a row-level pass whose cost grows with `unique timestamps x bucket rows` at fit, and aggregates are rebuilt at every recursive prediction step. Can be slow on large panels.
### `SeasonalRollingMax` Bases: [\_Seasonal\_RollingBase](#mlforecast.lag_transforms._Seasonal_RollingBase) Rolling statistic over seasonal periods
Note In pooled modes (`global_`/`groupby`/`partition_by`) seasonal rolling transforms have no aggregate-cache fast path: they fall back to a row-level pass whose cost grows with `unique timestamps x bucket rows` at fit, and aggregates are rebuilt at every recursive prediction step. Can be slow on large panels.
### `SeasonalRollingMin` Bases: [\_Seasonal\_RollingBase](#mlforecast.lag_transforms._Seasonal_RollingBase) Rolling statistic over seasonal periods
Note In pooled modes (`global_`/`groupby`/`partition_by`) seasonal rolling transforms have no aggregate-cache fast path: they fall back to a row-level pass whose cost grows with `unique timestamps x bucket rows` at fit, and aggregates are rebuilt at every recursive prediction step. Can be slow on large panels.
### `SeasonalRollingStd` Bases: [\_Seasonal\_RollingBase](#mlforecast.lag_transforms._Seasonal_RollingBase) Rolling statistic over seasonal periods
Note In pooled modes (`global_`/`groupby`/`partition_by`) seasonal rolling transforms have no aggregate-cache fast path: they fall back to a row-level pass whose cost grows with `unique timestamps x bucket rows` at fit, and aggregates are rebuilt at every recursive prediction step. Can be slow on large panels.
### `SeasonalRollingMean` Bases: [\_Seasonal\_RollingBase](#mlforecast.lag_transforms._Seasonal_RollingBase) Rolling statistic over seasonal periods
Note In pooled modes (`global_`/`groupby`/`partition_by`) seasonal rolling transforms have no aggregate-cache fast path: they fall back to a row-level pass whose cost grows with `unique timestamps x bucket rows` at fit, and aggregates are rebuilt at every recursive prediction step. Can be slow on large panels.
### `ExpandingQuantile` ```python theme={null} ExpandingQuantile(p, global_=False, groupby=None, partition_by=None, time_agg=None, **kwargs) ``` Bases: [\_ExpandingBase](#mlforecast.lag_transforms._ExpandingBase) Expanding quantile.
Note In pooled modes (`global_`/`groupby`/`partition_by`) this transform has no aggregate-cache fast path: it falls back to a row-level pass whose cost grows with `unique timestamps x bucket rows` at fit, and aggregates are rebuilt at every recursive prediction step. Can be slow on large panels.
### `ExpandingMax` Bases: [\_ExpandingBase](#mlforecast.lag_transforms._ExpandingBase) Expanding statistic **Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `global_` | bool | If True, compute the statistic across all series aggregated by timestamp. Requires all series to end at the same timestamp. Defaults to False. | *required* | | `groupby` | Sequence\[str] | Column names to group by before computing the statistic. Columns must be static features. Mutually exclusive with `global_`. Defaults to None. | *required* | | `partition_by` | Sequence\[str] | Column names to partition by. Each unique combination of partition values creates a separate bucket. Unlike `groupby`, partition columns may vary over time and must be supplied via `X_df` at prediction. Composes with `global_` (cross-series aggregates within each partition), `groupby` (group aggregates within each partition), or stands alone (per-(id, partition) buckets, *local* mode). See the Pooled lag transforms guide for details. Defaults to None. | *required* | | `time_agg` | str | Pre-aggregate all rows sharing a timestamp within each bucket into a single value before applying the transform. One of `"sum"`, `"count"`, `"mean"`, `"min"`, `"max"`. Requires `global_` or `groupby`. Defaults to None. | *required* | ### `ExpandingMin` Bases: [\_ExpandingBase](#mlforecast.lag_transforms._ExpandingBase) Expanding statistic **Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `global_` | bool | If True, compute the statistic across all series aggregated by timestamp. Requires all series to end at the same timestamp. Defaults to False. | *required* | | `groupby` | Sequence\[str] | Column names to group by before computing the statistic. Columns must be static features. Mutually exclusive with `global_`. Defaults to None. | *required* | | `partition_by` | Sequence\[str] | Column names to partition by. Each unique combination of partition values creates a separate bucket. Unlike `groupby`, partition columns may vary over time and must be supplied via `X_df` at prediction. Composes with `global_` (cross-series aggregates within each partition), `groupby` (group aggregates within each partition), or stands alone (per-(id, partition) buckets, *local* mode). See the Pooled lag transforms guide for details. Defaults to None. | *required* | | `time_agg` | str | Pre-aggregate all rows sharing a timestamp within each bucket into a single value before applying the transform. One of `"sum"`, `"count"`, `"mean"`, `"min"`, `"max"`. Requires `global_` or `groupby`. Defaults to None. | *required* | ### `ExpandingStd` Bases: [\_ExpandingBase](#mlforecast.lag_transforms._ExpandingBase) Expanding statistic **Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `global_` | bool | If True, compute the statistic across all series aggregated by timestamp. Requires all series to end at the same timestamp. Defaults to False. | *required* | | `groupby` | Sequence\[str] | Column names to group by before computing the statistic. Columns must be static features. Mutually exclusive with `global_`. Defaults to None. | *required* | | `partition_by` | Sequence\[str] | Column names to partition by. Each unique combination of partition values creates a separate bucket. Unlike `groupby`, partition columns may vary over time and must be supplied via `X_df` at prediction. Composes with `global_` (cross-series aggregates within each partition), `groupby` (group aggregates within each partition), or stands alone (per-(id, partition) buckets, *local* mode). See the Pooled lag transforms guide for details. Defaults to None. | *required* | | `time_agg` | str | Pre-aggregate all rows sharing a timestamp within each bucket into a single value before applying the transform. One of `"sum"`, `"count"`, `"mean"`, `"min"`, `"max"`. Requires `global_` or `groupby`. Defaults to None. | *required* | ### `ExpandingMean` Bases: [\_ExpandingBase](#mlforecast.lag_transforms._ExpandingBase) Expanding statistic **Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `global_` | bool | If True, compute the statistic across all series aggregated by timestamp. Requires all series to end at the same timestamp. Defaults to False. | *required* | | `groupby` | Sequence\[str] | Column names to group by before computing the statistic. Columns must be static features. Mutually exclusive with `global_`. Defaults to None. | *required* | | `partition_by` | Sequence\[str] | Column names to partition by. Each unique combination of partition values creates a separate bucket. Unlike `groupby`, partition columns may vary over time and must be supplied via `X_df` at prediction. Composes with `global_` (cross-series aggregates within each partition), `groupby` (group aggregates within each partition), or stands alone (per-(id, partition) buckets, *local* mode). See the Pooled lag transforms guide for details. Defaults to None. | *required* | | `time_agg` | str | Pre-aggregate all rows sharing a timestamp within each bucket into a single value before applying the transform. One of `"sum"`, `"count"`, `"mean"`, `"min"`, `"max"`. Requires `global_` or `groupby`. Defaults to None. | *required* | ### `ExponentiallyWeightedMean` ```python theme={null} ExponentiallyWeightedMean(alpha, global_=False, groupby=None, partition_by=None, time_agg='mean', **kwargs) ``` Bases: [\_BaseLagTransform](#mlforecast.lag_transforms._BaseLagTransform) Exponentially weighted average **Parameters:** | Name | Type | Description | Default | | -------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | `alpha` | [float](#float) | Smoothing factor. | *required* | | `global_` | [bool](#bool) | If True, compute the statistic across all series aggregated by timestamp. Requires all series to end at the same timestamp. Defaults to False. | False | | `groupby` | [Sequence](#typing.Sequence)\[[str](#str)] | Column names to group by before computing the statistic. Columns must be static features. Mutually exclusive with `global_`. Defaults to None. | None | | `partition_by` | [Sequence](#typing.Sequence)\[[str](#str)] | Column names to partition by. Each unique combination of partition values creates a separate bucket. Unlike `groupby`, partition columns may vary over time and must be supplied via `X_df` at prediction. Composes with `global_` (cross-series aggregates within each partition), `groupby` (group aggregates within each partition), or stands alone (per-(id, partition) buckets, *local* mode). See the Pooled lag transforms guide for details. Defaults to None. | None | | `time_agg` | [str](#str) | Pre-aggregate all rows sharing a timestamp within each bucket into a single value before applying the transform. One of `"sum"`, `"count"`, `"mean"`, `"min"`, `"max"`. Values other than `"mean"` require `global_` or `groupby`. Defaults to `"mean"`, which matches EWM's bucket-mean update rule: each timestamp contributes its bucket aggregate mean exactly once, regardless of how many rows aggregated there. `None` is not accepted. | 'mean' | ### `Offset` ```python theme={null} Offset(tfm, n) ``` Bases: [\_BaseLagTransform](#mlforecast.lag_transforms._BaseLagTransform) Shift series before computing transformation **Parameters:** | Name | Type | Description | Default | | ----- | ------------------------------------------ | ---------------------------------------------------------------------------- | ---------- | | `tfm` | [LagTransform](#LagTransform) | Transformation to be applied | *required* | | `n` | [int](#int) | Number of positions to shift (lag) series before applying the transformation | *required* | ### `Combine` ```python theme={null} Combine(tfm1, tfm2, operator) ``` Bases: [\_BaseLagTransform](#mlforecast.lag_transforms._BaseLagTransform) Combine two lag transformations using an operator **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------ | -------------------------------------------------------------------- | ---------- | | `tfm1` | [LagTransform](#LagTransform) | First transformation. | *required* | | `tfm2` | [LagTransform](#LagTransform) | Second transformation. | *required* | | `operator` | [callable](#callable) | Binary operator that defines how to combine the two transformations. | *required* | # LightGBMCV Source: https://nixtlaverse.nixtla.io/mlforecast/lgb_cv.html Time series cross validation with LightGBM. ## ### `LightGBMCV` ```python theme={null} LightGBMCV(freq, lags=None, lag_transforms=None, date_features=None, num_threads=1, target_transforms=None) ``` Create LightGBM CV object. **Parameters:** | Name | Type | Description | Default | | ------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `freq` | [str](#str) or [int](#int) | Pandas offset alias, e.g. 'D', 'W-THU' or integer denoting the frequency of the series. | *required* | | `lags` | list of int | Lags of the target to use as features. Defaults to None. | None | | `lag_transforms` | dict of int to list of functions | Mapping of target lags to their transformations. Defaults to None. | None | | `date_features` | list of str or callable | Features computed from the dates. Can be pandas date attributes or functions that will take the dates as input. Defaults to None. | None | | `num_threads` | [int](#int) | Number of threads to use when computing the features. Use -1 to use all available CPU cores. Defaults to 1. | 1 | | `target_transforms` | list of transformers | Transformations that will be applied to the target before computing the features and restored after the forecasting step. Defaults to None. | None | #### `LightGBMCV.fit` ```python theme={null} fit(df, n_windows, h, id_col='unique_id', time_col='ds', target_col='y', step_size=None, num_iterations=100, params=None, static_features=None, dropna=True, keep_last_n=None, eval_every=10, weights=None, metric='mape', verbose_eval=True, early_stopping_evals=2, early_stopping_pct=0.01, compute_cv_preds=False, before_predict_callback=None, after_predict_callback=None, input_size=None, weight_col=None) ``` Train boosters simultaneously and assess their performance on the complete forecasting window. **Parameters:** | Name | Type | Description | Default | | ------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas DataFrame | Series data in long format. | *required* | | `n_windows` | [int](#int) | Number of windows to evaluate. | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `step_size` | [int](#int) | Step size between each cross validation window. If None it will be equal to `h`. Defaults to None. | None | | `num_iterations` | [int](#int) | Maximum number of boosting iterations to run. Defaults to 100. | 100 | | `params` | [dict](#dict) | Parameters to be passed to the LightGBM Boosters. Defaults to None. | None | | `static_features` | list of str | Names of the features that are static and will be repeated when forecasting. Defaults to None. | None | | `dropna` | [bool](#bool) | Drop rows with missing values produced by the transformations. Defaults to True. | True | | `keep_last_n` | [int](#int) | Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it. Pooled lag transforms (global\_/groupby/partition\_by) with a window wider than this keep that wider window instead, since their shared aggregates have no per-series buffer to trim below it. Defaults to None. | None | | `eval_every` | [int](#int) | Number of boosting iterations to train before evaluating on the whole forecast window. Defaults to 10. | 10 | | `weights` | sequence of float | Weights to multiply the metric of each window. If None, all windows have the same weight. Defaults to None. | None | | `metric` | [str](#str) or [callable](#callable) | Metric used to assess the performance of the models and perform early stopping. Defaults to 'mape'. | 'mape' | | `verbose_eval` | [bool](#bool) | Print the metrics of each evaluation. | True | | `early_stopping_evals` | [int](#int) | Maximum number of evaluations to run without improvement. Defaults to 2. | 2 | | `early_stopping_pct` | [float](#float) | Minimum percentage improvement in metric value in `early_stopping_evals` evaluations. Defaults to 0.01. | 0.01 | | `compute_cv_preds` | [bool](#bool) | Compute predictions for each window after finding the best iteration. Defaults to False. | False | | `before_predict_callback` | [callable](#callable) | Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index. Defaults to None. | None | | `after_predict_callback` | [callable](#callable) | Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index. Defaults to None. | None | | `input_size` | [int](#int) | Maximum training samples per serie in each window. If None, will use an expanding window. Defaults to None. | None | **Returns:** | Type | Description | | -------------------------- | ----------------------------------------------- | | list of tuple | List of (boosting rounds, metric value) tuples. | #### `LightGBMCV.predict` ```python theme={null} predict(h, before_predict_callback=None, after_predict_callback=None, X_df=None) ``` Compute predictions with each of the trained boosters. **Parameters:** | Name | Type | Description | Default | | ------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `before_predict_callback` | [callable](#callable) | Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index. Defaults to None. | None | | `after_predict_callback` | [callable](#callable) | Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index. Defaults to None. | None | | `X_df` | [DataFrame](#pandas.DataFrame) | Dataframe with the future exogenous features. Should have the id column and the time column. Defaults to None. | None | **Returns:** | Type | Description | | ------------------------------------------- | -------------------------------------------------------------------- | | [DataFrame](#pandas.DataFrame) | Predictions for each serie and timestep, with one column per window. | #### `LightGBMCV.setup` ```python theme={null} setup(df, n_windows, h, id_col='unique_id', time_col='ds', target_col='y', step_size=None, params=None, static_features=None, dropna=True, keep_last_n=None, weights=None, metric='mape', input_size=None, weight_col=None) ``` Initialize internal data structures to iteratively train the boosters. Use this before calling partial\_fit. **Parameters:** | Name | Type | Description | Default | | ----------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas DataFrame | Series data in long format. | *required* | | `n_windows` | [int](#int) | Number of windows to evaluate. | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `step_size` | [int](#int) | Step size between each cross validation window. If None it will be equal to `h`. Defaults to None. | None | | `params` | [dict](#dict) | Parameters to be passed to the LightGBM Boosters. Defaults to None. | None | | `static_features` | list of str | Names of the features that are static and will be repeated when forecasting. Defaults to None. | None | | `dropna` | [bool](#bool) | Drop rows with missing values produced by the transformations. Defaults to True. | True | | `keep_last_n` | [int](#int) | Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it. Pooled lag transforms (global\_/groupby/partition\_by) with a window wider than this keep that wider window instead, since their shared aggregates have no per-series buffer to trim below it. Defaults to None. | None | | `weights` | sequence of float | Weights to multiply the metric of each window. If None, all windows have the same weight. Defaults to None. | None | | `metric` | [str](#str) or [callable](#callable) | Metric used to assess the performance of the models and perform early stopping. Defaults to 'mape'. | 'mape' | | `input_size` | [int](#int) | Maximum training samples per serie in each window. If None, will use an expanding window. Defaults to None. | None | | `weight_col` | [str](#str) | Column containing sample weights. Higher weights increase the influence of those samples during fitting and evaluation. | None | **Returns:** | Type | Description | | -------------------------------------------------------- | --------------------------------------------------------- | | [LightGBMCV](#mlforecast.lgb_cv.LightGBMCV) | CV object with internal data structures for partial\_fit. | #### `LightGBMCV.partial_fit` ```python theme={null} partial_fit(num_iterations, before_predict_callback=None, after_predict_callback=None, weight_col=None) ``` Train the boosters for some iterations. **Parameters:** | Name | Type | Description | Default | | ------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `num_iterations` | [int](#int) | Number of boosting iterations to run | *required* | | `before_predict_callback` | [callable](#callable) | Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index. Defaults to None. | None | | `after_predict_callback` | [callable](#callable) | Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index. Defaults to None. | None | **Returns:** | Type | Description | | ---------------------------- | --------------------------------------------------- | | [float](#float) | Weighted metric after training for num\_iterations. | ### Example This shows an example with just 4 series of the M4 dataset. If you want to run it yourself on all of them, you can refer to [this notebook](https://www.kaggle.com/code/lemuz90/m4-competition-cv). ```python theme={null} import random from datasetsforecast.m4 import M4, M4Info from fastcore.test import test_eq, test_fail from mlforecast.target_transforms import Differences from nbdev import show_doc from mlforecast.lag_transforms import SeasonalRollingMean ``` ```python theme={null} group = 'Hourly' await M4.async_download('data', group=group) df, *_ = M4.load(directory='data', group=group) df['ds'] = df['ds'].astype('int') ids = df['unique_id'].unique() random.seed(0) sample_ids = random.choices(ids, k=4) sample_df = df[df['unique_id'].isin(sample_ids)] sample_df ``` | | unique\_id | ds | y | | ------ | ---------- | ---- | ---- | | 86796 | H196 | 1 | 11.8 | | 86797 | H196 | 2 | 11.4 | | 86798 | H196 | 3 | 11.1 | | 86799 | H196 | 4 | 10.8 | | 86800 | H196 | 5 | 10.6 | | ... | ... | ... | ... | | 325235 | H413 | 1004 | 99.0 | | 325236 | H413 | 1005 | 88.0 | | 325237 | H413 | 1006 | 47.0 | | 325238 | H413 | 1007 | 41.0 | | 325239 | H413 | 1008 | 34.0 | ```python theme={null} info = M4Info[group] horizon = info.horizon valid = sample_df.groupby('unique_id').tail(horizon) train = sample_df.drop(valid.index) train.shape, valid.shape ``` ```text theme={null} ((3840, 3), (192, 3)) ``` What LightGBMCV does is emulate [LightGBM’s cv function](https://lightgbm.readthedocs.io/en/v3.3.2/pythonapi/lightgbm.cv.html#lightgbm.cv) where several Boosters are trained simultaneously on different partitions of the data, that is, one boosting iteration is performed on all of them at a time. This allows to have an estimate of the error by iteration, so if we combine this with early stopping we can find the best iteration to train a final model using all the data or even use these individual models’ predictions to compute an ensemble. In order to have a good estimate of the forecasting performance of our model we compute predictions for the whole test period and compute a metric on that. Since this step can slow down training, there’s an `eval_every` parameter that can be used to control this, that is, if `eval_every=10` (the default) every 10 boosting iterations we’re going to compute forecasts for the complete window and report the error. We also have early stopping parameters: * `early_stopping_evals`: how many evaluations of the full window should we go without improving to stop training? * `early_stopping_pct`: what’s the minimum percentage improvement we want in these `early_stopping_evals` in order to keep training? This makes the LightGBMCV class a good tool to quickly test different configurations of the model. Consider the following example, where we’re going to try to find out which features can improve the performance of our model. We start just using lags. ```python theme={null} static_fit_config = dict( n_windows=2, h=horizon, params={'verbose': -1}, compute_cv_preds=True, ) cv = LightGBMCV( freq=1, lags=[24 * (i+1) for i in range(7)], # one week of lags ) ``` ```python theme={null} hist = cv.fit(train, **static_fit_config) ``` ```text theme={null} [LightGBM] [Info] Start training from score 51.745632 [10] mape: 0.590690 [20] mape: 0.251093 [30] mape: 0.143643 [40] mape: 0.109723 [50] mape: 0.102099 [60] mape: 0.099448 [70] mape: 0.098349 [80] mape: 0.098006 [90] mape: 0.098718 Early stopping at round 90 Using best iteration: 80 ``` By setting `compute_cv_preds` we get the predictions from each model on their corresponding validation fold. ```python theme={null} cv.cv_preds_ ``` | | unique\_id | ds | y | Booster | window | | --- | ---------- | --- | ---- | --------- | ------ | | 0 | H196 | 865 | 15.5 | 15.522924 | 0 | | 1 | H196 | 866 | 15.1 | 14.985832 | 0 | | 2 | H196 | 867 | 14.8 | 14.667901 | 0 | | 3 | H196 | 868 | 14.4 | 14.514592 | 0 | | 4 | H196 | 869 | 14.2 | 14.035793 | 0 | | ... | ... | ... | ... | ... | ... | | 187 | H413 | 956 | 59.0 | 77.227905 | 1 | | 188 | H413 | 957 | 58.0 | 80.589641 | 1 | | 189 | H413 | 958 | 53.0 | 53.986834 | 1 | | 190 | H413 | 959 | 38.0 | 36.749786 | 1 | | 191 | H413 | 960 | 46.0 | 36.281225 | 1 | The individual models we trained are saved, so calling `predict` returns the predictions from every model trained. ```python theme={null} preds = cv.predict(horizon) preds ``` | | unique\_id | ds | Booster0 | Booster1 | | --- | ---------- | ---- | --------- | --------- | | 0 | H196 | 961 | 15.670252 | 15.848888 | | 1 | H196 | 962 | 15.522924 | 15.697399 | | 2 | H196 | 963 | 14.985832 | 15.166213 | | 3 | H196 | 964 | 14.985832 | 14.723238 | | 4 | H196 | 965 | 14.562152 | 14.451092 | | ... | ... | ... | ... | ... | | 187 | H413 | 1004 | 70.695242 | 65.917620 | | 188 | H413 | 1005 | 66.216580 | 62.615788 | | 189 | H413 | 1006 | 63.896573 | 67.848598 | | 190 | H413 | 1007 | 46.922797 | 50.981950 | | 191 | H413 | 1008 | 45.006541 | 42.752819 | We can average these predictions and evaluate them. ```python theme={null} def evaluate_on_valid(preds): preds = preds.copy() preds['final_prediction'] = preds.drop(columns=['unique_id', 'ds']).mean(1) merged = preds.merge(valid, on=['unique_id', 'ds']) merged['abs_err'] = abs(merged['final_prediction'] - merged['y']) / merged['y'] return merged.groupby('unique_id')['abs_err'].mean().mean() ``` ```python theme={null} eval1 = evaluate_on_valid(preds) eval1 ``` ```text theme={null} 0.11036194712311806 ``` Now, since these series are hourly, maybe we can try to remove the daily seasonality by taking the 168th (24 \* 7) difference, that is, substract the value at the same hour from one week ago, thus our target will be $z_t = y_{t} - y_{t-168}$. The features will be computed from this target and when we predict they will be automatically re-applied. ```python theme={null} cv2 = LightGBMCV( freq=1, target_transforms=[Differences([24 * 7])], lags=[24 * (i+1) for i in range(7)], ) hist2 = cv2.fit(train, **static_fit_config) ``` ```text theme={null} [LightGBM] [Info] Start training from score 0.519010 [10] mape: 0.089024 [20] mape: 0.090683 [30] mape: 0.092316 Early stopping at round 30 Using best iteration: 10 ``` ```python theme={null} assert hist2[-1][1] < hist[-1][1] ``` Nice! We achieve a better score in less iterations. Let’s see if this improvement translates to the validation set as well. ```python theme={null} preds2 = cv2.predict(horizon) eval2 = evaluate_on_valid(preds2) eval2 ``` ```text theme={null} 0.08956665504570135 ``` ```python theme={null} assert eval2 < eval1 ``` Great! Maybe we can try some lag transforms now. We’ll try the seasonal rolling mean that averages the values “every season”, that is, if we set `season_length=24` and `window_size=7` then we’ll average the value at the same hour for every day of the week. ```python theme={null} cv3 = LightGBMCV( freq=1, target_transforms=[Differences([24 * 7])], lags=[24 * (i+1) for i in range(7)], lag_transforms={ 48: [SeasonalRollingMean(season_length=24, window_size=7)], }, ) hist3 = cv3.fit(train, **static_fit_config) ``` ```text theme={null} [LightGBM] [Info] Start training from score 0.273641 [10] mape: 0.086724 [20] mape: 0.088466 [30] mape: 0.090536 Early stopping at round 30 Using best iteration: 10 ``` Seems like this is helping as well! ```python theme={null} assert hist3[-1][1] < hist2[-1][1] ``` Does this reflect on the validation set? ```python theme={null} preds3 = cv3.predict(horizon) eval3 = evaluate_on_valid(preds3) eval3 ``` ```text theme={null} 0.08961279023129345 ``` Nice! mlforecast also supports date features, but in this case our time column is made from integers so there aren’t many possibilites here. As you can see this allows you to iterate faster and get better estimates of the forecasting performance you can expect from your model. If you’re doing hyperparameter tuning it’s useful to be able to run a couple of iterations, assess the performance, and determine if this particular configuration isn’t promising and should be discarded. For example, [optuna](https://optuna.org/) has [pruners](https://optuna.readthedocs.io/en/stable/reference/pruners.html) that you can call with your current score and it decides if the trial should be discarded. We’ll now show how to do that. Since the CV requires a bit of setup, like the LightGBM datasets and the internal features, we have this `setup` method. ```python theme={null} cv4 = LightGBMCV( freq=1, lags=[24 * (i+1) for i in range(7)], ) cv4.setup( train, n_windows=2, h=horizon, params={'verbose': -1}, ) ``` ```text theme={null} LightGBMCV(freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168'], date_features=[], num_threads=1, bst_threads=8) ``` Once we have this we can call `partial_fit` to only train for some iterations and return the score of the forecast window. ```python theme={null} score = cv4.partial_fit(10) score ``` ```text theme={null} [LightGBM] [Info] Start training from score 51.745632 ``` ```text theme={null} 0.5906900462828166 ``` This is equal to the first evaluation from our first example. ```python theme={null} assert hist[0][1] == score ``` We can now use this score to decide if this configuration is promising. If we want to we can train some more iterations. ```python theme={null} score2 = cv4.partial_fit(20) ``` This is now equal to our third metric from the first example, since this time we trained for 20 iterations. ```python theme={null} assert hist[2][1] == score2 ``` ### Using a custom metric The built-in metrics are MAPE and RMSE, which are computed by serie and then averaged across all series. If you want to do something different or use a different metric entirely, you can define your own metric like the following: ```python theme={null} def weighted_mape( y_true: pd.Series, y_pred: pd.Series, ids: pd.Series, dates: pd.Series, ): """Weighs the MAPE by the magnitude of the series values""" abs_pct_err = abs(y_true - y_pred) / abs(y_true) mape_by_serie = abs_pct_err.groupby(ids).mean() totals_per_serie = y_pred.groupby(ids).sum() series_weights = totals_per_serie / totals_per_serie.sum() return (mape_by_serie * series_weights).sum() ``` ```python theme={null} _ = LightGBMCV( freq=1, lags=[24 * (i+1) for i in range(7)], ).fit( train, n_windows=2, h=horizon, params={'verbose': -1}, metric=weighted_mape, ) ``` ```text theme={null} [LightGBM] [Info] Start training from score 51.745632 [10] weighted_mape: 0.480353 [20] weighted_mape: 0.218670 [30] weighted_mape: 0.161706 [40] weighted_mape: 0.149992 [50] weighted_mape: 0.149024 [60] weighted_mape: 0.148496 Early stopping at round 60 Using best iteration: 60 ``` # Optimization Source: https://nixtlaverse.nixtla.io/mlforecast/optimization.html Utilities for hyperparameter optimization ## ### `mlforecast_objective` ```python theme={null} mlforecast_objective(df, config_fn, loss, model, freq, n_windows, h, step_size=None, input_size=None, refit=False, id_col='unique_id', time_col='ds', target_col='y', weight_col=None, cv_splits=None) ``` optuna objective function for the MLForecast class **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | [DataFrame](#utilsforecast.compat.DataFrame) | Series data in long format. | *required* | | `config_fn` | [callable](#callable) | Function that takes an optuna trial and produces a configuration with the following keys: - model\_params - mlf\_init\_params - mlf\_fit\_params | *required* | | `loss` | [callable](#callable) | Function that takes the validation and train dataframes and produces a float. | *required* | | `model` | [BaseEstimator](#sklearn.base.BaseEstimator) | scikit-learn compatible model to be trained | *required* | | `freq` | [str](#str) or [int](#int) | pandas' or polars' offset alias or integer denoting the frequency of the series. | *required* | | `n_windows` | [int](#int) | Number of windows to evaluate. | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `step_size` | [int](#int) | Step size between each cross validation window. If None it will be equal to `h`. Defaults to None. | None | | `input_size` | [int](#int) | Maximum training samples per serie in each window. If None, will use an expanding window. Defaults to None. | None | | `refit` | [bool](#bool) or [int](#int) | Retrain model for each cross validation window. If False, the models are trained at the beginning and then used to predict each window. If positive int, the models are retrained every `refit` windows. Defaults to False. | False | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `weight_col` | [str](#str) | Column that contains sample weights. Defaults to None. | None | | `cv_splits` | [List](#typing.List)\[[Tuple](#typing.Tuple)\[[DataFrame](#utilsforecast.compat.DataFrame), [DataFrame](#utilsforecast.compat.DataFrame), [DataFrame](#utilsforecast.compat.DataFrame)]] \| None | Optional cached CV splits (cutoffs, train, valid) to reuse across trials. If None, backtest splits are generated on each trial. | None | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------- | ------------------------- | | [Callable](#typing.Callable)\[\[[Trial](#optuna.Trial)], [float](#float)] | optuna objective function | ```python theme={null} import lightgbm as lgb from datasetsforecast.m4 import M4, M4Evaluation, M4Info from utilsforecast.losses import smape from mlforecast.lag_transforms import ExpandingMean, RollingMean from mlforecast.target_transforms import Differences, LocalBoxCox, LocalStandardScaler ``` ```python theme={null} def train_valid_split(group): df, *_ = M4.load(directory='data', group=group) df['ds'] = df['ds'].astype('int') horizon = M4Info[group].horizon valid = df.groupby('unique_id').tail(horizon) train = df.drop(valid.index) return train, valid ``` ```python theme={null} h = M4Info['Weekly'].horizon weekly_train, weekly_valid = train_valid_split('Weekly') weekly_train['unique_id'] = weekly_train['unique_id'].astype('category') weekly_valid['unique_id'] = weekly_valid['unique_id'].astype(weekly_train['unique_id'].dtype) ``` ```python theme={null} def config_fn(trial): candidate_lags = [ [1], [13], [1, 13], range(1, 33), ] lag_idx = trial.suggest_categorical('lag_idx', range(len(candidate_lags))) candidate_lag_tfms = [ { 1: [RollingMean(window_size=13)] }, { 1: [RollingMean(window_size=13)], 13: [RollingMean(window_size=13)], }, { 13: [RollingMean(window_size=13)], }, { 4: [ExpandingMean(), RollingMean(window_size=4)], 8: [ExpandingMean(), RollingMean(window_size=4)], } ] lag_tfms_idx = trial.suggest_categorical('lag_tfms_idx', range(len(candidate_lag_tfms))) candidate_targ_tfms = [ [Differences([1])], [LocalBoxCox()], [LocalStandardScaler()], [LocalBoxCox(), Differences([1])], [LocalBoxCox(), LocalStandardScaler()], [LocalBoxCox(), Differences([1]), LocalStandardScaler()], ] targ_tfms_idx = trial.suggest_categorical('targ_tfms_idx', range(len(candidate_targ_tfms))) return { 'model_params': { 'learning_rate': 0.05, 'objective': 'l1', 'bagging_freq': 1, 'num_threads': 2, 'verbose': -1, 'force_col_wise': True, 'n_estimators': trial.suggest_int('n_estimators', 10, 1000, log=True), 'num_leaves': trial.suggest_int('num_leaves', 31, 1024, log=True), 'lambda_l1': trial.suggest_float('lambda_l1', 0.01, 10, log=True), 'lambda_l2': trial.suggest_float('lambda_l2', 0.01, 10, log=True), 'bagging_fraction': trial.suggest_float('bagging_fraction', 0.75, 1.0), 'feature_fraction': trial.suggest_float('feature_fraction', 0.75, 1.0), }, 'mlf_init_params': { 'lags': candidate_lags[lag_idx], 'lag_transforms': candidate_lag_tfms[lag_tfms_idx], 'target_transforms': candidate_targ_tfms[targ_tfms_idx], }, 'mlf_fit_params': { 'static_features': ['unique_id'], } } def loss(df, train_df): return smape(df, models=['model'])['model'].mean() ``` ```python theme={null} optuna.logging.set_verbosity(optuna.logging.WARNING) objective = mlforecast_objective( df=weekly_train, config_fn=config_fn, loss=loss, model=lgb.LGBMRegressor(), freq=1, n_windows=2, h=h, ) study = optuna.create_study( direction='minimize', sampler=optuna.samplers.TPESampler(seed=0) ) study.optimize(objective, n_trials=2) best_cfg = study.best_trial.user_attrs['config'] final_model = MLForecast( models=[lgb.LGBMRegressor(**best_cfg['model_params'])], freq=1, **best_cfg['mlf_init_params'], ) final_model.fit(weekly_train, **best_cfg['mlf_fit_params']) preds = final_model.predict(h) M4Evaluation.evaluate('data', 'Weekly', preds['LGBMRegressor'].values.reshape(-1, 13)) ``` | | SMAPE | MASE | OWA | | ------ | -------- | -------- | -------- | | Weekly | 9.261538 | 2.614473 | 0.976158 | # Target transforms Source: https://nixtlaverse.nixtla.io/mlforecast/target_transforms.html ## ```python theme={null} import pandas as pd from fastcore.test import test_fail from sklearn.ensemble import HistGradientBoostingRegressor from sklearn.linear_model import LinearRegression from sklearn.preprocessing import PowerTransformer from utilsforecast.processing import counts_by_id from mlforecast import MLForecast from mlforecast.utils import generate_daily_series ``` ### `BaseTargetTransform` Bases: [ABC](#abc.ABC) Base class used for target transformations. ### `Differences` ```python theme={null} Differences(differences) ``` Bases: [\_BaseGroupedArrayTargetTransform](#mlforecast.target_transforms._BaseGroupedArrayTargetTransform) Subtracts previous values of the serie. Can be used to remove trend or seasonalities. ```python theme={null} series = generate_daily_series(10, min_length=50, max_length=100) diffs = Differences([1, 2, 5]) id_counts = counts_by_id(series, 'unique_id') indptr = np.append(0, id_counts['counts'].cumsum()) ga = GroupedArray(series['y'].values, indptr) # differences are applied correctly transformed = diffs.fit_transform(ga) assert diffs.fitted_ == [] expected = series.copy() for d in diffs.differences: expected['y'] -= expected.groupby('unique_id', observed=True)['y'].shift(d) np.testing.assert_allclose(transformed.data, expected['y'].values) # fitted differences are restored correctly diffs.store_fitted = True transformed = diffs.fit_transform(ga) keep_mask = ~np.isnan(transformed.data) restored = diffs.inverse_transform_fitted(transformed) np.testing.assert_allclose(ga.data[keep_mask], restored.data[keep_mask]) # test transform new_ga = GroupedArray(np.random.rand(10), np.arange(11)) prev_orig = [diffs.scalers_[i].tails_[::d].copy() for i, d in enumerate(diffs.differences)] expected = new_ga.data - np.add.reduce(prev_orig) updates = diffs.update(new_ga) np.testing.assert_allclose(expected, updates.data) np.testing.assert_allclose(diffs.scalers_[0].tails_, new_ga.data) np.testing.assert_allclose(diffs.scalers_[1].tails_[1::2], new_ga.data - prev_orig[0]) np.testing.assert_allclose(diffs.scalers_[2].tails_[4::5], new_ga.data - np.add.reduce(prev_orig[:2])) # variable sizes diff1 = Differences([1]) ga = GroupedArray(np.arange(10), np.array([0, 3, 10])) diff1.fit_transform(ga) new_ga = GroupedArray(np.arange(4), np.array([0, 1, 4])) updates = diff1.update(new_ga) np.testing.assert_allclose(updates.data, np.array([0 - 2, 1 - 9, 2 - 1, 3 - 2])) np.testing.assert_allclose(diff1.scalers_[0].tails_, np.array([0, 3])) # short series ga = GroupedArray(np.arange(20), np.array([0, 2, 20])) test_fail(lambda: diffs.fit_transform(ga), contains="[0]") # stack diffs = Differences([1, 2, 5]) ga = GroupedArray(series['y'].values, indptr) diffs.fit_transform(ga) stacked = Differences.stack([diffs, diffs]) for i in range(len(diffs.differences)): np.testing.assert_allclose( stacked.scalers_[i].tails_, np.tile(diffs.scalers_[i].tails_, 2) ) ``` ### `AutoDifferences` ```python theme={null} AutoDifferences(max_diffs) ``` Bases: [\_BaseGroupedArrayTargetTransform](#mlforecast.target_transforms._BaseGroupedArrayTargetTransform) Find and apply the optimal number of differences to each serie. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | --------------------------------------- | ---------- | | `max_diffs` | [int](#int) | Maximum number of differences to apply. | *required* | #### `AutoDifferences.inverse_transform_fitted` ```python theme={null} inverse_transform_fitted(ga) ``` Inverse transform fitted values. Reverses the differencing transformation by reconstructing the original values from the differenced fitted values. This is used when fitted=True to restore the fitted predictions to the original scale. **Parameters:** | Name | Type | Description | Default | | ---- | ------------------------------------------------------------------- | ------------------------------------------------------ | ---------- | | `ga` | [GroupedArray](#mlforecast.grouped_array.GroupedArray) | GroupedArray containing the differenced fitted values. | *required* | **Returns:** | Type | Description | | ------------------------------------------------------------------- | ------------------------------------------------------ | | [GroupedArray](#mlforecast.grouped_array.GroupedArray) | GroupedArray with fitted values in the original scale. | **Raises:** | Type | Description | | -------------------------------------- | ------------------------------------------------------- | | [ValueError](#ValueError) | If fitted differences are smaller than provided target. | ### `AutoSeasonalDifferences` ```python theme={null} AutoSeasonalDifferences(season_length, max_diffs, n_seasons=10) ``` Bases: [AutoDifferences](#mlforecast.target_transforms.AutoDifferences) Find and apply the optimal number of seasonal differences to each group. **Parameters:** | Name | Type | Description | Default | | --------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------- | | `season_length` | [int](#int) | Length of the seasonal period. | *required* | | `max_diffs` | [int](#int) | Maximum number of differences to apply. | *required* | | `n_seasons` | [int](#int) | Number of seasons to use to determine the number of differences. Defaults to 10. If `None` will use all samples, otherwise `season_length` \* `n_seasons samples` will be used for the test. Smaller values will be faster but could be less accurate. | 10 | #### `AutoSeasonalDifferences.inverse_transform_fitted` ```python theme={null} inverse_transform_fitted(ga) ``` Inverse transform fitted values. Reverses the differencing transformation by reconstructing the original values from the differenced fitted values. This is used when fitted=True to restore the fitted predictions to the original scale. **Parameters:** | Name | Type | Description | Default | | ---- | ------------------------------------------------------------------- | ------------------------------------------------------ | ---------- | | `ga` | [GroupedArray](#mlforecast.grouped_array.GroupedArray) | GroupedArray containing the differenced fitted values. | *required* | **Returns:** | Type | Description | | ------------------------------------------------------------------- | ------------------------------------------------------ | | [GroupedArray](#mlforecast.grouped_array.GroupedArray) | GroupedArray with fitted values in the original scale. | **Raises:** | Type | Description | | -------------------------------------- | ------------------------------------------------------- | | [ValueError](#ValueError) | If fitted differences are smaller than provided target. | ### `AutoSeasonalityAndDifferences` ```python theme={null} AutoSeasonalityAndDifferences(max_season_length, max_diffs, n_seasons=10) ``` Bases: [AutoDifferences](#mlforecast.target_transforms.AutoDifferences) Find the length of the seasonal period and apply the optimal number of differences to each group. **Parameters:** | Name | Type | Description | Default | | ------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | | `max_season_length` | [int](#int) | Maximum length of the seasonal period. | *required* | | `max_diffs` | [int](#int) | Maximum number of differences to apply. | *required* | | `n_seasons` | [int](#int) | Number of seasons to use to determine the number of differences. Defaults to 10. If `None` will use all samples, otherwise `max_season_length` \* `n_seasons samples` will be used for the test. Smaller values will be faster but could be less accurate. | 10 | **Raises:** | Type | Description | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [ValueError](#ValueError) | If any series has fewer than `max_diffs + 4` observations. This ensures that after differencing, there are at least 4 observations remaining for STL decomposition (minimum 2 periods × minimum period of 2). | #### `AutoSeasonalityAndDifferences.inverse_transform_fitted` ```python theme={null} inverse_transform_fitted(ga) ``` Inverse transform fitted values. Reverses the differencing transformation by reconstructing the original values from the differenced fitted values. This is used when fitted=True to restore the fitted predictions to the original scale. **Parameters:** | Name | Type | Description | Default | | ---- | ------------------------------------------------------------------- | ------------------------------------------------------ | ---------- | | `ga` | [GroupedArray](#mlforecast.grouped_array.GroupedArray) | GroupedArray containing the differenced fitted values. | *required* | **Returns:** | Type | Description | | ------------------------------------------------------------------- | ------------------------------------------------------ | | [GroupedArray](#mlforecast.grouped_array.GroupedArray) | GroupedArray with fitted values in the original scale. | **Raises:** | Type | Description | | -------------------------------------- | ------------------------------------------------------- | | [ValueError](#ValueError) | If fitted differences are smaller than provided target. | ```python theme={null} def test_scaler(sc, series): id_counts = counts_by_id(series, 'unique_id') indptr = np.append(0, id_counts['counts'].cumsum()) ga = GroupedArray(series['y'].values, indptr) transformed = sc.fit_transform(ga) np.testing.assert_allclose( sc.inverse_transform(transformed).data, ga.data, ) transformed2 = sc.update(ga) np.testing.assert_allclose(transformed.data, transformed2.data) idxs = [0, 7] subset = ga.take(idxs) transformed_subset = transformed.take(idxs) subsc = sc.take(idxs) np.testing.assert_allclose( subsc.inverse_transform(transformed_subset).data, subset.data, ) stacked = sc.stack([sc, sc]) stacked_stats = stacked.scaler_.stats_ np.testing.assert_allclose( stacked_stats, np.tile(sc.scaler_.stats_, (2, 1)), ) ``` ### `LocalStandardScaler` Bases: [\_BaseLocalScaler](#mlforecast.target_transforms._BaseLocalScaler) Standardizes each serie by subtracting its mean and dividing by its standard deviation. ### `LocalMinMaxScaler` Bases: [\_BaseLocalScaler](#mlforecast.target_transforms._BaseLocalScaler) Scales each serie to be in the \[0, 1] interval. ### `LocalRobustScaler` ```python theme={null} LocalRobustScaler(scale) ``` Bases: [\_BaseLocalScaler](#mlforecast.target_transforms._BaseLocalScaler) Scaler robust to outliers. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `scale` | [str](#str) | Statistic to use for scaling. Can be either 'iqr' (Inter Quartile Range) or 'mad' (Median Asbolute Deviation). Defaults to 'iqr'. | *required* | ### `LocalBoxCox` ```python theme={null} LocalBoxCox() ``` Bases: [\_BaseLocalScaler](#mlforecast.target_transforms._BaseLocalScaler) Finds the optimum lambda for each serie and applies the Box-Cox transformation ### `GlobalSklearnTransformer` ```python theme={null} GlobalSklearnTransformer(transformer) ``` Bases: [BaseTargetTransform](#mlforecast.target_transforms.BaseTargetTransform) Applies the same scikit-learn transformer to all series. ```python theme={null} # need this import in order for isinstance to work from mlforecast.target_transforms import Differences as ExportedDifferences ``` ```python theme={null} sk_boxcox = PowerTransformer(method='box-cox', standardize=False) boxcox_global = GlobalSklearnTransformer(sk_boxcox) single_difference = ExportedDifferences([1]) series = generate_daily_series(10) fcst = MLForecast( models=[LinearRegression(), HistGradientBoostingRegressor()], freq='D', lags=[1, 2], target_transforms=[boxcox_global, single_difference] ) prep = fcst.preprocess(series, dropna=False) expected = ( pd.Series( sk_boxcox.fit_transform(series[['y']])[:, 0], index=series['unique_id'] ).groupby('unique_id', observed=True) .diff() .dropna() .values ) np.testing.assert_allclose(prep['y'].values, expected) preds = fcst.fit(series).predict(5) ``` # Utils | MLForecast Source: https://nixtlaverse.nixtla.io/mlforecast/utils.html ```python theme={null} from fastcore.test import test_eq, test_fail from nbdev import show_doc ``` ### `generate_daily_series` ```python theme={null} generate_daily_series(n_series, min_length=50, max_length=500, n_static_features=0, equal_ends=False, static_as_categorical=True, with_trend=False, seed=0, engine='pandas') ``` Generate Synthetic Panel Series. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ---------------------------------- | ------------------------------------------------------------------ | --------------------- | | `n_series` | [int](#int) | Number of series for synthetic panel. | *required* | | `min_length` | int, default=50 | Minimum length of synthetic panel's series. | 50 | | `max_length` | int, default=500 | Maximum length of synthetic panel's series. | 500 | | `n_static_features` | int, default=0 | Number of static exogenous variables for synthetic panel's series. | 0 | | `equal_ends` | bool, default=False | Series should end in the same date stamp `ds`. | False | | `static_as_categorical` | bool, default=True | Static features should have a categorical data type. | True | | `with_trend` | bool, default=False | Series should have a (positive) trend. | False | | `seed` | int, default=0 | Random seed used for generating the data. | 0 | | `engine` | str, default='pandas' | Output Dataframe type. | 'pandas' | **Returns:** | Type | Description | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | [DataFrame](#utilsforecast.compat.DataFrame) | pandas or polars DataFrame: Synthetic panel with columns \[`unique_id`, `ds`, `y`] and exogenous features. | Generate 20 series with lengths between 100 and 1,000. ```python theme={null} n_series = 20 min_length = 100 max_length = 1000 series = generate_daily_series(n_series, min_length, max_length) series ``` | | unique\_id | ds | y | | ----- | ---------- | ---------- | -------- | | 0 | id\_00 | 2000-01-01 | 0.395863 | | 1 | id\_00 | 2000-01-02 | 1.264447 | | 2 | id\_00 | 2000-01-03 | 2.284022 | | 3 | id\_00 | 2000-01-04 | 3.462798 | | 4 | id\_00 | 2000-01-05 | 4.035518 | | ... | ... | ... | ... | | 12446 | id\_19 | 2002-03-11 | 0.309275 | | 12447 | id\_19 | 2002-03-12 | 1.189464 | | 12448 | id\_19 | 2002-03-13 | 2.325032 | | 12449 | id\_19 | 2002-03-14 | 3.333198 | | 12450 | id\_19 | 2002-03-15 | 4.306117 | We can also add static features to each serie (these can be things like product\_id or store\_id). Only the first static feature (`static_0`) is relevant to the target. ```python theme={null} n_static_features = 2 series_with_statics = generate_daily_series(n_series, min_length, max_length, n_static_features) series_with_statics ``` | | unique\_id | ds | y | static\_0 | static\_1 | | ----- | ---------- | ---------- | ---------- | --------- | --------- | | 0 | id\_00 | 2000-01-01 | 7.521388 | 18 | 10 | | 1 | id\_00 | 2000-01-02 | 24.024502 | 18 | 10 | | 2 | id\_00 | 2000-01-03 | 43.396423 | 18 | 10 | | 3 | id\_00 | 2000-01-04 | 65.793168 | 18 | 10 | | 4 | id\_00 | 2000-01-05 | 76.674843 | 18 | 10 | | ... | ... | ... | ... | ... | ... | | 12446 | id\_19 | 2002-03-11 | 27.834771 | 89 | 42 | | 12447 | id\_19 | 2002-03-12 | 107.051746 | 89 | 42 | | 12448 | id\_19 | 2002-03-13 | 209.252845 | 89 | 42 | | 12449 | id\_19 | 2002-03-14 | 299.987801 | 89 | 42 | | 12450 | id\_19 | 2002-03-15 | 387.550536 | 89 | 42 | ```python theme={null} for i in range(n_static_features): assert all(series_with_statics.groupby('unique_id')[f'static_{i}'].nunique() == 1) ``` If `equal_ends=False` (the default) then every serie has a different end date. ```python theme={null} assert series_with_statics.groupby('unique_id')['ds'].max().nunique() > 1 ``` We can have all of them end at the same date by specifying `equal_ends=True`. ```python theme={null} series_equal_ends = generate_daily_series(n_series, min_length, max_length, equal_ends=True) assert series_equal_ends.groupby('unique_id')['ds'].max().nunique() == 1 ``` *** ### `generate_prices_for_series` ```python theme={null} generate_prices_for_series(series, horizon=7, seed=0) ``` ```python theme={null} series_for_prices = generate_daily_series(20, n_static_features=2, equal_ends=True) series_for_prices.rename(columns={'static_1': 'product_id'}, inplace=True) prices_catalog = generate_prices_for_series(series_for_prices, horizon=7) prices_catalog ``` | | ds | unique\_id | price | | ---- | ---------- | ---------- | -------- | | 0 | 2000-10-05 | id\_00 | 0.548814 | | 1 | 2000-10-06 | id\_00 | 0.715189 | | 2 | 2000-10-07 | id\_00 | 0.602763 | | 3 | 2000-10-08 | id\_00 | 0.544883 | | 4 | 2000-10-09 | id\_00 | 0.423655 | | ... | ... | ... | ... | | 5009 | 2001-05-17 | id\_19 | 0.288027 | | 5010 | 2001-05-18 | id\_19 | 0.846305 | | 5011 | 2001-05-19 | id\_19 | 0.791284 | | 5012 | 2001-05-20 | id\_19 | 0.578636 | | 5013 | 2001-05-21 | id\_19 | 0.288589 | ```python theme={null} test_eq(set(prices_catalog['unique_id']), set(series_for_prices['unique_id'])) test_fail(lambda: generate_prices_for_series(series), contains='equal ends') ``` *** ### `PredictionIntervals` ```python theme={null} PredictionIntervals(n_windows=2, h=1, method='conformal_distribution', scale_estimator=None) ``` Class for storing prediction intervals metadata information. # Hyperparameter Optimization | NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/common.base_auto.html BaseAuto class for hyperparameter optimization in NeuralForecast. Integrates Optuna, HyperOpt, Dragonfly through Ray for automated model tuning with cross-validation. Machine Learning forecasting methods are defined by many hyperparameters that control their behavior, with effects ranging from their speed and memory requirements to their predictive performance. For a long time, manual hyperparameter tuning prevailed. This approach is time-consuming, **automated hyperparameter optimization** methods have been introduced, proving more efficient than manual tuning, grid search, and random search.

The `BaseAuto` class offers shared API connections to hyperparameter optimization algorithms like [Optuna](https://docs.ray.io/en/latest/tune/examples/bayesopt_example.html), [HyperOpt](https://docs.ray.io/en/latest/tune/examples/hyperopt_example.html), [Dragonfly](https://docs.ray.io/en/releases-2.7.0/tune/examples/dragonfly_example.html) among others through `ray`, which gives you access to grid search, bayesian optimization and other state-of-the-art tools like hyperband. Comprehending the impacts of hyperparameters is still a precious skill, as it can help guide the design of informed hyperparameter spaces that are faster to explore automatically. *Figure 1. Example of dataset split (left), validation (yellow) and test (orange). The hyperparameter optimization guiding signal is obtained from the validation set.* ## ### `BaseAuto` ```python theme={null} BaseAuto( cls_model, h, loss, valid_loss, config, search_alg=BasicVariantGenerator(random_state=1), num_samples=10, time_budget=None, refit_with_val=False, verbose=False, alias=None, backend="ray", callbacks=None, ray_options=None, optuna_options=None, cpus=None, gpus=None, ) ``` Bases: [LightningModule](#pytorch_lightning.LightningModule) Class for Automatic Hyperparameter Optimization, it builds on top of `ray` to give access to a wide variety of hyperparameter optimization tools ranging from classic grid search, to Bayesian optimization and HyperBand algorithm. The validation loss to be optimized is defined by the `config['loss']` dictionary value, the config also contains the rest of the hyperparameter search space. It is important to note that the success of this hyperparameter optimization heavily relies on a strong correlation between the validation and test periods. **Parameters:** | Name | Type | Description | Default | | ---------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | | `cls_model` | PyTorch/PyTorchLightning model | See `neuralforecast.models` [collection here](./models.html). | *required* | | `h` | [int](#int) | Forecast horizon | *required* | | `loss` | PyTorch module | Instantiated train loss class from [losses collection](./losses.pytorch.html). | *required* | | `valid_loss` | PyTorch module | Instantiated valid loss class from [losses collection](./losses.pytorch.html). | *required* | | `config` | [dict](#dict) or [callable](#callable) | Dictionary with ray.tune defined search space or function that takes an optuna trial and returns a configuration dict. The config must include every parameter of the underlying model that has no default value (e.g. `input_size`, and `n_series` for multivariate models), either as a fixed value or as a search variable. `h`, `loss`, and `valid_loss` are injected automatically and must not be set in `config`. | *required* | | `search_alg` | ray.tune.search variant or optuna.sampler | For ray see [https://docs.ray.io/en/latest/tune/api\_docs/suggestion.html](https://docs.ray.io/en/latest/tune/api_docs/suggestion.html) For optuna see [https://optuna.readthedocs.io/en/stable/reference/samplers/index.html](https://optuna.readthedocs.io/en/stable/reference/samplers/index.html). | [BasicVariantGenerator](#ray.tune.search.basic_variant.BasicVariantGenerator)(random\_state=1) | | `num_samples` | [int](#int) | Number of hyperparameter optimization steps/samples. | 10 | | `time_budget` | [int](#int) | Time budget in seconds for the hyperparameter search. | None | | `refit_with_val` | [bool](#bool) | Refit of best model should preserve val\_size. | False | | `verbose` | [bool](#bool) | Track progress. | False | | `alias` | [str](#str) | Custom name of the model. | None | | `backend` | [str](#str) | Backend to use for searching the hyperparameter space, can be either 'ray' or 'optuna'. | 'ray' | | `callbacks` | list of callable | List of functions to call during the optimization process. ray reference: [https://docs.ray.io/en/latest/tune/tutorials/tune-metrics.html](https://docs.ray.io/en/latest/tune/tutorials/tune-metrics.html) optuna reference: [https://optuna.readthedocs.io/en/stable](https://optuna.readthedocs.io/en/stable) | None | | `ray_options` | [RayOptions](#neuralforecast.common._base_auto.RayOptions) | Container for Ray-only options. See `RayOptions` for the supported fields (`run_config`, `scheduler`, `cpus`, `gpus`). Only used with `backend='ray'`. | None | | `optuna_options` | [OptunaOptions](#neuralforecast.common._base_auto.OptunaOptions) | Container for Optuna-only options. See `OptunaOptions` for the supported fields (`study_kwargs`, `create_study_kwargs`). Only used with `backend='optuna'`. | None | | `cpus` | | No longer supported as of v3.2.0. Pin neuralforecast to v3.1.9, or pass `ray_options=RayOptions(cpus=...)` instead. | None | | `gpus` | | No longer supported as of v3.2.0. Pin neuralforecast to v3.1.9, or pass `ray_options=RayOptions(gpus=...)` instead. | None | #### `BaseAuto.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` BaseAuto.fit Perform the hyperparameter optimization as specified by the BaseAuto configuration dictionary `config`. The optimization is performed on the `TimeSeriesDataset` using temporal cross validation with the validation set that sequentially precedes the test set. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------------------------------- | --------------------------------------------------------------------------- | ----------------- | | `dataset` | NeuralForecast's `TimeSeriesDataset` | NeuralForecast's `TimeSeriesDataset` see details [here](./tsdataset.html) | *required* | | `val_size` | [int](#int) | Size of temporal validation set (needs to be bigger than 0). | 0 | | `test_size` | [int](#int) | Size of temporal test set (default 0). | 0 | | `random_seed` | [int](#int) | Random seed for hyperparameter exploration algorithms, not yet implemented. | None | **Returns:** | Name | Type | Description | | ------ | ---- | -------------------------------------------------------------------- | | `self` | | Fitted instance of `BaseAuto` with best hyperparameters and results. | #### `BaseAuto.predict` ```python theme={null} predict(dataset, step_size=1, h=None, **data_kwargs) ``` BaseAuto.predict Predictions of the best performing model on validation. **Parameters:** | Name | Type | Description | Default | | -------------- | ------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------- | | `dataset` | NeuralForecast's `TimeSeriesDataset` | NeuralForecast's `TimeSeriesDataset` see details [here](./tsdataset.html) | *required* | | `step_size` | [int](#int) | Steps between sequential predictions, (default 1). | 1 | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `**data_kwarg` | | Additional parameters for the dataset module. | *required* | **Returns:** | Name | Type | Description | | ------- | ---- | ------------------------------------------------ | | `y_hat` | | Numpy predictions of the `NeuralForecast` model. | ### Usage Example ```python theme={null} class RayLogLossesCallback(tune.Callback): def on_trial_complete(self, iteration, trials, trial, **info): result = trial.last_result print(40 * '-' + 'Trial finished' + 40 * '-') print(f'Train loss: {result["train_loss"]:.2f}. Valid loss: {result["loss"]:.2f}') print(80 * '-') ``` ```python theme={null} config = { "hidden_size": tune.choice([512]), "num_layers": tune.choice([3, 4]), "input_size": 12, "max_steps": 10, "val_check_steps": 5 } auto = BaseAuto(h=12, loss=MAE(), valid_loss=MSE(), cls_model=MLP, config=config, num_samples=2, cpus=1, gpus=0, callbacks=[RayLogLossesCallback()]) auto.fit(dataset=dataset) y_hat = auto.predict(dataset=dataset) assert mae(Y_test_df['y'].values, y_hat[:, 0]) < 200 ``` ```python theme={null} def config_f(trial): return { "hidden_size": trial.suggest_categorical('hidden_size', [512]), "num_layers": trial.suggest_categorical('num_layers', [3, 4]), "input_size": 12, "max_steps": 10, "val_check_steps": 5 } class OptunaLogLossesCallback: def __call__(self, study, trial): metrics = trial.user_attrs['METRICS'] print(40 * '-' + 'Trial finished' + 40 * '-') print(f'Train loss: {metrics["train_loss"]:.2f}. Valid loss: {metrics["loss"]:.2f}') print(80 * '-') ``` ```python theme={null} auto2 = BaseAuto(h=12, loss=MAE(), valid_loss=MSE(), cls_model=MLP, config=config_f, search_alg=optuna.samplers.RandomSampler(), num_samples=2, backend='optuna', callbacks=[OptunaLogLossesCallback()]) auto2.fit(dataset=dataset) assert isinstance(auto2.results, optuna.Study) y_hat2 = auto2.predict(dataset=dataset) assert mae(Y_test_df['y'].values, y_hat2[:, 0]) < 200 ``` ### References * [James Bergstra, Remi Bardenet, Yoshua Bengio, and Balazs Kegl (2011). “Algorithms for Hyper-Parameter Optimization”. In: Advances in Neural Information Processing Systems. url: https://proceedings.neurips.cc/paper/2011/file/86e8f7ab32cfd12577bc2619bc635690-Paper.pdf](https://proceedings.neurips.cc/paper/2011/file/86e8f7ab32cfd12577bc2619bc635690-Paper.pdf) * [Kirthevasan Kandasamy, Karun Raju Vysyaraju, Willie Neiswanger, Biswajit Paria, Christopher R. Collins, Jeff Schneider, Barnabas Poczos, Eric P. Xing (2019). “Tuning Hyperparameters without Grad Students: Scalable and Robust Bayesian Optimisation with Dragonfly”. Journal of Machine Learning Research. url: https://arxiv.org/abs/1903.06694](https://arxiv.org/abs/1903.06694) * [Lisha Li, Kevin Jamieson, Giulia DeSalvo, Afshin Rostamizadeh, Ameet Talwalkar (2016). “Hyperband: A Novel Bandit-Based Approach to Hyperparameter Optimization”. Journal of Machine Learning Research. url: https://arxiv.org/abs/1603.06560](https://arxiv.org/abs/1603.06560) # NN Modules Source: https://nixtlaverse.nixtla.io/neuralforecast/common.modules.html Neural network building blocks for NeuralForecast: MLP layers, temporal convolutions, Transformer encoders-decoders, attention mechanisms, and embeddings. ## 1. MLP Multi-Layer Perceptron ### `MLP` ```python theme={null} MLP(in_features, out_features, activation, hidden_size, num_layers, dropout) ``` Bases: [Module](#torch.nn.Module) Multi-Layer Perceptron for time series forecasting. A feedforward neural network with configurable depth and width. The network consists of an input layer, multiple hidden layers with activation functions and dropout, and an output layer. All hidden layers have the same dimensionality. **Parameters:** | Name | Type | Description | Default | | -------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `in_features` | [int](#int) | Dimension of input features. | *required* | | `out_features` | [int](#int) | Dimension of output features. | *required* | | `activation` | [str](#str) | Activation function name. Must be one of the supported activations in ACTIVATIONS list (e.g., 'ReLU', 'Tanh', 'GELU', 'ELU'). Ignored when num\_layers=1. | *required* | | `hidden_size` | [int](#int) | Number of units in each hidden layer. All hidden layers share the same dimensionality. Ignored when num\_layers=1. | *required* | | `num_layers` | [int](#int) | Total number of layers including input and output layers. Use num\_layers=1 for a direct linear projection with no hidden layers or activation. For num\_layers>=2, creates: input layer, (num\_layers-2) hidden layers, and output layer. | *required* | | `dropout` | [float](#float) | Dropout probability applied after each hidden layer's activation. Should be in range \[0.0, 1.0]. Not applied to output layer. Ignored when num\_layers=1. | *required* | **Returns:** | Type | Description | | ------------------------------------ | --------------------------------------------------------- | | [Tensor](#torch.Tensor) | Transformed output tensor of shape \[..., out\_features]. |
Notes * The activation function is applied after each hidden layer's linear transformation, but not after the final output layer. * Dropout is applied after activation in hidden layers for regularization. * This MLP is used as a decoder component in various forecasting models including RNN, LSTM, GRU, DilatedRNN, TCN, xLSTM, and DeepAR.
## 2. Temporal Convolutions For long time in deep learning, sequence modelling was synonymous with recurrent networks, yet several papers have shown that simple convolutional architectures can outperform canonical recurrent networks like LSTMs by demonstrating longer effective memory. **References** -[van den Oord, A., Dieleman, S., Zen, H., Simonyan, K., Vinyals, O., Graves, A., Kalchbrenner, N., Senior, A. W., & Kavukcuoglu, K. (2016). Wavenet: A generative model for raw audio. Computing Research Repository, abs/1609.03499. URL: http://arxiv.org/abs/1609.03499. arXiv:1609.03499.](https://arxiv.org/abs/1609.03499) -[Shaojie Bai, Zico Kolter, Vladlen Koltun. (2018). An Empirical Evaluation of Generic Convolutional and Recurrent Networks for Sequence Modeling. Computing Research Repository, abs/1803.01271. URL: https://arxiv.org/abs/1803.01271.](https://arxiv.org/abs/1803.01271) ### `Chomp1d` ```python theme={null} Chomp1d(horizon) ``` Bases: [Module](#torch.nn.Module) Temporal trimming layer for 1D sequences. Removes the rightmost `horizon` timesteps from a 3D tensor. This is commonly\ used to trim padding added by convolution operations, ensuring the output\ sequence has the desired length. The operation trims the temporal dimension: \[N, C, T] -> \[N, C, T-horizon] **Parameters:** | Name | Type | Description | Default | | --------- | ------------------------ | --------------------------------------------------------------------- | ---------- | | `horizon` | [int](#int) | Number of timesteps to remove from the end of the temporal dimension. | *required* | **Returns:** | Type | Description | | ------------------------------------ | ------------------------------------------- | | [Tensor](#torch.Tensor) | Trimmed tensor of shape \[N, C, T-horizon]. |
Notes * Commonly used in `CausalConv1d` to remove padding after convolution.
### CausalConv1d ### `CausalConv1d` ```python theme={null} CausalConv1d( in_channels, out_channels, kernel_size, padding, dilation, activation, stride=1, ) ``` Bases: [Module](#torch.nn.Module) Causal Convolution 1d Receives `x` input of dim \[N,C\_in,T], and computes a causal convolution in the time dimension. Skipping the H steps of the forecast horizon, through its dilation. Consider a batch of one element, the dilated convolution operation on the $t$ time step is defined: ```math theme={null} \mathrm{Conv1D}(\mathbf{x},\mathbf{w})(t) = (\mathbf{x}_{[*d]} \mathbf{w})(t) = \sum^{K}_{k=1} w_{k} \mathbf{x}_{t-dk} ``` where $d$ is the dilation factor, $K$ is the kernel size, $t-dk$ is the index of the considered past observation. The dilation effectively applies a filter with skip connections. If $d=1$ one recovers a normal convolution. **Parameters:** | Name | Type | Description | Default | | -------------- | ------------------------ | ------------------------------------------------- | ---------- | | `in_channels` | [int](#int) | Dimension of `x` input's initial channels. | *required* | | `out_channels` | [int](#int) | Dimension of `x` outputs's channels. | *required* | | `activation` | [str](#str) | Identifying activations from PyTorch activations. | *required* | | `padding` | [int](#int) | Number of zero padding used to the left. | *required* | | `kernel_size` | [int](#int) | Convolution's kernel size. | *required* | | `dilation` | [int](#int) | Dilation skip connections. | *required* | **Returns:** | Type | Description | | ------------------------------------ | ---------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Torch tensor of dim \[N,C\_out,T] activation(conv1d(inputs, kernel) + bias). | ### TemporalConvolutionEncoder ## 3. Transformers **References** * [Haoyi Zhou, Shanghang Zhang, Jieqi Peng, Shuai Zhang, Jianxin Li, Hui Xiong, Wancai Zhang. “Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting”](https://arxiv.org/abs/2012.07436) * [Haixu Wu, Jiehui Xu, Jianmin Wang, Mingsheng Long.](https://arxiv.org/abs/2106.13008) ### `TransEncoder` ```python theme={null} TransEncoder(attn_layers, conv_layers=None, norm_layer=None) ``` Bases: [Module](#torch.nn.Module) Transformer Encoder. A stack of transformer encoder layers that processes input sequences through\ multiple self-attention and feed-forward layers. Optionally includes convolutional\ layers between attention layers for distillation and a final normalization layer. **Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `attn_layers` | list of TransEncoderLayer | List of transformer encoder layers to stack. | *required* | | `conv_layers` | list of nn.Module | List of convolutional layers applied between attention layers. Must have length len(attn\_layers) - 1 if provided. Used for distillation in models like Informer. | None | | `norm_layer` | [Module](#torch.nn.Module) | Normalization layer applied to the final output. Typically nn.LayerNorm. | None | **Returns:** | Type | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | [Tensor](#torch.Tensor) | Encoded output tensor of shape \[batch, seq\_len, hidden\_size] after passing through all encoder layers and optional normalization. | | list\[torch.Tensor]] | List of attention weights from each encoder layer, each of shape \[batch, n\_heads, seq\_len, seq\_len] (or None if not computed). |
Notes When conv\_layers is provided, the encoder alternates between attention layers and convolutional layers, with the final attention layer applied without a subsequent convolution. This architecture is used in the Informer model.
### `TransEncoderLayer` ```python theme={null} TransEncoderLayer( attention, hidden_size, conv_hidden_size=None, dropout=0.1, activation="relu", ) ``` Bases: [Module](#torch.nn.Module) Transformer Encoder Layer. A single layer of the transformer encoder that applies self-attention followed by\ a position-wise feed-forward network with residual connections and layer normalization.\ Dropout is applied after the self-attention output and twice in the feed-forward network\ (after each convolution) before the residual connections for regularization. **Parameters:** | Name | Type | Description | Default | | ------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------- | | `attention` | [AttentionLayer](#neuralforecast.common._modules.AttentionLayer) | Self-attention mechanism to apply. | *required* | | `hidden_size` | [int](#int) | Dimension of the model's hidden representations. | *required* | | `conv_hidden_size` | [int](#int) | Dimension of the feed-forward network's hidden layer. Defaults to 4 \* hidden\_size if not specified. | None | | `dropout` | [float](#float) | Dropout probability applied after attention and feed-forward layers. | 0.1 | | `activation` | [str](#str) | Activation function to use in the feed-forward network. Either "relu" or "gelu". | 'relu' | **Returns:** | Type | Description | | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Output tensor of shape \[batch, seq\_len, hidden\_size] after applying self-attention and feed-forward transformations. | | [Tensor](#torch.Tensor) or None | Attention weights of shape \[batch, n\_heads, seq\_len, seq\_len] if output\_attention is True in the attention layer, otherwise None. |
Notes The layer applies two main operations in sequence: 1. Self-attention on the input with dropout, residual connection, and normalization 2. Position-wise feed-forward network using 1D convolutions with dropout applied twice (after the first convolution with activation, and after the second convolution), residual connection, and normalization This layer is used as a building block in transformer-based models like Informer, VanillaTransformer, iTransformer, and SOFTS.
### `TransDecoder` ```python theme={null} TransDecoder(layers, norm_layer=None, projection=None) ``` Bases: [Module](#torch.nn.Module) Transformer decoder module for sequence-to-sequence forecasting. Stacks multiple TransDecoderLayer modules to process decoder inputs with\ self-attention and cross-attention mechanisms. Optionally applies layer\ normalization and a final projection layer to produce output predictions. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------- | | `layers` | [list](#list) | List of TransDecoderLayer instances to stack sequentially. | *required* | | `norm_layer` | [Module](#torch.nn.Module) | Layer normalization module applied after all decoder layers. | None | | `projection` | [Module](#torch.nn.Module) | Final projection layer (typically nn.Linear) to map hidden representations to output dimension. | None | **Returns:** | Type | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Decoded output tensor. If projection is provided, returns tensor of shape \[batch, target\_seq\_len, output\_dim]. Otherwise, returns tensor of shape \[batch, target\_seq\_len, hidden\_size]. |
Notes * The forward method requires both decoder input (x) and encoder output (cross). * Masks are optional and used for attention masking in self-attention (x\_mask) and cross-attention (cross\_mask). * Each layer performs self-attention on decoder input, cross-attention with encoder output, and feedforward transformation.
### `TransDecoderLayer` ```python theme={null} TransDecoderLayer( self_attention, cross_attention, hidden_size, conv_hidden_size=None, dropout=0.1, activation="relu", ) ``` Bases: [Module](#torch.nn.Module) Transformer Decoder Layer. A single layer of the transformer decoder that applies masked self-attention,\ cross-attention with encoder outputs, and a position-wise feed-forward network\ with residual connections and layer normalization. Dropout is applied after each\ sub-layer (self-attention, cross-attention, and twice in the feed-forward network)\ before the residual connection for regularization. **Parameters:** | Name | Type | Description | Default | | ------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------- | | `self_attention` | [AttentionLayer](#neuralforecast.common._modules.AttentionLayer) | Masked self-attention mechanism for the decoder. | *required* | | `cross_attention` | [AttentionLayer](#neuralforecast.common._modules.AttentionLayer) | Cross-attention mechanism to attend to encoder outputs. | *required* | | `hidden_size` | [int](#int) | Dimension of the model's hidden representations. | *required* | | `conv_hidden_size` | [int](#int) | Dimension of the feed-forward network's hidden layer. Defaults to 4 \* hidden\_size if not specified. | None | | `dropout` | [float](#float) | Dropout probability applied after attention and feed-forward layers. | 0.1 | | `activation` | [str](#str) | Activation function to use in the feed-forward network. Either "relu" or "gelu". | 'relu' | **Returns:** | Type | Description | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Output tensor of shape \[batch, target\_seq\_len, hidden\_size] after applying masked self-attention, cross-attention, and feed-forward transformations. |
Notes The layer applies three main operations in sequence: 1. Masked self-attention on the decoder input with dropout, residual connection, and normalization 2. Cross-attention between decoder and encoder outputs with dropout, residual connection, and normalization 3. Position-wise feed-forward network using 1D convolutions with dropout applied twice (after each convolution), residual connection, and normalization
### `AttentionLayer` ```python theme={null} AttentionLayer(attention, hidden_size, n_heads, d_keys=None, d_values=None) ``` Bases: [Module](#torch.nn.Module) Multi-head attention layer wrapper. This layer wraps an attention mechanism and handles the linear projections\ for queries, keys, and values in multi-head attention. It projects inputs\ to multiple heads, applies the inner attention mechanism, and projects back\ to the original hidden dimension. **Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------- | | `attention` | [Module](#torch.nn.Module) | Inner attention mechanism (e.g., FullAttention, ProbAttention) that computes attention scores and outputs. | *required* | | `hidden_size` | [int](#int) | Dimension of the model's hidden states. | *required* | | `n_heads` | [int](#int) | Number of attention heads. | *required* | | `d_keys` | [int](#int) | Dimension of keys per head. If `None` defaults to hidden\_size // n\_heads. | None | | `d_values` | [int](#int) | Dimension of values per head. If `None` defaults to hidden\_size // n\_heads. | None | **Returns:** | Type | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Output tensor of shape \[batch, seq\_len, hidden\_size] after applying multi-head attention. | | (torch.Tensor) or None: Attention weights of shape \[batch, n\_heads, seq\_len, seq\_len] if output\_attention is True in the inner attention mechanism, otherwise None. | |
Notes * The forward method accepts queries, keys, values, and optional masks. * Additional parameters tau and delta are passed through to the inner attention mechanism for specialized attention variants.
### `FullAttention` ```python theme={null} FullAttention( mask_flag=True, factor=5, scale=None, attention_dropout=0.1, output_attention=False, ) ``` Bases: [Module](#torch.nn.Module) Full attention mechanism with scaled dot-product attention. Implements standard multi-head attention using scaled dot-product attention.\ Supports both efficient computation via PyTorch's scaled\_dot\_product\_attention\ and explicit attention computation when attention weights are needed. Optional\ causal masking prevents attention to future positions in autoregressive models. **Parameters:** | Name | Type | Description | Default | | ------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------ | | `mask_flag` | [bool](#bool) | If True, applies causal masking to prevent attention to future positions. | True | | `factor` | [int](#int) | Attention factor parameter (unused in FullAttention, kept for API compatibility with ProbAttention). | 5 | | `scale` | [float](#float) | Custom scaling factor for attention scores. If None, uses 1/sqrt(d\_k) where d\_k is the key dimension. | None | | `attention_dropout` | [float](#float) | Dropout rate applied to attention weights. | 0.1 | | `output_attention` | [bool](#bool) | If True, returns attention weights along with output. If False, uses efficient flash attention. | False | **Returns:** | Type | Description | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Attention output of shape \[batch, seq\_len, n\_heads, head\_dim]. | | [Tensor](#torch.Tensor) or None | Attention weights of shape \[batch, n\_heads, seq\_len, seq\_len] if output\_attention is True, otherwise None. |
Notes * When output\_attention=False, uses PyTorch's optimized scaled\_dot\_product\_attention for better performance (flash attention). * When output\_attention=True, computes attention explicitly using einsum operations. * If mask\_flag=True and no attn\_mask is provided, automatically creates a TriangularCausalMask for autoregressive attention. * The tau and delta parameters are accepted for API compatibility but unused.
### `TriangularCausalMask` ```python theme={null} TriangularCausalMask(B, L, device='cpu') ``` Triangular causal mask for autoregressive attention. Creates an upper triangular boolean mask that prevents attention mechanisms\ from attending to future positions in the sequence. This ensures causality\ in autoregressive models where predictions at time t should only depend on\ positions before t. The mask is created using torch.triu with diagonal=1, resulting in a mask\ where positions (i, j) are True when j > i, effectively masking out future\ positions during attention computation. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ----------------------------------- | ------------------ | | `B` | [int](#int) | Batch size. | *required* | | `L` | [int](#int) | Sequence length. | *required* | | `device` | [str](#str) | Device to place the mask tensor on. | 'cpu' | **Attributes:** | Name | Type | Description | | --------------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | [`_mask`](#neuralforecast.common._modules.TriangularCausalMask._mask) | [Tensor](#torch.Tensor) | Boolean mask tensor of shape \[B, 1, L, L] where True values indicate positions to mask (future positions). }} |
Notes * The mask shape \[B, 1, L, L] is designed for multi-head attention where\ the second dimension broadcasts across attention heads. * True values in the mask indicate positions that should be masked out\ (set to -inf before softmax in attention).
### `DataEmbedding_inverted` ```python theme={null} DataEmbedding_inverted(c_in, hidden_size, dropout=0.1) ``` Bases: [Module](#torch.nn.Module) Inverted data embedding module for variate-as-token transformer architectures. Transforms time series data by treating each variate (channel) as a token rather\ than each time step. The input is permuted from \[Batch, Time, Variate] to\ \[Batch, Variate, Time], then a linear layer projects the time dimension to the\ hidden dimension. Optionally concatenates temporal covariates along the variate\ dimension. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------- | --------------------------------------------- | ---------------- | | `c_in` | [int](#int) | Number of input time steps (sequence length). | *required* | | `hidden_size` | [int](#int) | Dimension of the embedding vectors. | *required* | | `dropout` | [float](#float) | Dropout rate applied to the embeddings. | 0.1 | **Returns:** | Type | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Inverted embeddings of shape \[batch, n\_variates, hidden\_size] or \[batch, n\_variates + n\_temporal\_features, hidden\_size] if x\_mark is provided. |
Notes * Input x has shape \[Batch, Time, Variate] and is permuted to \[Batch, Variate, Time]. * If x\_mark is provided, it's concatenated along the variate dimension after permutation. * The linear layer projects from c\_in (time steps) to hidden\_size dimensions. * This architecture is used in inverted transformers like iTransformer and TimeXer.
### `DataEmbedding` ```python theme={null} DataEmbedding( c_in, exog_input_size, hidden_size, pos_embedding=True, dropout=0.1 ) ``` Bases: [Module](#torch.nn.Module) Data embedding module combining value, positional, and temporal embeddings. Transforms time series data into high-dimensional embeddings by combining: * Value embeddings: Convolutional encoding of the time series values * Positional embeddings: Sinusoidal encodings for relative position within window * Temporal embeddings: Linear projection of absolute calendar features (optional) **Parameters:** | Name | Type | Description | Default | | ----------------- | ---------------------------- | ------------------------------------------------------------------------------ | ----------------- | | `c_in` | [int](#int) | Number of input channels (variates) in the time series. | *required* | | `exog_input_size` | [int](#int) | Number of exogenous/temporal features. If 0, temporal embeddings are disabled. | *required* | | `hidden_size` | [int](#int) | Dimension of the embedding vectors. | *required* | | `pos_embedding` | [bool](#bool) | Whether to include positional embeddings. | True | | `dropout` | [float](#float) | Dropout rate applied to the final embeddings. | 0.1 | **Returns:** | Type | Description | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Combined embeddings of shape \[batch, seq\_len, hidden\_size] after applying dropout to the sum of value, positional, and temporal embeddings. |
Notes * Value embeddings use `TokenEmbedding` with 1D convolution (kernel\_size=3). * Positional embeddings use sinusoidal functions (sine for even dims, cosine for odd). * Temporal embeddings use a linear layer to project calendar features. * All three embeddings are summed element-wise before dropout is applied. * If `x_mark` is None, only value and positional embeddings are used.
### `TemporalEmbedding` ```python theme={null} TemporalEmbedding(d_model, embed_type='fixed', freq='h') ``` Bases: [Module](#torch.nn.Module) Temporal embedding module for encoding calendar-based time features. Creates learnable or fixed embeddings for temporal features including month,\ day, weekday, hour, and optionally minute. These embeddings are summed to\ produce a combined temporal representation. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | -------------------- | | `d_model` | [int](#int) | Dimension of the embedding vectors. | *required* | | `embed_type` | [str](#str) | Type of embedding to use. Options are "fixed" for FixedEmbedding (sinusoidal) or "learned" for nn.Embedding (learnable). | 'fixed' | | `freq` | [str](#str) | Frequency of the time series data. If "t", includes minute embeddings. | 'h' | **Returns:** | Type | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | | [Tensor](#torch.Tensor) | Combined temporal embeddings of shape \[batch, seq\_len, d\_model], representing the sum of all temporal component embeddings. |
Notes * Input tensor x should have shape \[batch\_size, seq\_len, num\_features] where features are ordered as \[month, day, weekday, hour, minute]. * Month embeddings use size 13 (0-12), day uses 32 (0-31), weekday uses 7 (0-6), hour uses 24 (0-23), and minute uses 4 (0-3). * The embeddings are summed element-wise to produce the final output.
### `FixedEmbedding` ```python theme={null} FixedEmbedding(c_in, d_model) ``` Bases: [Module](#torch.nn.Module) Fixed sinusoidal embedding for categorical temporal features. Creates non-trainable embeddings using sine and cosine functions at different\ frequencies. Unlike PositionalEmbedding which encodes continuous positions,\ FixedEmbedding is designed for discrete categorical inputs (e.g., hour of day,\ day of month, month of year). The embeddings are precomputed and frozen,\ making them non-learnable parameters.
The embedding for category c and dimension i is computed as Emb(c, 2i) = sin(c / 10000^(2i/d\_model))\ Emb(c, 2i+1) = cos(c / 10000^(2i/d\_model))
**Parameters:** | Name | Type | Description | Default | | --------- | ------------------------ | ------------------------------------------------------- | ---------- | | `c_in` | [int](#int) | Number of categories (e.g., 24 for hours, 32 for days). | *required* | | `d_model` | [int](#int) | Dimension of the embedding vectors. | *required* | **Returns:** | Type | Description | | ------------------------------------ | -------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Fixed embeddings of shape \[batch, seq\_len, d\_model], detached from the computation graph. |
Notes * Embeddings are frozen and cannot be trained. * The forward method returns detached tensors to prevent gradient flow. * Used as an alternative to nn.Embedding for temporal features. * Provides consistent representations across different time periods.
### `TimeFeatureEmbedding` ```python theme={null} TimeFeatureEmbedding(input_size, hidden_size) ``` Bases: [Module](#torch.nn.Module) Linear embedding for temporal/calendar features. Transforms time-based features (e.g., hour, day, month) into embeddings using\ a single linear projection without bias. This embedding is typically used to\ incorporate calendar information into transformer models, providing absolute\ temporal context that complements positional encodings. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ---------------------------------------------------------------------------------- | ---------- | | `input_size` | [int](#int) | Number of input temporal features (e.g., 5 for month, day, weekday, hour, minute). | *required* | | `hidden_size` | [int](#int) | Dimension of the output embeddings, matching the model's hidden dimension. | *required* | **Returns:** | Type | Description | | ------------------------------------ | ------------------------------------------------------------------ | | [Tensor](#torch.Tensor) | Time feature embeddings of shape \[batch, seq\_len, hidden\_size]. |
Notes * Uses a bias-free linear layer for simple feature projection. * Typically combined with TokenEmbedding and PositionalEmbedding. * Input features are usually calendar-based (month, day, hour, etc.). * The embedding is learned during training, unlike fixed positional encodings.
### `PositionalEmbedding` ```python theme={null} PositionalEmbedding(hidden_size, max_len=5000) ``` Bases: [Module](#torch.nn.Module) Sinusoidal positional embedding for transformer models. Generates fixed sinusoidal positional encodings using sine and cosine functions\ at different frequencies. These encodings provide position information to\ transformer models, allowing them to understand the relative or absolute position\ of tokens in a sequence. The encodings are precomputed and stored as a buffer,\ making them non-trainable.
The positional encoding for position pos and dimension i is computed as PE(pos, 2i) = sin(pos / 10000^(2i/hidden\_size))\ PE(pos, 2i+1) = cos(pos / 10000^(2i/hidden\_size))
**Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------------------------------------ | ----------------- | | `hidden_size` | [int](#int) | Dimension of the model's hidden states. Must be even for proper sine/cosine pairing. | *required* | | `max_len` | [int](#int) | Maximum sequence length to precompute encodings for. | 5000 | **Returns:** | Type | Description | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Positional encodings of shape \[1, seq\_len, hidden\_size] where seq\_len is the length of the input sequence. |
Notes * The positional encodings are fixed (not learned) and stored as a buffer. * The forward method returns encodings for the input sequence length only. * Different frequencies allow the model to attend to relative positions. * The encoding dimension must match the model's hidden\_size.
### `SeriesDecomp` ```python theme={null} SeriesDecomp(kernel_size) ``` Bases: [Module](#torch.nn.Module) Series decomposition block for trend-residual decomposition. Decomposes time series into trend and residual components using moving average filtering. The trend is extracted via a moving average filter, and the residual is computed as the difference between the input and the trend. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ------------------------------------------------------- | ---------- | | `kernel_size` | [int](#int) | Size of the moving average window for trend extraction. | *required* | **Returns:** | Type | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Residual component of shape \[batch, seq\_len, channels], computed as the input minus the trend. | | [Tensor](#torch.Tensor) | Trend component of shape \[batch, seq\_len, channels], extracted using the moving average filter. |
Notes * The kernel\_size is passed to MovingAvg with stride=1. * The residual component is computed as input minus trend. * The trend component is the smoothed series from the moving average. * Commonly used in decomposition-based forecasting models like DLinear and Autoformer.
### `MovingAvg` ```python theme={null} MovingAvg(kernel_size, stride) ``` Bases: [Module](#torch.nn.Module) Moving average block to highlight the trend of time series. Applies a moving average filter using 1D average pooling to smooth time series\ data and extract trend components. The input is padded on both ends by repeating\ the first and last values to maintain the original sequence length. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ----------------------------------------- | ---------- | | `kernel_size` | [int](#int) | Size of the moving average window. | *required* | | `stride` | [int](#int) | Stride for the average pooling operation. | *required* | **Returns:** | Type | Description | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Smoothed time series of shape \[batch, seq\_len, channels], representing the trend component after applying moving average. |
Notes * Input x has shape \[Batch, Time, Channels]. * Padding is applied by repeating the first value (kernel\_size-1)//2 times at the beginning and the last value (kernel\_size-1)//2 times at the end. * The output maintains the same shape as the input after padding and pooling. * Commonly used with stride=1 for trend extraction in decomposition models.
### `RevIN` ```python theme={null} RevIN( num_features, eps=1e-05, affine=False, subtract_last=False, non_norm=False ) ``` Bases: [Module](#torch.nn.Module) Reversible Instance Normalization for time series forecasting. Normalizes time series data by removing the mean (or last value) and scaling by\ standard deviation. The normalization can be reversed after model predictions to\ restore the original scale. Optionally includes learnable affine parameters for\ additional transformation flexibility. **Parameters:** | Name | Type | Description | Default | | --------------- | ---------------------------- | ----------------------------------------------------------------------- | ------------------ | | `num_features` | [int](#int) | The number of features or channels in the time series. | *required* | | `eps` | [float](#float) | A value added for numerical stability. | 1e-05 | | `affine` | [bool](#bool) | If True, RevIN has learnable affine parameters (weight and bias). | False | | `subtract_last` | [bool](#bool) | If True, subtracts the last value instead of the mean in normalization. | False | | `non_norm` | [bool](#bool) | If True, no normalization is performed (identity operation). | False | **Returns:** | Type | Description | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Normalized tensor (if mode="norm") or denormalized tensor (if mode="denorm") of the same shape as the input \[batch, seq\_len, num\_features]. |
Notes * The forward method requires a mode parameter: "norm" for normalization or "denorm" for denormalization. * Statistics (mean/last and stdev) are computed during normalization and stored for use in denormalization. * If affine=True, learnable parameters are initialized as weight=1 and bias=0. * The subtract\_last option is useful for non-stationary time series. * Used in models like PatchTST and TimeLLM for input preprocessing.
### `RevINMultivariate` ```python theme={null} RevINMultivariate( num_features, eps=1e-05, affine=False, subtract_last=False, non_norm=False ) ``` Bases: [Module](#torch.nn.Module) Reversible Instance Normalization for multivariate time series models. Normalizes multivariate time series data using batch statistics computed across\ the time dimension. The normalization can be reversed after model predictions to\ restore the original scale. Optionally includes learnable affine parameters for\ additional transformation flexibility. **Parameters:** | Name | Type | Description | Default | | --------------- | ---------------------------- | ----------------------------------------------------------------------------- | ------------------ | | `num_features` | [int](#int) | The number of features or channels in the time series. | *required* | | `eps` | [float](#float) | A value added for numerical stability. | 1e-05 | | `affine` | [bool](#bool) | If True, RevINMultivariate has learnable affine parameters (weight and bias). | False | | `subtract_last` | [bool](#bool) | Not used in this implementation (kept for API compatibility). | False | | `non_norm` | [bool](#bool) | Not used in this implementation (kept for API compatibility). | False | **Returns:** | Type | Description | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | [Tensor](#torch.Tensor) | Normalized tensor (if mode="norm") or denormalized tensor (if mode="denorm") of the same shape as the input \[batch, seq\_len, num\_features]. |
Notes * The forward method requires a mode parameter: "norm" for normalization or "denorm" for denormalization. * Batch statistics (mean and std) are computed across axis=1 (time dimension). * If affine=True, learnable parameters have shape \[1, 1, num\_features]. * Used in multivariate models like TSMixer, TSMixerx, and RMoK.
# TemporalNorm Source: https://nixtlaverse.nixtla.io/neuralforecast/common.scalers.html TemporalNorm: Temporal normalization techniques for neural forecasting. Scalers include standard, robust, invariant, and RevIN for distribution shift handling. ## Introduction Temporal normalization has proven to be essential in neural forecasting tasks, as it enables network's non-linearities to express themselves. Forecasting scaling methods take particular interest in the temporal dimension where most of the variance dwells, contrary to other deep learning techniques like `BatchNorm` that normalizes across batch and temporal dimensions, and `LayerNorm` that normalizes across the feature dimension. Currently we support the following techniques: `std`, `median`, `norm`, `norm1`, `invariant`, `revin`. ## References * [Kin G. Olivares, David Luo, Cristian Challu, Stefania La Vattiata, Max Mergenthaler, Artur Dubrawski (2023). "HINT: Hierarchical Mixture Networks For Coherent Probabilistic Forecasting". Neural Information Processing Systems, submitted. Working Paper version available at arxiv.](https://arxiv.org/abs/2305.07089) * [Taesung Kim and Jinhee Kim and Yunwon Tae and Cheonbok Park and Jang-Ho Choi and Jaegul Choo. "Reversible Instance Normalization for Accurate Time-Series Forecasting against Distribution Shift". ICLR 2022.](https://openreview.net/pdf?id=cGDAkQo1C0p) * [David Salinas, Valentin Flunkert, Jan Gasthaus, Tim Januschowski (2020). "DeepAR: Probabilistic forecasting with autoregressive recurrent networks". International Journal of Forecasting.](https://www.sciencedirect.com/science/article/pii/S0169207019301888) *Figure 1. Illustration of temporal normalization (left), layer normalization (center) and batch normalization (right). The entries in green show the components used to compute the normalizing statistics.* ## 1. Auxiliary Functions ### `masked_median` ```python theme={null} masked_median(x, mask, dim=-1, keepdim=True) ``` Masked Median Compute the median of tensor `x` along dim, ignoring values where `mask` is False. `x` and `mask` need to be broadcastable. **Parameters:** | Name | Type | Description | Default | | --------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `x` | [Tensor](#torch.Tensor) | Tensor to compute median of along `dim` dimension. | *required* | | `mask` | [Tensor](#torch.Tensor) | Tensor bool with same shape as `x`, where `x` is valid and False where `x` should be masked. Mask should not be all False in any column of dimension dim to avoid NaNs from zero division. | *required* | | `dim` | [int](#int) | Dimension to take median of. Defaults to -1. | -1 | | `keepdim` | [bool](#bool) | Keep dimension of `x` or not. Defaults to True. | True | **Returns:** | Type | Description | | -------------------------------- | ----------- | | torch.Tensor: Normalized values. | | ### `masked_mean` ```python theme={null} masked_mean(x, mask, dim=-1, keepdim=True) ``` Masked Mean Compute the mean of tensor `x` along dimension, ignoring values where `mask` is False. `x` and `mask` need to be broadcastable. **Parameters:** | Name | Type | Description | Default | | --------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `x` | [Tensor](#torch.Tensor) | Tensor to compute mean of along `dim` dimension. | *required* | | `mask` | [Tensor](#torch.Tensor) | Tensor bool with same shape as `x`, where `x` is valid and False where `x` should be masked. Mask should not be all False in any column of dimension dim to avoid NaNs from zero division. | *required* | | `dim` | [int](#int) | Dimension to take mean of. Defaults to -1. | -1 | | `keepdim` | [bool](#bool) | Keep dimension of `x` or not. Defaults to True. | True | **Returns:** | Type | Description | | -------------------------------- | ----------- | | torch.Tensor: Normalized values. | | ## 2. Scalers ### `minmax_statistics` ```python theme={null} minmax_statistics(x, mask, eps=1e-06, dim=-1) ``` MinMax Scaler Standardizes temporal features by ensuring its range dweels between \[0,1] range. This transformation is often used as an alternative to the standard scaler. The scaled features are obtained as: ```math theme={null} \mathbf{z} = (\mathbf{x}_{[B,T,C]}-\mathrm{min}({\mathbf{x}})_{[B,1,C]})/ (\mathrm{max}({\mathbf{x}})_{[B,1,C]}- \mathrm{min}({\mathbf{x}})_{[B,1,C]}) ``` **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `x` | [Tensor](#torch.Tensor) | Input tensor. | *required* | | `mask` | [Tensor](#torch.Tensor) | Tensor bool, same dimension as `x`, indicates where `x` is valid and False where `x` should be masked. Mask should not be all False in any column of dimension dim to avoid NaNs from zero division. | *required* | | `eps` | [float](#float) | Small value to avoid division by zero. Defaults to 1e-6. | 1e-06 | | `dim` | [int](#int) | Dimension over to compute min and max. Defaults to -1. | -1 | **Returns:** | Type | Description | | ----------------------------------------------- | ----------- | | torch.Tensor: Same shape as `x`, except scaled. | | ### `minmax1_statistics` ```python theme={null} minmax1_statistics(x, mask, eps=1e-06, dim=-1) ``` MinMax1 Scaler Standardizes temporal features by ensuring its range dweels between \[-1,1] range. This transformation is often used as an alternative to the standard scaler or classic Min Max Scaler. The scaled features are obtained as: ```math theme={null} \mathbf{z} = 2 (\mathbf{x}_{[B,T,C]}-\mathrm{min}({\mathbf{x}})_{[B,1,C]})/ (\mathrm{max}({\mathbf{x}})_{[B,1,C]}- \mathrm{min}({\mathbf{x}})_{[B,1,C]})-1 ``` **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `x` | [Tensor](#torch.Tensor) | Input tensor. | *required* | | `mask` | [Tensor](#torch.Tensor) | Tensor bool, same dimension as `x`, indicates where `x` is valid and False where `x` should be masked. Mask should not be all False in any column of dimension dim to avoid NaNs from zero division. | *required* | | `eps` | [float](#float) | Small value to avoid division by zero. Defaults to 1e-6. | 1e-06 | | `dim` | [int](#int) | Dimension over to compute min and max. Defaults to -1. | -1 | **Returns:** | Type | Description | | ----------------------------------------------- | ----------- | | torch.Tensor: Same shape as `x`, except scaled. | | ### `std_statistics` ```python theme={null} std_statistics(x, mask, dim=-1, eps=1e-06) ``` Standard Scaler Standardizes features by removing the mean and scaling to unit variance along the `dim` dimension. For example, for `base_windows` models, the scaled features are obtained as (with dim=1): ```math theme={null} \mathbf{z} = (\mathbf{x}_{[B,T,C]}-\bar{\mathbf{x}}_{[B,1,C]})/\hat{\sigma}_{[B,1,C]} ``` **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `x` | [Tensor](#torch.Tensor) | Input tensor. | *required* | | `mask` | [Tensor](#torch.Tensor) | Tensor bool, same dimension as `x`, indicates where `x` is valid and False where `x` should be masked. Mask should not be all False in any column of dimension dim to avoid NaNs from zero division. | *required* | | `eps` | [float](#float) | Small value to avoid division by zero. Defaults to 1e-6. | 1e-06 | | `dim` | [int](#int) | Dimension over to compute mean and std. Defaults to -1. | -1 | **Returns:** | Type | Description | | ----------------------------------------------- | ----------- | | torch.Tensor: Same shape as `x`, except scaled. | | ### `robust_statistics` ```python theme={null} robust_statistics(x, mask, dim=-1, eps=1e-06) ``` Robust Median Scaler Standardizes features by removing the median and scaling with the mean absolute deviation (mad) a robust estimator of variance. This scaler is particularly useful with noisy data where outliers can heavily influence the sample mean / variance in a negative way. In these scenarios the median and amd give better results. For example, for `base_windows` models, the scaled features are obtained as (with dim=1): ```math theme={null} \mathbf{z} = (\mathbf{x}_{[B,T,C]}-\textrm{median}(\mathbf{x})_{[B,1,C]})/\textrm{mad}(\mathbf{x})_{[B,1,C]} ``` ```math theme={null} \textrm{mad}(\mathbf{x}) = \frac{1}{N} \sum_{}|\mathbf{x} - \mathrm{median}(x)| ``` **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `x` | [Tensor](#torch.Tensor) | Input tensor. | *required* | | `mask` | [Tensor](#torch.Tensor) | Tensor bool, same dimension as `x`, indicates where `x` is valid and False where `x` should be masked. Mask should not be all False in any column of dimension dim to avoid NaNs from zero division. | *required* | | `eps` | [float](#float) | Small value to avoid division by zero. Defaults to 1e-6. | 1e-06 | | `dim` | [int](#int) | Dimension over to compute median and mad. Defaults to -1. | -1 | **Returns:** | Type | Description | | ----------------------------------------------- | ----------- | | torch.Tensor: Same shape as `x`, except scaled. | | ### `invariant_statistics` ```python theme={null} invariant_statistics(x, mask, dim=-1, eps=1e-06) ``` Invariant Median Scaler Standardizes features by removing the median and scaling with the mean absolute deviation (mad) a robust estimator of variance. Aditionally it complements the transformation with the arcsinh transformation. For example, for `base_windows` models, the scaled features are obtained as (with dim=1): ```math theme={null} \mathbf{z} = (\mathbf{x}_{[B,T,C]}-\textrm{median}(\mathbf{x})_{[B,1,C]})/\textrm{mad}(\mathbf{x})_{[B,1,C]} ``` ```math theme={null} \mathbf{z} = \textrm{arcsinh}(\mathbf{z}) ``` **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `x` | [Tensor](#torch.Tensor) | Input tensor. | *required* | | `mask` | [Tensor](#torch.Tensor) | Tensor bool, same dimension as `x`, indicates where `x` is valid and False where `x` should be masked. Mask should not be all False in any column of dimension dim to avoid NaNs from zero division. | *required* | | `eps` | [float](#float) | Small value to avoid division by zero. Defaults to 1e-6. | 1e-06 | | `dim` | [int](#int) | Dimension over to compute median and mad. Defaults to -1. | -1 | **Returns:** | Type | Description | | ----------------------------------------------- | ----------- | | torch.Tensor: Same shape as `x`, except scaled. | | ### `identity_statistics` ```python theme={null} identity_statistics(x, mask, dim=-1, eps=1e-06) ``` Identity Scaler A placeholder identity scaler, that is argument insensitive. **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `x` | [Tensor](#torch.Tensor) | Input tensor. | *required* | | `mask` | [Tensor](#torch.Tensor) | Tensor bool, same dimension as `x`, indicates where `x` is valid and False where `x` should be masked. Mask should not be all False in any column of dimension dim to avoid NaNs from zero division. | *required* | | `eps` | [float](#float) | Small value to avoid division by zero. Defaults to 1e-6. | 1e-06 | | `dim` | [int](#int) | Dimension over to compute median and mad. Defaults to -1. | -1 | **Returns:** | Type | Description | | --------------------------- | ----------- | | torch.Tensor: Original `x`. | | ## 3. TemporalNorm Module ### `TemporalNorm` ```python theme={null} TemporalNorm(scaler_type='robust', dim=-1, eps=1e-06, num_features=None) ``` Bases: [Module](#torch.nn.Module) Temporal Normalization Standardization of the features is a common requirement for many machine learning estimators, and it is commonly achieved by removing the level and scaling its variance. The `TemporalNorm` module applies temporal normalization over the batch of inputs as defined by the type of scaler. ```math theme={null} \mathbf{z}_{[B,T,C]} = \textrm{Scaler}(\mathbf{x}_{[B,T,C]}) ``` If `scaler_type` is `revin` learnable normalization parameters are added on top of the usual normalization technique, the parameters are learned through scale decouple global skip connections. The technique is available for point and probabilistic outputs. ```math theme={null} \mathbf{\hat{z}}_{[B,T,C]} = \boldsymbol{\hat{\gamma}}_{[1,1,C]} \mathbf{z}_{[B,T,C]} +\boldsymbol{\hat{\beta}}_{[1,1,C]} ``` **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | `scaler_type` | [str](#str) | Defines the type of scaler used by TemporalNorm. Available \[`identity`, `standard`, `robust`, `minmax`, `minmax1`, `invariant`, `revin`]. Defaults to "robust". | 'robust' | `dim` (int, optional): Dimension over to compute scale and shift. Defaults to -1. eps (float, optional): Small value to avoid division by zero. Defaults to 1e-6. num\_features (int, optional): For RevIN-like learnable affine parameters initialization. Defaults to None.
References * [Kin G. Olivares, David Luo, Cristian Challu, Stefania La Vattiata, Max Mergenthaler, Artur Dubrawski (2023). "HINT: Hierarchical Mixture Networks For Coherent Probabilistic Forecasting". Neural Information Processing Systems, submitted. Working Paper version available at arxiv.](https://arxiv.org/abs/2305.07089)
#### `TemporalNorm.transform` ```python theme={null} transform(x, mask) ``` Center and scale the data. **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `x` | [Tensor](#torch.Tensor) | Tensor shape \[batch, time, channels]. | *required* | | `mask` | [Tensor](#torch.Tensor) | Tensor bool, shape \[batch, time] where `x` is valid and False where `x` should be masked. Mask should not be all False in any column of dimension dim to avoid NaNs from zero division. | *required* | **Returns:** | Type | Description | | ----------------------------------------------- | ----------- | | torch.Tensor: Same shape as `x`, except scaled. | | #### `TemporalNorm.inverse_transform` ```python theme={null} inverse_transform(z, x_shift=None, x_scale=None) ``` Scale back the data to the original representation. **Parameters:** | Name | Type | Description | Default | | --------- | ------------------------------------ | -------------------------------------------------------- | ----------------- | | `z` | [Tensor](#torch.Tensor) | Tensor shape \[batch, time, channels], scaled. | *required* | | `x_shift` | [Tensor](#torch.Tensor) | Tensor shape \[1, 1, channels], shift. Defaults to None. | None | | `x_scale` | [Tensor](#torch.Tensor) | Tensor shape \[1, 1, channels], scale. Defaults to None. | None | **Returns:** | Type | Description | | ---------------------------- | ----------- | | torch.Tensor: Original data. | | ## Example ```python theme={null} import numpy as np ``` ```python theme={null} # Declare synthetic batch to normalize x1 = 10**0 * np.arange(36)[:, None] x2 = 10**1 * np.arange(36)[:, None] np_x = np.concatenate([x1, x2], axis=1) np_x = np.repeat(np_x[None, :,:], repeats=2, axis=0) np_x[0,:,:] = np_x[0,:,:] + 100 np_mask = np.ones(np_x.shape) np_mask[:, -12:, :] = 0 print(f'x.shape [batch, time, features]={np_x.shape}') print(f'mask.shape [batch, time, features]={np_mask.shape}') ``` ```python theme={null} # Validate scalers x = 1.0*torch.tensor(np_x) mask = torch.tensor(np_mask) scaler = TemporalNorm(scaler_type='standard', dim=1) x_scaled = scaler.transform(x=x, mask=mask) x_recovered = scaler.inverse_transform(x_scaled) plt.plot(x[0,:,0], label='x1', color='#78ACA8') plt.plot(x[0,:,1], label='x2', color='#E3A39A') plt.title('Before TemporalNorm') plt.xlabel('Time') plt.legend() plt.show() plt.plot(x_scaled[0,:,0], label='x1', color='#78ACA8') plt.plot(x_scaled[0,:,1]+0.1, label='x2+0.1', color='#E3A39A') plt.title(f'TemporalNorm \'{scaler.scaler_type}\' ') plt.xlabel('Time') plt.legend() plt.show() plt.plot(x_recovered[0,:,0], label='x1', color='#78ACA8') plt.plot(x_recovered[0,:,1], label='x2', color='#E3A39A') plt.title('Recovered') plt.xlabel('Time') plt.legend() plt.show() ``` # Core | NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/core.html NeuralForecast core class for high-level time series forecasting. Fits multiple PyTorch models on pandas DataFrames with parallelization and distributed computation. NeuralForecast contains two main components, PyTorch implementations deep learning predictive models, as well as parallelization and distributed computation utilities. The first component comprises low-level PyTorch model estimator classes like `models.NBEATS` and `models.RNN`. The second component is a high-level `core.NeuralForecast` wrapper class that operates with sets of time series data stored in pandas DataFrames. ## ### `NeuralForecast` ```python theme={null} NeuralForecast( models, freq, local_scaler_type=None, local_static_scaler_type=None ) ``` The `core.StatsForecast` class allows you to efficiently fit multiple `NeuralForecast` models for large sets of time series. It operates with a pandas DataFrame `df` that identifies series and datestamps with the `unique_id` and `ds` columns. The `y` column denotes the target time series variable. **Parameters:** | Name | Type | Description | Default | | -------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `models` | [List](#typing.List)\[[Any](#typing.Any)] | Instantiated `neuralforecast.models` see [collection here](./models.html). | *required* | | `freq` | [str](#str) or [int](#int) | Frequency of the data. Must be a valid pandas or polars offset alias, or an integer. | *required* | | `local_scaler_type` | [str](#str) | Scaler to apply per-serie to temporal features before fitting, which is inverted after predicting. Can be 'standard', 'robust', 'robust-iqr', 'minmax' or 'boxcox'. | None | | `local_static_scaler_type` | [str](#str) | Scaler to apply to static exogenous features before fitting. Can be 'standard', 'robust', 'robust-iqr', 'minmax' or 'boxcox'. | None | **Returns:** | Name | Type | Description | | ---------------- | ---- | -------------------------------------------- | | `NeuralForecast` | | Returns instantiated `NeuralForecast` class. | #### `NeuralForecast.fit` ```python theme={null} fit( df=None, static_df=None, val_size=0, val_df=None, use_init_models=False, verbose=False, id_col="unique_id", time_col="ds", target_col="y", distributed_config=None, prediction_intervals=None, ) ``` Fit the core.NeuralForecast Fit `models` to a large set of time series from DataFrame `df` and store fitted models for later inspection. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas, polars or spark DataFrame, or a list of parquet files containing the series | DataFrame with columns \[`unique_id`, `ds`, `y`] and exogenous variables. If None, a previously stored dataset is required. | None | | `static_df` | pandas, polars or spark DataFrame | DataFrame with columns \[`unique_id`] and static exogenous. | None | | `val_size` | [int](#int) | Size of validation set. Cannot be used together with `val_df`. | 0 | | `val_df` | pandas or polars DataFrame | Explicit validation DataFrame with columns \[`unique_id`, `ds`, `y`] and exogenous variables. `val_df` can be temporally independent (no requirement that it starts immediately after `df`). Cannot be used together with `val_size`. Only supported when `df` is a pandas or polars DataFrame. All series in `val_df` must have the same length. | None | | `use_init_models` | [bool](#bool) | If True, discards any previously fitted weights and reinitializes the models from the configs passed at `NeuralForecast(__init__)`. Use this to start training from scratch. Defaults to False. | False | | `verbose` | [bool](#bool) | Print processing steps. | False | | `id_col` | [str](#str) | Column that identifies each serie. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. | 'y' | | `distributed_config` | [DistributedConfig](#neuralforecast.DistributedConfig) | Configuration to use for DDP training. Currently only spark is supported. | None | | `prediction_intervals` | [PredictionIntervals](#neuralforecast.utils.PredictionIntervals) | Configuration to calibrate prediction intervals (Conformal Prediction). | None | **Returns:** | Name | Type | Description | | ---------------- | ----------------- | ---------------------------------------------------- | | `NeuralForecast` | None | Returns `NeuralForecast` class with fitted `models`. | #### `NeuralForecast.predict` ```python theme={null} predict( df=None, static_df=None, futr_df=None, verbose=False, engine=None, level=None, quantiles=None, h=None, **data_kwargs ) ``` Predict with core.NeuralForecast. Use stored fitted `models` to predict large set of time series from DataFrame `df`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `df` | pandas, polars or spark DataFrame | DataFrame with columns \[`unique_id`, `ds`, `y`] and exogenous variables. If a DataFrame is passed, it is used to generate forecasts. | None | | `static_df` | pandas, polars or spark DataFrame | DataFrame with columns \[`unique_id`] and static exogenous. | None | | `futr_df` | pandas, polars or spark DataFrame | DataFrame with \[`unique_id`, `ds`] columns and `df`'s future exogenous. | None | | `verbose` | [bool](#bool) | Print processing steps. | False | | `engine` | spark session | Distributed engine for inference. Only used if df is a spark dataframe or if fit was called on a spark dataframe. | None | | `level` | list of ints or floats | Confidence levels between 0 and 100. | None | | `quantiles` | list of floats | Alternative to level, target quantiles to predict. | None | | `h` | [int](#int) | Forecasting horizon. If None, uses the horizon of the fitted models. | None | | `data_kwargs` | [kwargs](#kwargs) | Extra arguments to be passed to the dataset within each model. | | **Returns:** | Name | Type | Description | | ---------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `fcsts_df` | pandas or polars DataFrame | DataFrame with insample `models` columns for point predictions and probabilistic predictions for all fitted `models`. | #### `NeuralForecast.cross_validation` ```python theme={null} cross_validation( df=None, static_df=None, n_windows=1, step_size=1, val_size=0, test_size=None, use_init_models=False, use_fitted=False, verbose=False, refit=False, id_col="unique_id", time_col="ds", target_col="y", prediction_intervals=None, level=None, quantiles=None, h=None, **data_kwargs ) ``` Temporal Cross-Validation with core.NeuralForecast. `core.NeuralForecast`'s cross-validation efficiently fits a list of NeuralForecast models through multiple windows, in either chained or rolled manner. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | DataFrame with columns \[`unique_id`, `ds`, `y`] and exogenous variables. If None, a previously stored dataset is required. | None | | `static_df` | pandas or polars DataFrame | DataFrame with columns \[`unique_id`] and static exogenous. Defaults to None. | None | | `n_windows` | ([int](#int), None) | Number of windows used for cross validation. If None, define `test_size`. | 1 | | `step_size` | [int](#int) | Step size between each window. | 1 | | `val_size` | [int](#int) | Length of validation size. If passed, set `n_windows=None`. Defaults to 0. | 0 | | `test_size` | [int](#int) | Length of test size. If passed, set `n_windows=None`. | None | | `use_init_models` | [bool](#bool) | If True, discards any previously fitted weights and reinitializes the models from the configs passed at `NeuralForecast(__init__)`. Use this to start cross-validation from scratch. Defaults to False. | False | | `use_fitted` | [bool](#bool) | Evaluate the already-fitted model on `df` without retraining (transfer-learning cross-validation). Requires a previous `fit` call, `refit=False`, `use_init_models=False`, and `prediction_intervals=None`. Local scalers, if any, are refit per series on `df` and the fitted state (model weights, stored dataset, scalers) is restored after CV completes. Defaults to False. | False | | `verbose` | [bool](#bool) | Print processing steps. | False | | `refit` | [bool](#bool) or [int](#int) | Retrain model for each cross validation window. If False, the models are trained at the beginning and then used to predict each window. If positive int, the models are retrained every `refit` windows. | False | | `id_col` | [str](#str) | Column that identifies each serie. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. | 'y' | | `prediction_intervals` | [PredictionIntervals](#neuralforecast.utils.PredictionIntervals) | Configuration to calibrate prediction intervals (Conformal Prediction). Defaults to None. | None | | `level` | list of ints or floats | Confidence levels between 0 and 100. | None | | `quantiles` | list of floats | Alternative to level, target quantiles to predict. | None | | `h` | [int](#int) | Forecasting horizon. If None, uses the horizon of the fitted models. | None | | `data_kwargs` | [kwargs](#kwargs) | Extra arguments to be passed to the dataset within each model. | | **Returns:** | Name | Type | Description | | ---------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `fcsts_df` | pandas or polars DataFrame | DataFrame with insample `models` columns for point predictions and probabilistic predictions for all fitted `models`. | #### `NeuralForecast.predict_insample` ```python theme={null} predict_insample(step_size=1, level=None, quantiles=None) ``` Predict insample with core.NeuralForecast. `core.NeuralForecast`'s `predict_insample` uses stored fitted `models` to predict historic values of a time series from the stored dataframe. **Parameters:** | Name | Type | Description | Default | | ----------- | ----------------------------------- | -------------------------------------------------- | ----------------- | | `step_size` | [int](#int) | Step size between each window. | 1 | | `level` | list of ints or floats | Confidence levels between 0 and 100. | None | | `quantiles` | list of floats | Alternative to level, target quantiles to predict. | None | **Returns:** | Name | Type | Description | | ---------- | ------------------------------------------- | ------------------------------------------------------------ | | `fcsts_df` | [DataFrame](#pandas.DataFrame) | DataFrame with insample predictions for all fitted `models`. | #### `NeuralForecast.save` ```python theme={null} save(path, model_index=None, save_dataset=True, overwrite=False) ``` Save NeuralForecast core class. `core.NeuralForecast`'s method to save current status of models, dataset, and configuration. Note that by default the `models` are not saving training checkpoints to save disk memory, to get them change the individual model `**trainer_kwargs` to include `enable_checkpointing=True`. **Parameters:** | Name | Type | Description | Default | | -------------- | -------------------------- | -------------------------------------------------------------- | ------------------ | | `path` | [str](#str) | Directory to save current status. | *required* | | `model_index` | [list](#list) | List to specify which models from list of self.models to save. | None | | `save_dataset` | [bool](#bool) | Whether to save dataset or not. | True | | `overwrite` | [bool](#bool) | Whether to overwrite files or not. | False | #### `NeuralForecast.load` ```python theme={null} load(path, verbose=False, **kwargs) ``` Load NeuralForecast `core.NeuralForecast`'s method to load checkpoint from path. **Parameters:** | Name | Type | Description | Default | | ---------- | -------------------------- | --------------------------------------------------------------------------------- | ------------------ | | `path` | [str](#str) | Directory with stored artifacts. | *required* | | `verbose` | [bool](#bool) | Defaults to False. | False | | `**kwargs` | | Additional keyword arguments to be passed to the function `load_from_checkpoint`. | | **Returns:** | Name | Type | Description | | -------- | ------------------------------------------------------------------ | ------------------------------------ | | `result` | [NeuralForecast](#neuralforecast.core.NeuralForecast) | Instantiated `NeuralForecast` class. | # NeuralForecast Map Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/api-reference/neuralforecast_map.html > Modules of the NeuralForecast library The `neuralforecast` library provides a comprehensive set of state-of-the-art deep learning models designed to power-up time series forecasting pipelines. The library is constructed using a modular approach, where different responsibilities are isolated within specific modules. These modules include the user interface functions (`core`), data processing and loading (`tsdataset`), scalers, losses, and base classes for models. This tutorial aims to explain the library’s structure and to describe how the different modules interact with each other. ## I. Map The following diagram presents the modules of the `neuralforecast` library and their relations.
Neuralforecast map
Neuralforecast map
## II. Modules ### 1. Core (`core.py`) The `core` module acts as the primary interaction point for users of the `neuralforecast` library. It houses the `NeuralForecast` class, which incorporates a range of key user interface functions designed to simplify the process of training and forecasting models. Functions include `fit`, `predict`, `cross_validation`, and `predict_insample`, each one constructed to be intuitive and user-friendly. The design of the `NeuralForecast` class is centered around enabling users to streamline their forecasting pipelines and to comfortably train and evaluate models. ### 2. Dataset and Loader (`tsdataset.py`) The `TimeSeriesDataset` class, located within the `tsdataset` module, is responsible for the storage and preprocessing of the input time series dataset. Once the `TimeSeriesDataset` class has prepared the data, it’s then consumed by the `TimeSeriesLoader` class, which samples batches (or subsets) of the time series during the training and inference stages. ### 3. Base Model (`common`) The `common` module contains three `BaseModel` classes, which serve as the foundation for all the model structures provided in the library. These base classes allow for a level of abstraction and code-reusability in the design of the models. We currently support three type of models: * `BaseWindows`: designed for window-based models like `NBEATS` and `Transformers`. * `BaseRecurrent`: designed for recurrent models like `RNN` and `LSTM`. * `BaseMultivariate`: caters to multivariate models like `StemGNN`. ### 4. Model (`models`) The `models` module encompasses all the specific model classes available for use in the library. These include a variety of both simple and complex models such as `RNN`, `NHITS`, `LSTM`, `StemGNN`, and `TFT`. Each model in this module extends from one of the `BaseModel` classes in the `common` module. ### 5. Losses (`losses`) The `losses` module includes both `numpy` and `pytorch` losses, used for evalaution and training respectively. The module contains a wide range of losses, including `MAE`, `MSE`, `MAPE`, `HuberLoss`, among many others. ### 6. Scalers (`_scalers.py`) The `_scalers.py` module houses the `TemporalNorm` class. This class is responsible for the scaling (normalization) and de-scaling (reversing the normalization) of time series data. This step is crucial because it ensures all data fed to the model have a similar range, leading to more stable and efficient training processes. ## III. Flow The `user` first instantiates a model and the `NeuralForecast` core class. When they call the `fit` method, the following flow is executed: 1. The `fit` method instantiates a `TimeSeriesDataset` object to store and pre-process the input time series dataset, and the `TimeSeriesLoader` object to sample batches. 2. The `fit` method calls the model’s `fit` method (in the `BaseModel` class). 3. The model’s `fit` method instantiates a Pytorch-Lightning `Trainer` object, in charge of training the model. 4. The `Trainer` method samples a batch from the `TimeSeriesLoader` object, and calls the model’s `training_step` method (in the `BaseModel` class). 5. The model’s `training_step`: * Samples windows from the original batch. * Normalizes the windows with the `scaler` module. * Calls the model’s `forward` method. * Computes the loss using the `losses` module. * Returns the loss. 6. The `Trainer` object repeats step 4 and 5 until `max_steps` iterations are completed. 7. The model is fitted, and can be used for forecasting future values (with the `predict` method) or recover insample predictions (using the `predict_insample` method). ## IV. Next Steps: add your own model Congratulations! You now know the internal details of the `neuralforecast` library. With this knowledge you can easily add new models to the library, by just creating a `model` class which only requires the `init` and `forward` methods. Check our detailed guide on how to add new models! # Categorical features Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/categorical_features.html On top of continuous numerical features, NeuralForecast models also support categorical features. These features take discrete values that may change over time (e.g., day of the week, month of the year, etc.). In this tutorial, we explore how categorical features are handled in NeuralForecast and how you can incorporate them when training a model. > **Important** > > Not all models support exogenous features, either numerical or > categorical. Refer to the [model list](./overview.html) to see which > model supports what type of exogenous features. ## How it works NeuralForecast handles categorical features with learned embeddings. Internally, it: * builds a panel-wide vocabulary at `fit` time. This creates a mapping from category to index for each feature across all series. Index 0 is reserved for unseen categories during training. * routes the categorical features around the scalers. Categorical features are not scaled, unlike numerical features. * learns an embedding for each feature. One vector per category is created and is learned jointly with other continuous features. * concatenates each feature’s embeddings with the continuous features and feeds the result to the model. By using learned embeddings, models learn a rich representation of the features which usually results in better forecasts than using a traditional label encoder. Note that the current handling of static exogenous features stays unchanged; i.e., they are not embedded unless you pass them as categorical features. ### Choosing the embedding dimension Choosing the right embedding dimension determines the representational capacity of the model. There are four strategies available. Note that the cardinality is the number of unique categories for a feature. | Strategy | Formula | Character | | --------------------- | --------------------------------------- | --------------------------------------------- | | `"fastai"` (default) | `min(50, ceil(1.6 · cardinality^0.56))` | Balanced, cardinality-aware, sublinear growth | | `"sqrt"` | `min(50, ceil(sqrt(cardinality)))` | Compact; smallest for mid/high cardinality | | `"half"` | `min(50, (cardinality + 1) // 2)` | Generous for low cardinality; saturates fast | | an integer (e.g. `8`) | exactly that value | Uniform across all features | Be aware of the tradeoff when choosing the emebdding’s dimension. A larger dimension means a larger representational capacity, but it also means more parameters to train and a risk of overfitting. * “fastai” (default). It’s a well-established rule of thumb that scales nicely to large vocabularies, and rarely needs tuning. * “sqrt”. Use for high-cardinality features or limited data. It produces the most compact embeddings for mid-to-high cardinality, which means fewer parameters and stronger regularization. * “half”. Use when categories are few but highly informative and you have enough data to fit them. Tt gives low-cardinality features more capacity. Note it saturates at the cap quickly, so for high cardinality it behaves like a flat 50. * An explicit integer. Use when you want uniform, predictable sizes, want to enforce a specific dimension, or want to tune cat\_emb\_dim as a hyperparameter. ### What is currently supported This is a large change that is being implemented in multiple phases. Currently, categorical features are supported for: - univariate models * multivariate models * recurrent models ### Upcoming work All elements listed here do not support the use of categorical features: * explainability with categorical features * simulation with categorical features ## Example Let’s see a simple example of how categorical features can be incorporated in NeuralForecast ```python theme={null} import pandas as pd from neuralforecast import NeuralForecast from neuralforecast.models import NHITS from neuralforecast.utils import AirPassengersPanel ``` ```python theme={null} df = AirPassengersPanel[["unique_id", "ds", "y"]].copy() df["ds"] = pd.to_datetime(df["ds"]) ``` Now, let’s create a categorical feature called “month”. This is a known feature in the future. ```python theme={null} df["month"] = df["ds"].dt.month ``` ```python theme={null} test = df.groupby("unique_id").tail(12) train = df.drop(test.index) ``` To include categorical features, we must specify four hyperparameters: 1. `futr_exog_list` or `hist_exog_list`. These parameters take a list of features, depending on whether they are historical (we only know their past values), or future (we know their future values). They can contain both numerical and categorical features. 2. `cat_exog_list`. This parameter specifies which features are categorical. That way, we automatically resolve which features are categorical from `futr_exog_list` and `hist_exog_list`. 3. `categorical_cardinalities`. A dictionary that assigns the cardinality (the number of unique values) for each categorical feature. 4. `cat_embed_dim`. The strategy to set the dimension of the learned embeddings. In our current example, “month” is a categorical feature with known future values, so it is included in `futr_exog_list`. Since there are 12 months in a year, its cardinality is 12. Below is an example of traning a NHITS model with a categorical feature. ```python theme={null} model = NHITS( h=12, input_size=24, max_steps=500, futr_exog_list=["month"], cat_exog_list=["month"], categorical_cardinalities={"month": 12}, cat_emb_dim="fastai", ) nf = NeuralForecast(models=[model], freq="ME") nf.fit(train) ``` Once trained, the model can be used for inference. Since we specified “month” as a future exogenous feature, we must provide its values over the forecast horizon. ```python theme={null} futr = test[["unique_id", "ds", "month"]] preds = nf.predict(futr_df=futr) ``` # NeuralForecast Cross Validation API and Parameters Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/cross_validation.html Understand NeuralForecast cross validation parameters, including horizons, windows, step size, validation size, test size, and refitting behavior. > **Prerequisites** > > This Guide assumes basic familiarity with NeuralForecast. For a > minimal example visit the [Quick > Start](../getting-started/quickstart.html) To measure the performance of a forecasting model, we can assess its performance on historical data using *cross-validation*. Cross-validation is done by defining a sliding window of input data to predict the following period. We do this operation many times such that the model predicts new periods, resulting in a more robust assessment of its performance. Below, you can see an illustration of cross-validation. In this illustration, the cross-validation process generates six different forecasting periods where we can compare the model’s predictions against the actual values of the past. ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) This mimicks the process of making predictions in the future and collecting actual data to then evaluate the prediction’s accuracy. In this tutorial, we explore in detail the cross-validation function in `neuralforecast`. ## 1. Libraries Make sure to install `neuralforecast` to follow along. ```python theme={null} %%capture !pip install neuralforecast ``` ```python theme={null} import logging import matplotlib.pyplot as plt import pandas as pd from utilsforecast.plotting import plot_series from neuralforecast import NeuralForecast from neuralforecast.models import NHITS ``` ```python theme={null} logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) ``` ## 2. Read the data For this tutorial, we use part of the hourly M4 dataset. It is stored in a parquet file for efficiency. However, you can use ordinary pandas operations to read your data in other formats likes `.csv`. The input to `NeuralForecast` is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestampe ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. Depending on your internet connection, this step should take around 10 seconds. ```python theme={null} Y_df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | For simplicity, we focus on a single time series to explore the cross-validation functionality in detail. We also use only the first 700 time steps, which allows us to work with round numbers and makes the cross-validation process easier to visualize and understand. ```python theme={null} Y_df = Y_df.query("unique_id == 'H1'")[:700] Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | ```python theme={null} plot_series(Y_df) ``` ## 3. Using cross-validation ### 3.1 Using `n_windows` To use the `cross_validation` method, we can either: - Set the sizes of a validation and test set - Set a number of cross-validation windows Let’s see how it works in a minimal example. Here, we use the NHITS model and set the horizon to 100, and give an input size of 200. First, let’s use `n_windows = 4`. We also set `step_size` equal to the horizon. This parameter controls the distance between each cross-validation window. By setting it equal to the horizon, we perform *chained cross-validation* where the windows do not overlap. ```python theme={null} h = 100 nf = NeuralForecast(models=[NHITS(h=h, input_size=2*h, max_steps=500, enable_progress_bar=False, logger=False)], freq=1); cv_df = nf.cross_validation(Y_df, n_windows=4, step_size=h, verbose=0) cv_df.head() ``` ```text theme={null} Seed set to 1 ``` | | unique\_id | ds | cutoff | NHITS | y | | - | ---------- | --- | ------ | ---------- | ----- | | 0 | H1 | 301 | 300 | 490.048950 | 485.0 | | 1 | H1 | 302 | 300 | 537.713867 | 525.0 | | 2 | H1 | 303 | 300 | 612.900635 | 585.0 | | 3 | H1 | 304 | 300 | 689.346313 | 670.0 | | 4 | H1 | 305 | 300 | 760.153992 | 747.0 | ```python theme={null} cutoffs = cv_df['cutoff'].unique() plt.figure(figsize=(15,5)) plt.plot(Y_df['ds'], Y_df['y']) plt.plot(cv_df['ds'], cv_df['NHITS'], label='NHITS', ls='--') for cutoff in cutoffs: plt.axvline(x=cutoff, color='black', ls=':') plt.xlabel('Time steps') plt.ylabel('Target [H1]') plt.legend() plt.tight_layout() ``` In the figure above, we observe four cutoff points, each corresponding to a cross-validation window. Note that these windows are defined from the end of the dataset, ensuring that the model is trained on past data to predict future data. > **Important note** > > We start counting at 0, so counting from 0 to 99 results in a sequence > of 100 data points. Thus, the model is initially trained using time steps 0 to 299. Then, to make predictions, it takes time steps 100 to 299 (input size of 200) and it makes predictions for time steps 300 to 399 (horizon of 100). Then, the actual values from 200 to 399 (because our model has an `input_size` of 200) are used to generate predictions over the next window, from 400 to 499. This process is repeated until we run out of windows. ### 3.2 Using a validation and test set Instead of setting a number of windows, we can define a validation and test set. In that case, we must set `n_windows=None` ```python theme={null} cv_df_val_test = nf.cross_validation(Y_df, val_size=200, test_size=200, step_size=h, n_windows=None) ``` ```python theme={null} cutoffs = cv_df_val_test['cutoff'].unique() plt.figure(figsize=(15,5)) # Plot the original data and NHITS predictions plt.plot(Y_df['ds'], Y_df['y']) plt.plot(cv_df_val_test['ds'], cv_df_val_test['NHITS'], label='NHITS', ls='--') # Add highlighted areas for validation and test sets plt.axvspan(Y_df['ds'].iloc[300], Y_df['ds'].iloc[499], alpha=0.2, color='yellow', label='Validation Set') plt.axvspan(Y_df['ds'].iloc[500], Y_df['ds'].iloc[699], alpha=0.2, color='red', label='Test Set') # Add vertical lines for cutoffs for cutoff in cutoffs: plt.axvline(x=cutoff, color='black', ls=':') # Set labels and legend plt.xlabel('Time steps') plt.ylabel('Target [H1]') plt.legend() plt.tight_layout() plt.show() ``` Here, we predict only the test set, which corresponds to the last 200 time steps. Since the model has a forecast horizon of 100, and `step_size` is also set to 100, there are only two cross-validation windows in the test set (200/100 = 2). Thus, we only see two cutoff points. ### 3.3 Cross-validation with refit In the previous sections, we trained the model only once and predicted over many cross-validation windows. However, in real life, we often retrain our model with new observed data before making the next set of predictions. We can simulate that process using `refit=True`. That way, the model is retrained at every step in the cross-validation process. In other words, the training set is gradually expanded with new observed values and the model is retrained before making the next set of predictions. ```python theme={null} cv_df_refit = nf.cross_validation(Y_df, n_windows=4, step_size=h, refit=True) ``` ```python theme={null} cutoffs = cv_df_refit['cutoff'].unique() plt.figure(figsize=(15,5)) plt.plot(Y_df['ds'], Y_df['y']) plt.plot(cv_df_refit['ds'], cv_df_refit['NHITS'], label='NHITS', ls='--') for cutoff in cutoffs: plt.axvline(x=cutoff, color='black', ls=':') plt.xlabel('Time steps') plt.ylabel('Target [H1]') plt.legend() plt.tight_layout() ``` Notice that when we run cross-validation with `refit=True`, there were 4 training loops that were completed. This is expected because the model is now retrained with new data for each fold in the cross-validation: - fold 1: train on the first 300 steps, predict the next 100 - fold 2: train on the first 400 steps, predict the next 100 - fold 3: train on the first 500 steps, predict the next 100 - fold 4: train on the first 600 steps, predict the next 100 ### 3.4 Overlapping windows in cross-validation In the case where `step_size` is smaller than the horizon, we get overlapping windows. This means that we make predictions more than once for some time steps. This is useful to test the model over more forecast windows, and it provides a more robust evaluation, as the model is tested across different segments of the series. However, it comes with a higher computation cost, as we are making predictions more than once for some of the time steps. ```python theme={null} cv_df_refit_overlap = nf.cross_validation(Y_df, n_windows=2, step_size=50, refit=True) ``` ```python theme={null} cutoffs = cv_df_refit_overlap['cutoff'].unique() fold1 = cv_df_refit_overlap.query("cutoff==550") fold2 = cv_df_refit_overlap.query("cutoff==600") plt.figure(figsize=(15,5)) plt.plot(Y_df['ds'], Y_df['y']) plt.plot(fold1['ds'], fold1['NHITS'], label='NHITS (fold 1)', ls='--', color='blue') plt.plot(fold2['ds'], fold2['NHITS'], label='NHITS (fold 2)', ls='-.', color='red') for cutoff in cutoffs: plt.axvline(x=cutoff, color='black', ls=':') plt.xlabel('Time steps') plt.ylabel('Target [H1]') plt.xlim(500, 700) plt.legend() plt.tight_layout() ``` In the figure above, we see that our two folds overlap between time steps 601 and 650, since the step size is 50. This happens because: * fold 1: model is trained using time steps 0 to 550 and predicts 551 to 650 (h=100) * fold 2: model is trained using time steps 0 to 600 (`step_size=50`) and predicts 601 to 700 Be aware that when evaluating a model trained with overlapping cross-validation windows, some time steps have more than one prediction. This may bias your evaluation metric, as the repeated time steps are taken into account in the metric multiple times. # Exogenous Variables Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/exogenous_variables.html Exogenous variables can provide additional information to greatly improve forecasting accuracy. Some examples include price or future promotions variables for demand forecasting, and weather data for electricity load forecast. In this notebook we show an example on how to add different types of exogenous variables to NeuralForecast models for making day-ahead hourly electricity price forecasts (EPF) for France and Belgium markets. All NeuralForecast models are capable of incorporating exogenous variables to model the following conditional predictive distribution: $\mathbb{P}(\mathbf{y}_{t+1:t+H} \;|\; \mathbf{y}_{[:t]},\; \mathbf{x}^{(h)}_{[:t]},\; \mathbf{x}^{(f)}_{[:t+H]},\; \mathbf{x}^{(s)} )$ where the regressors are static exogenous $\mathbf{x}^{(s)}$, historic exogenous $\mathbf{x}^{(h)}_{[:t]}$, exogenous available at the time of the prediction $\mathbf{x}^{(f)}_{[:t+H]}$ and autoregressive features $\mathbf{y}_{[:t]}$. Depending on the [train loss](../../losses.pytorch.html), the model outputs can be point forecasts (location estimators) or uncertainty intervals (quantiles). We will show you how to include exogenous variables in the data, specify variables to a model, and produce forecasts using future exogenous variables. > **Important** > > This Guide assumes basic knowledge on the NeuralForecast library. For > a minimal example visit the [Getting > Started](../getting-started/quickstart.html) guide. You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Libraries ```python theme={null} %%capture !pip install neuralforecast ``` ## 2. Load data The `df` dataframe contains the target and exogenous variables past information to train the model. The `unique_id` column identifies the markets, `ds` contains the datestamps, and `y` the electricity price. Include both historic and future temporal variables as columns. In this example, we are adding the system load (`system_load`) as historic data. For future variables, we include a forecast of how much electricity will be produced (`gen_forecast`) and day of week (`week_day`). Both the electricity system demand and offer impact the price significantly, including these variables to the model greatly improve performance, as we demonstrate in Olivares et al. (2022). The distinction between historic and future variables will be made later as parameters of the model. ```python theme={null} import pandas as pd from utilsforecast.plotting import plot_series ``` ```python theme={null} df = pd.read_csv( 'https://datasets-nixtla.s3.amazonaws.com/EPF_FR_BE.csv', parse_dates=['ds'], ) df.head() ``` | | unique\_id | ds | y | gen\_forecast | system\_load | week\_day | | - | ---------- | ------------------- | ----- | ------------- | ------------ | --------- | | 0 | FR | 2015-01-01 00:00:00 | 53.48 | 76905.0 | 74812.0 | 3 | | 1 | FR | 2015-01-01 01:00:00 | 51.93 | 75492.0 | 71469.0 | 3 | | 2 | FR | 2015-01-01 02:00:00 | 48.76 | 74394.0 | 69642.0 | 3 | | 3 | FR | 2015-01-01 03:00:00 | 42.27 | 72639.0 | 66704.0 | 3 | | 4 | FR | 2015-01-01 04:00:00 | 38.41 | 69347.0 | 65051.0 | 3 | > **Tip** > > Calendar variables such as day of week, month, and year are very > useful to capture long seasonalities. ```python theme={null} plot_series(df) ``` Add the static variables in a separate `static_df` dataframe. In this example, we are using one-hot encoding of the electricity market. The `static_df` must include one observation (row) for each `unique_id` of the `df` dataframe, with the different statics variables as columns. ```python theme={null} static_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/EPF_FR_BE_static.csv') static_df.head() ``` | | unique\_id | market\_0 | market\_1 | | - | ---------- | --------- | --------- | | 0 | FR | 1 | 0 | | 1 | BR | 0 | 1 | ## 3. Training with exogenous variables We distinguish the exogenous variables by whether they reflect static or time-dependent aspects of the modeled data. * **Static exogenous variables**: The static exogenous variables carry time-invariant information for each time series. When the model is built with global parameters to forecast multiple time series, these variables allow sharing information within groups of time series with similar static variable levels. Examples of static variables include designators such as identifiers of regions, groups of products, etc. * **Historic exogenous variables**: This time-dependent exogenous variable is restricted to past observed values. Its predictive power depends on Granger-causality, as its past values can provide significant information about future values of the target variable $\mathbf{y}$. * **Future exogenous variables**: In contrast with historic exogenous variables, future values are available at the time of the prediction. Examples include calendar variables, weather forecasts, and known events that can cause large spikes and dips such as scheduled promotions. To add exogenous variables to the model, first specify the name of each variable from the previous dataframes to the corresponding model hyperparameter during initialization: `futr_exog_list`, `hist_exog_list`, and `stat_exog_list`. We also set `horizon` as 24 to produce the next day hourly forecasts, and set `input_size` to use the last 5 days of data as input. ```python theme={null} import logging from neuralforecast.auto import NHITS, BiTCN from neuralforecast.core import NeuralForecast ``` ```python theme={null} logging.getLogger("pytorch_lightning").setLevel(logging.WARNING) ``` ```python theme={null} horizon = 24 # day-ahead daily forecast models = [NHITS(h = horizon, max_steps=100, input_size = 5*horizon, futr_exog_list = ['gen_forecast', 'week_day'], # <- Future exogenous variables hist_exog_list = ['system_load'], # <- Historical exogenous variables stat_exog_list = ['market_0', 'market_1'], # <- Static exogenous variables scaler_type = 'robust'), BiTCN(h = horizon, input_size = 5*horizon, max_steps=100, futr_exog_list = ['gen_forecast', 'week_day'], # <- Future exogenous variables hist_exog_list = ['system_load'], # <- Historical exogenous variables stat_exog_list = ['market_0', 'market_1'], # <- Static exogenous variables scaler_type = 'robust', ), ] ``` ```text theme={null} Seed set to 1 Seed set to 1 ``` > **Tip** > > When including exogenous variables always use a scaler by setting the > `scaler_type` hyperparameter. The scaler will scale all the temporal > features: the target variable `y`, historic and future variables. > **Important** > > Make sure future and historic variables are correctly placed. Defining > historic variables as future variables will lead to data leakage. Next, pass the datasets to the `df` and `static_df` inputs of the `fit` method. > **Tip** > > You can scale static variables using the `local_static_scaler_type` > parameter when initializing a `NeuralForecast` instance. ```python theme={null} nf = NeuralForecast(models=models, freq='h') nf.fit(df=df, static_df=static_df) ``` ## 4. Forecasting with exogenous variables Before predicting the prices, we need to gather the future exogenous variables for the day we want to forecast. Define a new dataframe (`futr_df`) with the `unique_id`, `ds`, and future exogenous variables. There is no need to add the target variable `y` and historic variables as they won’t be used by the model. ```python theme={null} futr_df = pd.read_csv( 'https://datasets-nixtla.s3.amazonaws.com/EPF_FR_BE_futr.csv', parse_dates=['ds'], ) futr_df.head() ``` | | unique\_id | ds | gen\_forecast | week\_day | | - | ---------- | ------------------- | ------------- | --------- | | 0 | FR | 2016-11-01 00:00:00 | 49118.0 | 1 | | 1 | FR | 2016-11-01 01:00:00 | 47890.0 | 1 | | 2 | FR | 2016-11-01 02:00:00 | 47158.0 | 1 | | 3 | FR | 2016-11-01 03:00:00 | 45991.0 | 1 | | 4 | FR | 2016-11-01 04:00:00 | 45378.0 | 1 | > **Important** > > Make sure `futr_df` has informations for the entire forecast horizon. > In this example, we are forecasting 24 hours ahead, so `futr_df` must > have 24 rows for each time series. Finally, use the `predict` method to forecast the day-ahead prices. ```python theme={null} Y_hat_df = nf.predict(futr_df=futr_df) Y_hat_df.head() ``` ```text theme={null} Predicting: | | 0/? [00:00 In summary, to add exogenous variables to a model make sure to follow the next steps: 1. Add temporal exogenous variables as columns to the main dataframe (`df`). 2. Add static exogenous variables with the `static_df` dataframe. 3. Specify the name for each variable in the corresponding model hyperparameter. 4. If the model uses future exogenous variables, pass the future dataframe (`futr_df`) to the `predict` method. ## References * [Kin G. Olivares, Cristian Challu, Grzegorz Marcjasz, Rafał Weron, Artur Dubrawski, Neural basis expansion analysis with exogenous variables: Forecasting electricity prices with NBEATSx, International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207022000413) * [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Hyperparameter Optimization | NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/hyperparameter_tuning.html Deep-learning models are the state-of-the-art in time series forecasting. They have outperformed statistical and tree-based approaches in recent large-scale competitions, such as the M series, and are being increasingly adopted in industry. However, their performance is greatly affected by the choice of hyperparameters. Selecting the optimal configuration, a process called hyperparameter tuning, is essential to achieve the best performance. The main steps of hyperparameter tuning are: 1. Define training and validation sets. 2. Define search space. 3. Sample configurations with a search algorithm, train models, and evaluate them on the validation set. 4. Select and store the best model. With `Neuralforecast`, we automatize and simplify the hyperparameter tuning process with the `Auto` models. Every model in the library has an `Auto` version (for example, `AutoNHITS`, `AutoTFT`) which can perform automatic hyperparameter selection on default or user-defined search space. The `Auto` models can be used with two backends: Ray’s `Tune` library and `Optuna`, with a user-friendly and simplified API, with most of their capabilities. In this tutorial, we show in detail how to instantiate and train an `AutoNHITS` model with a custom search space with both `Tune` and `Optuna` backends, install and use `HYPEROPT` search algorithm, and use the model with optimal hyperparameters to forecast. You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Install `Neuralforecast` ```python theme={null} %%capture !pip install neuralforecast hyperopt ``` ## 2. Load Data In this example we will use the `AirPasengers`, a popular dataset with monthly airline passengers in the US from 1949 to 1960. Load the data, available at our `utils` methods in the required format. See [https://nixtlaverse.nixtla.io/neuralforecast/utils.html#example-data](https://nixtlaverse.nixtla.io/neuralforecast/utils.html#example-data) for more details on the data input format. ```python theme={null} import logging from neuralforecast.utils import AirPassengersDF ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) ``` ```python theme={null} Y_df = AirPassengersDF Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ----- | | 0 | 1.0 | 1949-01-31 | 112.0 | | 1 | 1.0 | 1949-02-28 | 118.0 | | 2 | 1.0 | 1949-03-31 | 132.0 | | 3 | 1.0 | 1949-04-30 | 129.0 | | 4 | 1.0 | 1949-05-31 | 121.0 | ## 3. Ray’s `Tune` backend First, we show how to use the `Tune` backend. This backend is based on Ray’s `Tune` library, which is a scalable framework for hyperparameter tuning. It is a popular library in the machine learning community, and it is used by many companies and research labs. If you plan to use the `Optuna` backend, you can skip this section. ### 3.a Define hyperparameter grid Each `Auto` model contains a default search space that was extensively tested on multiple large-scale datasets. Search spaces are specified with dictionaries, where keys corresponds to the model’s hyperparameter and the value is a `Tune` function to specify how the hyperparameter will be sampled. For example, use `randint` to sample integers uniformly, and `choice` to sample values of a list. ### 3.a.1 Default hyperparameter grid The default search space dictionary can be accessed through the `get_default_config` function of the `Auto` model. This is useful if you wish to use the default parameter configuration but want to change one or more hyperparameter spaces without changing the other default values. To extract the default config, you need to define: \* `h`: forecasting horizon. \* `backend`: backend to use. \* `n_series`: Optional, the number of unique time series, required only for Multivariate models. In this example, we will use `h=12` and we use `ray` as backend. We will use the default hyperparameter space but only change `random_seed` range and `n_pool_kernel_size`. ```python theme={null} from ray import tune from neuralforecast.auto import AutoNHITS ``` ```python theme={null} nhits_config = AutoNHITS.get_default_config(h = 12, backend="ray") # Extract the default hyperparameter settings nhits_config["random_seed"] = tune.randint(1, 10) # Random seed nhits_config["n_pool_kernel_size"] = tune.choice([[2, 2, 2], [16, 8, 1]]) # MaxPool's Kernelsize ``` ### 3.a.2 Custom hyperparameter grid More generally, users can define fully customized search spaces tailored for particular datasets and tasks, by fully specifying a hyperparameter search space dictionary. In the following example we are optimizing the `learning_rate` and two `NHITS` specific hyperparameters: `n_pool_kernel_size` and `n_freq_downsample`. Additionaly, we use the search space to modify default hyperparameters, such as `max_steps` and `val_check_steps`. ```python theme={null} nhits_config = { "max_steps": 100, # Number of SGD steps "input_size": 24, # Size of input window "learning_rate": tune.loguniform(1e-5, 1e-1), # Initial Learning rate "n_pool_kernel_size": tune.choice([[2, 2, 2], [16, 8, 1]]), # MaxPool's Kernelsize "n_freq_downsample": tune.choice([[168, 24, 1], [24, 12, 1], [1, 1, 1]]), # Interpolation expressivity ratios "val_check_steps": 50, # Compute validation every 50 steps "random_seed": tune.randint(1, 10), # Random seed } ``` > **Important** > > Configuration dictionaries are not interchangeable between models > since they have different hyperparameters. Refer to > [https://nixtlaverse.nixtla.io/neuralforecast/models.html](https://nixtlaverse.nixtla.io/neuralforecast/models.html) for a > complete list of each model’s hyperparameters. ### 3.b Instantiate `Auto` model To instantiate an `Auto` model you need to define: * `h`: forecasting horizon. * `loss`: training and validation loss from `neuralforecast.losses.pytorch`. * `config`: hyperparameter search space. If `None`, the `Auto` class will use a pre-defined suggested hyperparameter space. * `search_alg`: search algorithm (from `tune.search`), default is random search. Refer to [https://docs.ray.io/en/latest/tune/api\_docs/suggestion.html](https://docs.ray.io/en/latest/tune/api_docs/suggestion.html) for more information on the different search algorithm options. * `backend`: backend to use, default is `ray`. If `optuna`, the `Auto` class will use the `Optuna` backend. * `num_samples`: number of configurations explored. In this example we set horizon `h` as 12, use the `MAE` loss for training and validation, and use the `HYPEROPT` search algorithm. ```python theme={null} from ray.tune.search.hyperopt import HyperOptSearch from neuralforecast.losses.pytorch import MAE from neuralforecast.auto import AutoNHITS ``` > **Tip** > > You can configure different options for Ray using `ray_options`. > Specifically you can pass: - `run_config` which is forwarded to > `tune.Tuner` - `scheduler` for different schedulers - `cpus` for the > number of CPUs to use during optimization - `gpu` for the number of > GPUs to use during optimization ```python theme={null} model = AutoNHITS( h=12, loss=MAE(), config=nhits_config, search_alg=HyperOptSearch(), backend='ray', num_samples=10, ) ``` > **Tip** > > The number of samples, `num_samples`, is a crucial parameter! Larger > values will usually produce better results as we explore more > configurations in the search space, but it will increase training > times. Larger search spaces will usually require more samples. As a > general rule, we recommend setting `num_samples` higher than 20. We > set 10 in this example for demonstration purposes. ### 3.c Train model and predict with `Core` class Next, we use the `Neuralforecast` class to train the `Auto` model. In this step, `Auto` models will automatically perform hyperparameter tuning, training multiple models with different hyperparameters, producing the forecasts on the validation set, and evaluating them. The best configuration is selected based on the error on a validation set. Only the best model is stored and used during inference. ```python theme={null} from neuralforecast import NeuralForecast ``` Use the `val_size` parameter of the `fit` method to control the length of the validation set. In this case we set the validation set as twice the forecasting horizon. ```python theme={null} %%capture nf = NeuralForecast(models=[model], freq='ME') nf.fit(df=Y_df, val_size=24) ``` The results of the hyperparameter tuning are available in the `results` attribute of the `Auto` model. Use the `get_dataframe` method to get the results in a pandas dataframe. ```python theme={null} results = nf.models[0].results.get_dataframe() results.head() ``` | | loss | train\_loss | timestamp | checkpoint\_dir\_name | done | training\_iteration | trial\_id | date | time\_this\_iter\_s | time\_total\_s | ... | config/input\_size | config/learning\_rate | config/n\_pool\_kernel\_size | config/n\_freq\_downsample | config/val\_check\_steps | config/random\_seed | config/h | config/loss | config/valid\_loss | logdir | | - | --------- | ----------- | ---------- | --------------------- | ----- | ------------------- | --------- | -------------------- | ------------------- | -------------- | --- | ------------------ | --------------------- | ---------------------------- | -------------------------- | ------------------------ | ------------------- | -------- | ----------- | ------------------ | -------- | | 0 | 21.948565 | 11.748630 | 1732660404 | None | False | 2 | e684ab59 | 2024-11-26\_22-33-24 | 0.473169 | 1.742914 | ... | 24 | 0.000583 | (16, 8, 1) | (1, 1, 1) | 50 | 9 | 12 | MAE() | MAE() | e684ab59 | | 1 | 23.497557 | 13.491600 | 1732660411 | None | False | 2 | 28016d96 | 2024-11-26\_22-33-31 | 0.467711 | 1.767644 | ... | 24 | 0.000222 | (16, 8, 1) | (168, 24, 1) | 50 | 5 | 12 | MAE() | MAE() | 28016d96 | | 2 | 29.214516 | 16.968582 | 1732660419 | None | False | 2 | ded66a42 | 2024-11-26\_22-33-39 | 0.969751 | 2.623766 | ... | 24 | 0.009816 | (16, 8, 1) | (24, 12, 1) | 50 | 5 | 12 | MAE() | MAE() | ded66a42 | | 3 | 45.178616 | 28.338690 | 1732660427 | None | False | 2 | 2964d41f | 2024-11-26\_22-33-47 | 0.985556 | 2.656381 | ... | 24 | 0.012083 | (16, 8, 1) | (24, 12, 1) | 50 | 7 | 12 | MAE() | MAE() | 2964d41f | | 4 | 32.580570 | 21.667740 | 1732660434 | None | False | 2 | 766cc549 | 2024-11-26\_22-33-54 | 0.418154 | 1.465539 | ... | 24 | 0.000040 | (2, 2, 2) | (1, 1, 1) | 50 | 4 | 12 | MAE() | MAE() | 766cc549 | Next, we use the `predict` method to forecast the next 12 months using the optimal hyperparameters. ```python theme={null} Y_hat_df = nf.predict() Y_hat_df.head() ``` ```text theme={null} Predicting: | … ``` | | unique\_id | ds | AutoNHITS | | - | ---------- | ---------- | ---------- | | 0 | 1.0 | 1961-01-31 | 438.724091 | | 1 | 1.0 | 1961-02-28 | 415.593628 | | 2 | 1.0 | 1961-03-31 | 493.484894 | | 3 | 1.0 | 1961-04-30 | 493.120728 | | 4 | 1.0 | 1961-05-31 | 499.806702 | ## 4. `Optuna` backend In this section we show how to use the `Optuna` backend. `Optuna` is a lightweight and versatile platform for hyperparameter optimization. If you plan to use the `Tune` backend, you can skip this section. ### 4.a Define hyperparameter grid Each `Auto` model contains a default search space that was extensively tested on multiple large-scale datasets. Search spaces are specified with a function that returns a dictionary, where keys corresponds to the model’s hyperparameter and the value is a `suggest` function to specify how the hyperparameter will be sampled. For example, use `suggest_int` to sample integers uniformly, and `suggest_categorical` to sample values of a list. See [https://optuna.readthedocs.io/en/stable/reference/generated/optuna.trial.Trial.html](https://optuna.readthedocs.io/en/stable/reference/generated/optuna.trial.Trial.html) for more details. ### 4.a.1 Default hyperparameter grid The default search space dictionary can be accessed through the `get_default_config` function of the `Auto` model. This is useful if you wish to use the default parameter configuration but want to change one or more hyperparameter spaces without changing the other default values. To extract the default config, you need to define: \* `h`: forecasting horizon. \* `backend`: backend to use. \* `n_series`: Optional, the number of unique time series, required only for Multivariate models. In this example, we will use `h=12` and we use `optuna` as backend. We will use the default hyperparameter space but only change `random_seed` range and `n_pool_kernel_size`. ```python theme={null} import optuna ``` ```python theme={null} optuna.logging.set_verbosity(optuna.logging.WARNING) # Use this to disable training prints from optuna nhits_default_config = AutoNHITS.get_default_config(h = 12, backend="optuna") # Extract the default hyperparameter settings def config_nhits(trial): config = {**nhits_default_config(trial)} config.update({ "random_seed": trial.suggest_int("random_seed", 1, 10), "n_pool_kernel_size": trial.suggest_categorical("n_pool_kernel_size", [[2, 2, 2], [16, 8, 1]]) }) return config ``` ### 3.a.2 Custom hyperparameter grid More generally, users can define fully customized search spaces tailored for particular datasets and tasks, by fully specifying a hyperparameter search space function. In the following example we are optimizing the `learning_rate` and two `NHITS` specific hyperparameters: `n_pool_kernel_size` and `n_freq_downsample`. Additionaly, we use the search space to modify default hyperparameters, such as `max_steps` and `val_check_steps`. ```python theme={null} def config_nhits(trial): return { "max_steps": 100, # Number of SGD steps "input_size": 24, # Size of input window "learning_rate": trial.suggest_loguniform("learning_rate", 1e-5, 1e-1), # Initial Learning rate "n_pool_kernel_size": trial.suggest_categorical("n_pool_kernel_size", [[2, 2, 2], [16, 8, 1]]), # MaxPool's Kernelsize "n_freq_downsample": trial.suggest_categorical("n_freq_downsample", [[168, 24, 1], [24, 12, 1], [1, 1, 1]]), # Interpolation expressivity ratios "val_check_steps": 50, # Compute validation every 50 steps "random_seed": trial.suggest_int("random_seed", 1, 10), # Random seed } ``` ### 4.b Instantiate `Auto` model To instantiate an `Auto` model you need to define: * `h`: forecasting horizon. * `loss`: training and validation loss from `neuralforecast.losses.pytorch`. * `config`: hyperparameter search space. If `None`, the `Auto` class will use a pre-defined suggested hyperparameter space. * `search_alg`: search algorithm (from `optuna.samplers`), default is TPESampler (Tree-structured Parzen Estimator). Refer to [https://optuna.readthedocs.io/en/stable/reference/samplers/index.html](https://optuna.readthedocs.io/en/stable/reference/samplers/index.html) for more information on the different search algorithm options. * `backend`: backend to use, default is `ray`. If `optuna`, the `Auto` class will use the `Optuna` backend. * `num_samples`: number of configurations explored. > **Tip** > > You can configure different options for Optuna using `optuna_options`. > Specifically you can pass: - `study_kwargs` for > `optuna.Study.optimize` - `create_study_kwargs` for > `optuna.create_study` ```python theme={null} model = AutoNHITS( h=12, loss=MAE(), config=config_nhits, search_alg=optuna.samplers.TPESampler(seed=0), backend='optuna', num_samples=10, ) ``` > **Important** > > Configuration dictionaries and search algorithms for `Tune` and > `Optuna` are not interchangeable! Use the appropriate type of search > algorithm and custom configuration dictionary for each backend. ### 4.c Train model and predict with `Core` class Use the `val_size` parameter of the `fit` method to control the length of the validation set. In this case we set the validation set as twice the forecasting horizon. ```python theme={null} %%capture nf = NeuralForecast(models=[model], freq='ME') nf.fit(df=Y_df, val_size=24) ``` The results of the hyperparameter tuning are available in the `results` attribute of the `Auto` model. Use the `trials_dataframe` method to get the results in a pandas dataframe. ```python theme={null} results = nf.models[0].results.trials_dataframe() results.drop(columns='user_attrs_ALL_PARAMS') ``` | | number | value | datetime\_start | datetime\_complete | duration | params\_learning\_rate | params\_n\_freq\_downsample | params\_n\_pool\_kernel\_size | params\_random\_seed | user\_attrs\_METRICS | state | | - | ------ | ------------ | -------------------------- | -------------------------- | ---------------------- | ---------------------- | --------------------------- | ----------------------------- | -------------------- | --------------------------------------------------- | -------- | | 0 | 0 | 1.827570e+01 | 2024-11-26 22:34:29.382448 | 2024-11-26 22:34:30.773811 | 0 days 00:00:01.391363 | 0.001568 | \[1, 1, 1] | \[2, 2, 2] | 5 | \{'loss': tensor(18.2757), 'train\_loss': tensor... | COMPLETE | | 1 | 1 | 9.055198e+06 | 2024-11-26 22:34:30.774153 | 2024-11-26 22:34:32.090132 | 0 days 00:00:01.315979 | 0.036906 | \[168, 24, 1] | \[2, 2, 2] | 10 | \{'loss': tensor(9055198.), 'train\_loss': tenso... | COMPLETE | | 2 | 2 | 5.554298e+01 | 2024-11-26 22:34:32.090466 | 2024-11-26 22:34:33.425103 | 0 days 00:00:01.334637 | 0.000019 | \[1, 1, 1] | \[2, 2, 2] | 10 | \{'loss': tensor(55.5430), 'train\_loss': tensor... | COMPLETE | | 3 | 3 | 9.857751e+01 | 2024-11-26 22:34:33.425460 | 2024-11-26 22:34:34.962057 | 0 days 00:00:01.536597 | 0.015727 | \[24, 12, 1] | \[16, 8, 1] | 10 | \{'loss': tensor(98.5775), 'train\_loss': tensor... | COMPLETE | | 4 | 4 | 1.966841e+01 | 2024-11-26 22:34:34.962357 | 2024-11-26 22:34:36.951450 | 0 days 00:00:01.989093 | 0.001223 | \[168, 24, 1] | \[2, 2, 2] | 1 | \{'loss': tensor(19.6684), 'train\_loss': tensor... | COMPLETE | | 5 | 5 | 1.524971e+01 | 2024-11-26 22:34:36.951775 | 2024-11-26 22:34:38.280982 | 0 days 00:00:01.329207 | 0.002955 | \[168, 24, 1] | \[16, 8, 1] | 5 | \{'loss': tensor(15.2497), 'train\_loss': tensor... | COMPLETE | | 6 | 6 | 1.678810e+01 | 2024-11-26 22:34:38.281381 | 2024-11-26 22:34:39.648595 | 0 days 00:00:01.367214 | 0.006173 | \[168, 24, 1] | \[16, 8, 1] | 4 | \{'loss': tensor(16.7881), 'train\_loss': tensor... | COMPLETE | | 7 | 7 | 2.014485e+01 | 2024-11-26 22:34:39.649025 | 2024-11-26 22:34:41.075568 | 0 days 00:00:01.426543 | 0.000285 | \[168, 24, 1] | \[2, 2, 2] | 2 | \{'loss': tensor(20.1448), 'train\_loss': tensor... | COMPLETE | | 8 | 8 | 2.109382e+01 | 2024-11-26 22:34:41.075891 | 2024-11-26 22:34:42.449451 | 0 days 00:00:01.373560 | 0.004097 | \[168, 24, 1] | \[16, 8, 1] | 7 | \{'loss': tensor(21.0938), 'train\_loss': tensor... | COMPLETE | | 9 | 9 | 5.091650e+01 | 2024-11-26 22:34:42.449762 | 2024-11-26 22:34:43.804981 | 0 days 00:00:01.355219 | 0.000036 | \[1, 1, 1] | \[16, 8, 1] | 1 | \{'loss': tensor(50.9165), 'train\_loss': tensor... | COMPLETE | Next, we use the `predict` method to forecast the next 12 months using the optimal hyperparameters. ```python theme={null} Y_hat_df_optuna = nf.predict() Y_hat_df_optuna.head() ``` ```text theme={null} Predicting: | … ``` | | unique\_id | ds | AutoNHITS | | - | ---------- | ---------- | ---------- | | 0 | 1.0 | 1961-01-31 | 446.410736 | | 1 | 1.0 | 1961-02-28 | 422.048523 | | 2 | 1.0 | 1961-03-31 | 508.271515 | | 3 | 1.0 | 1961-04-30 | 496.549133 | | 4 | 1.0 | 1961-05-31 | 506.865723 | ## 5. Plots Finally, we compare the forecasts produced by the `AutoNHITS` model with both backends. ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} plot_series( Y_df, Y_hat_df.merge( Y_hat_df_optuna, on=['unique_id', 'ds'], suffixes=['_ray', '_optuna'], ), ) ``` ### References * [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) * [James Bergstra, Remi Bardenet, Yoshua Bengio, and Balazs Kegl (2011). “Algorithms for Hyper-Parameter Optimization”. In: Advances in Neural Information Processing Systems. url: https://proceedings.neurips.cc/paper/2011/file/86e8f7ab32cfd12577bc2619bc635690-Paper.pdf](https://proceedings.neurips.cc/paper/2011/file/86e8f7ab32cfd12577bc2619bc635690-Paper.pdf) * [Kirthevasan Kandasamy, Karun Raju Vysyaraju, Willie Neiswanger, Biswajit Paria, Christopher R. Collins, Jeff Schneider, Barnabas Poczos, Eric P. Xing (2019). “Tuning Hyperparameters without Grad Students: Scalable and Robust Bayesian Optimisation with Dragonfly”. Journal of Machine Learning Research. url: https://arxiv.org/abs/1903.06694](https://arxiv.org/abs/1903.06694) * [Lisha Li, Kevin Jamieson, Giulia DeSalvo, Afshin Rostamizadeh, Ameet Talwalkar (2016). “Hyperband: A Novel Bandit-Based Approach to Hyperparameter Optimization”. Journal of Machine Learning Research. url: https://arxiv.org/abs/1603.06560](https://arxiv.org/abs/1603.06560) # Optimization Objectives Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/objectives.html NeuralForecast is a highly modular framework capable of augmenting a wide variety of robust neural network architectures with different point or probability outputs as defined by their optimization objectives. ## Point losses | Scale-Dependent | Percentage-Errors | Scale-Independent | Robust | | :----------------------------------------- | :------------------------------------------- | :----------------------------------------- | :------------------------------------------------------- | | [**MAE**](../../losses.pytorch.html#mae) | [**MAPE**](../../losses.pytorch.html#mape) | [**MASE**](../../losses.pytorch.html#mase) | [**Huber**](../../losses.pytorch.html#huber-loss) | | [**MSE**](../../losses.pytorch.html#mse) | [**sMAPE**](../../losses.pytorch.html#smape) | | [**Tukey**](../../losses.pytorch.html#tukeyloss) | | [**RMSE**](../../losses.pytorch.html#rmse) | | | [**HuberMQLoss**](../../losses.pytorch.html#hubermqloss) | ## Probabilistic losses | Parametric Probabilities | Non-Parametric Probabilities | | :------------------------------------------------------------------ | :--------------------------------------------------------- | | [**Normal**](../../losses.pytorch.html#distributionloss) | [**QuantileLoss**](../../losses.pytorch.html#quantileloss) | | [**StudenT**](../../losses.pytorch.html#distributionloss) | [**MQLoss**](../../losses.pytorch.html#mqloss) | | [**Poisson**](../../losses.pytorch.html#distributionloss) | [**HuberQLoss**](../../losses.pytorch.html#huberiqloss) | | [**Negative Binomial**](../../losses.pytorch.html#distributionloss) | [**HuberMQLoss**](../../losses.pytorch.html#hubermqloss) | | [**Tweedie**](../../losses.pytorch.html#distributionloss) | [**IQLoss**](../../losses.pytorch.html#iqloss) | | [**PMM**](../../losses.pytorch.html#pmm) | [**HuberIQLoss**](../../losses.pytorch.html#huberiqloss) | | [**GMM**](../../losses.pytorch.html#gmm) | [**ISQF**](../../losses.pytorch.html#isqf) | | [**NBMM**](../../losses.pytorch.html#nbmm) | | # Forecasting Models Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/overview.html NeuralForecast currently offers the following models. | Model1 | AutoModel2 | Family3 | Univariate / Multivariate4 | Forecast Type5 | Exogenous6 | | :------------------- | :----------------------- | :----------------- | :------------------------------------ | :------------------------ | :-------------------- | | `Autoformer` | `AutoAutoformer` | Transformer | Univariate | Direct | F | | `BiTCN` | `AutoBiTCN` | CNN | Univariate | Direct | F/H/S | | `DeepAR` | `AutoDeepAR` | RNN | Univariate | Direct | F/S | | `DeepNPTS` | `AutoDeepNPTS` | MLP | Univariate | Direct | F/H/S | | `DilatedRNN` | `AutoDilatedRNN` | RNN | Univariate | Direct | F/H/S | | `FEDformer` | `AutoFEDformer` | Transformer | Univariate | Direct | F | | `GRU` | `AutoGRU` | RNN | Univariate | Both8 | F/H/S | | `HINT` | `AutoHINT` | Any7 | Both7 | Both7 | F/H/S | | `Informer` | `AutoInformer` | Transformer | Univariate | Direct | F | | `iTransformer` | `AutoiTransformer` | Transformer | Multivariate | Direct | - | | `KAN` | `AutoKAN` | KAN | Univariate | Direct | F/H/S | | `LSTM` | `AutoLSTM` | RNN | Univariate | Both8 | F/H/S | | `MLP` | `AutoMLP` | MLP | Univariate | Direct | F/H/S | | `MLPMultivariate` | `AutoMLPMultivariate` | MLP | Multivariate | Direct | F/H/S | | `NBEATS` | `AutoNBEATS` | MLP | Univariate | Direct | - | | `NBEATSx` | `AutoNBEATSx` | MLP | Univariate | Direct | F/H/S | | `NHITS` | `AutoNHITS` | MLP | Univariate | Direct | F/H/S | | `NLinear` | `AutoNLinear` | MLP | Univariate | Direct | - | | `PatchTST` | `AutoPatchTST` | Transformer | Univariate | Direct | - | | `RMoK` | `AutoRMoK` | KAN | Multivariate | Direct | - | | `RNN` | `AutoRNN` | RNN | Univariate | Both8 | F/H/S | | `SOFTS` | `AutoSOFTS` | MLP | Multivariate | Direct | - | | `SOFTSSharp` | `AutoSOFTSSharp` | MLP | Multivariate | Direct | - | | `StemGNN` | `AutoStemGNN` | GNN | Multivariate | Direct | - | | `TCN` | `AutoTCN` | CNN | Univariate | Direct | F/H/S | | `TFT` | `AutoTFT` | Transformer | Univariate | Direct | F/H/S | | `TiDE` | `AutoTiDE` | MLP | Univariate | Direct | F/H/S | | `TimeMixer` | `AutoTimeMixer` | MLP | Multivariate | Direct | - | | `TimeLLM` | - | LLM | Univariate | Direct | - | | `TimesNet` | `AutoTimesNet` | CNN | Univariate | Direct | F | | `TimeXer` | `AutoTimeXer` | Transformer | Multivariate | Direct | H/S | | `TSMixer` | `AutoTSMixer` | MLP | Multivariate | Direct | - | | `TSMixerx` | `AutoTSMixerx` | MLP | Multivariate | Direct | F/H/S | | `VanillaTransformer` | `AutoVanillaTransformer` | Transformer | Univariate | Direct | F | | `XLinear` | `AutoXLinear` | MLP | Multivariate | Direct | F/H/S | | `xLSTM` | `AutoxLSTM` | mLSTM | Univariate | Direct | F/H/S | 1. **Model**: The model name. 2. **AutoModel**: NeuralForecast offers most models also in an Auto\* version, in which the hyperparameters of the underlying model are automatically optimized and the best-performing model for a validation set is selected. The optimization methods include grid search, random search, and Bayesian optimization. 3. **Family**: The main neural network architecture underpinning the model. 4. **Univariate / Multivariate**: A multivariate model explicitly models the interactions between multiple time series in a dataset and will provide predictions for multiple time series concurrently. In contrast, a univariate model trained on multiple time series implicitly models interactions between multiple time series and provides predictions for single time series concurrently. Multivariate models are typically computationally expensive and empirically do not necessarily offer better forecasting performance compared to using a univariate model. 5. **Forecast Type**: Direct forecast models are models that produce all steps in the forecast horizon at once. In contrast, recursive forecast models predict one-step ahead, and subsequently use the prediction to compute the next step in the forecast horizon, and so forth. Direct forecast models typically suffer less from bias and variance propagation as compared to recursive forecast models, whereas recursive models can be computationally less expensive. 6. **Exogenous**: Whether the model accepts exogenous variables. This can be exogenous variables that contain information about the past and future (F), about the past only (*historical*, H), or that contain static information (*static*, S). 7. **HINT** is a modular framework that can combine any type of neural architecture with task-specialized mixture probability and advanced hierarchical reconciliation strategies. 8. Models that can produce forecasts recursively and direct. For example, the RNN model uses an RNN to encode the past sequence, and subsequently the user can choose between producing forecasts recursively using the RNN or direct using an MLP that uses the encoded sequence as input. The models feature an `recursive=False` feature that sets how they produce forecasts. # Predict Insample Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/predictInsample.html > Tutorial on how to produce insample predictions. This tutorial provides and example on how to use the `predict_insample` function of the `core` class to produce forecasts of the train and validation sets. In this example we will train the `NHITS` model on the AirPassengers data, and show how to recover the insample predictions after model is fitted. *Predict Insample*: The process of producing forecasts of the train and validation sets. *Use Cases*: \* Debugging: producing insample predictions is useful for debugging purposes. For example, to check if the model is able to fit the train set. \* Training convergence: check if the model has converged. \* Anomaly detection: insample predictions can be used to detect anomalous behavior in the train set (e.g. outliers). (Note: if a model is too flexible it might be able to perfectly forecast outliers) You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing NeuralForecast ```python theme={null} %%capture !pip install neuralforecast ``` ## 2. Loading AirPassengers Data The `core.NeuralForecast` class contains shared, `fit`, `predict` and other methods that take as inputs pandas DataFrames with columns `['unique_id', 'ds', 'y']`, where `unique_id` identifies individual time series from the dataset, `ds` is the date, and `y` is the target variable. In this example dataset consists of a set of a single series, but you can easily fit your model to larger datasets in long format. ```python theme={null} from neuralforecast.utils import AirPassengersPanel ``` ```python theme={null} Y_df = AirPassengersPanel Y_df.head() ``` | | unique\_id | ds | y | trend | y\_\[lag12] | | - | ---------- | ---------- | ----- | ----- | ----------- | | 0 | Airline1 | 1949-01-31 | 112.0 | 0 | 112.0 | | 1 | Airline1 | 1949-02-28 | 118.0 | 1 | 118.0 | | 2 | Airline1 | 1949-03-31 | 132.0 | 2 | 132.0 | | 3 | Airline1 | 1949-04-30 | 129.0 | 3 | 129.0 | | 4 | Airline1 | 1949-05-31 | 121.0 | 4 | 121.0 | ## 3. Model Training First, we train the `NHITS` models on the AirPassengers data. We will use the `fit` method of the `core` class to train the models. ```python theme={null} import logging import pandas as pd from neuralforecast import NeuralForecast from neuralforecast.models import NHITS, LSTM ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) ``` ```python theme={null} horizon = 12 # Try different hyperparameters to improve accuracy. models = [NHITS(h=horizon, # Forecast horizon input_size=2 * horizon, # Length of input sequence max_steps=100, # Number of steps to train n_freq_downsample=[2, 1, 1], # Downsampling factors for each stack output mlp_units = 3 * [[1024, 1024]], ) # Number of units in each block. ] nf = NeuralForecast(models=models, freq='ME') nf.fit(df=Y_df, val_size=horizon) ``` ## 4. Predict Insample Using the `NeuralForecast.predict_insample` method you can obtain the forecasts for the train and validation sets after the models are fitted. The function will always take the last dataset used for training in either the `fit` or `cross_validation` methods. With the `step_size` parameter you can specify the step size between consecutive windows to produce the forecasts. In this example we will set `step_size=horizon` to produce non-overlapping forecasts. The following diagram shows how the forecasts are produced based on the `step_size` parameter and `h` (horizon) of the model. In the diagram we set `step_size=2` and `h=4`. ```python theme={null} Y_hat_insample = nf.predict_insample(step_size=horizon) ``` The `predict_insample` function returns a pandas DataFrame with the following columns: \* `unique_id`: the unique identifier of the time series. \* `ds`: the datestamp of the forecast for each row. \* `cutoff`: the datestamp at which the forecast was made. \* `y`: the actual value of the target variable. \* `model_name`: the forecasted values for the models. In this case, `NHITS`. ```python theme={null} Y_hat_insample.head() ``` | | unique\_id | ds | cutoff | NHITS | y | | - | ---------- | ---------- | ---------- | -------- | ----- | | 0 | Airline1 | 1949-01-31 | 1948-12-31 | 0.064625 | 112.0 | | 1 | Airline1 | 1949-02-28 | 1948-12-31 | 0.074300 | 118.0 | | 2 | Airline1 | 1949-03-31 | 1948-12-31 | 0.133020 | 132.0 | | 3 | Airline1 | 1949-04-30 | 1948-12-31 | 0.221040 | 129.0 | | 4 | Airline1 | 1949-05-31 | 1948-12-31 | 0.176580 | 121.0 | > **Important** > > The function will produce forecasts from the first timestamp of the > time series. For these initial timestamps, the forecasts might not be > accurate given that models have very limited input information to > produce forecasts. ## 5. Plot Predictions Finally, we plot the forecasts for the train and validation sets. ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} plot_series(forecasts_df=Y_hat_insample.drop(columns='cutoff')) ``` ## 6. Insample predictions with prediction intervals We can also show insample prediction intervals for models trained with a distribution loss function. This can be achieved by simply specifying the required level in the `predict_insample` function. Note that the following settings are not yet supported: - Prediction intervals on insample predictions on models trained with conformal prediction intervals (e.g. a model trained with MAE and conformal prediction intervals); - Prediction intervals on insample predictions on multivariate models (e.g. a TSMixer model). ```python theme={null} from neuralforecast.losses.pytorch import DistributionLoss, GMM ``` ```python theme={null} horizon = 12 # Try different hyperparameters to improve accuracy. models = [ NHITS(h=horizon, input_size=2 * horizon, loss=DistributionLoss(distribution="Poisson", num_samples=50), max_steps=100, scaler_type="robust", ), LSTM(h=horizon, input_size=2 * horizon, loss=GMM(), max_steps=500, scaler_type="robust", ), ] nf = NeuralForecast(models=models, freq='ME') nf.fit(df=Y_df, val_size=horizon) Y_hat_insample = nf.predict_insample( step_size=horizon, level=[80], ) ``` ```python theme={null} plot_series(forecasts_df=Y_hat_insample.drop(columns=['cutoff']), level=[80]) ``` ## References * [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Save and Load Models Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/save_load_models.html Saving and loading trained Deep Learning models has multiple valuable uses. These models are often costly to train; storing a pre-trained model can help reduce costs as it can be loaded and reused to forecast multiple times. Moreover, it enables Transfer learning capabilities, consisting of pre-training a flexible model on a large dataset and using it later on other data with little to no training. It is one of the most outstanding 🚀 achievements in Machine Learning 🧠 and has many practical applications. In this notebook we show an example on how to save and load `NeuralForecast` models. The two methods to consider are:
1. `NeuralForecast.save`: Saves models into disk, allows save dataset and config.
2. `NeuralForecast.load`: Loads models from a given path.
> **Important** > > This Guide assumes basic knowledge on the NeuralForecast library. For > a minimal example visit the [Getting > Started](../getting-started/quickstart.html) guide. You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing NeuralForecast ```python theme={null} %%capture !pip install neuralforecast ``` ## 2. Loading AirPassengers Data For this example we will use the classical [AirPassenger Data set](https://www.kaggle.com/datasets/rakannimer/air-passengers). Import the pre-processed AirPassenger from `utils`. ```python theme={null} from neuralforecast.utils import AirPassengersDF ``` ```python theme={null} Y_df = AirPassengersDF Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ----- | | 0 | 1.0 | 1949-01-31 | 112.0 | | 1 | 1.0 | 1949-02-28 | 118.0 | | 2 | 1.0 | 1949-03-31 | 132.0 | | 3 | 1.0 | 1949-04-30 | 129.0 | | 4 | 1.0 | 1949-05-31 | 121.0 | ## 3. Model Training Next, we instantiate and train three models: `NBEATS`, `NHITS`, and `AutoMLP`. The models with their hyperparameters are defined in the `models` list. ```python theme={null} import logging from ray import tune from neuralforecast.core import NeuralForecast from neuralforecast.auto import AutoMLP from neuralforecast.models import NBEATS, NHITS ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) ``` ```python theme={null} horizon = 12 models = [NBEATS(input_size=2 * horizon, h=horizon, max_steps=50), NHITS(input_size=2 * horizon, h=horizon, max_steps=50), AutoMLP(# Ray tune explore config config=dict(max_steps=100, # Operates with steps not epochs input_size=tune.choice([3*horizon]), learning_rate=tune.choice([1e-3])), h=horizon, num_samples=1, cpus=1)] ``` ```text theme={null} Seed set to 1 Seed set to 1 ``` ```python theme={null} %%capture nf = NeuralForecast(models=models, freq='ME') nf.fit(df=Y_df) ``` Produce the forecasts with the `predict` method. ```python theme={null} Y_hat_df = nf.predict() Y_hat_df.head() ``` ```text theme={null} Predicting: | … ``` ```text theme={null} Predicting: | … ``` ```text theme={null} Predicting: | … ``` | | unique\_id | ds | NBEATS | NHITS | AutoMLP | | - | ---------- | ---------- | ---------- | ---------- | ---------- | | 0 | 1.0 | 1961-01-31 | 446.882172 | 447.219238 | 454.914154 | | 1 | 1.0 | 1961-02-28 | 465.145813 | 464.558014 | 430.188446 | | 2 | 1.0 | 1961-03-31 | 469.978424 | 474.637238 | 458.478577 | | 3 | 1.0 | 1961-04-30 | 493.650665 | 502.670349 | 477.244507 | | 4 | 1.0 | 1961-05-31 | 537.569275 | 559.405212 | 522.252991 | We plot the forecasts for each model. ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} plot_series(Y_df, Y_hat_df) ``` ## 4. Save models To save all the trained models use the `save` method. This method will save both the hyperparameters and the learnable weights (parameters). The `save` method has the following inputs: * `path`: directory where models will be saved. * `model_index`: optional list to specify which models to save. For example, to only save the `NHITS` model use `model_index=[2]`. * `overwrite`: boolean to overwrite existing files in `path`. When True, the method will only overwrite models with conflicting names. * `save_dataset`: boolean to save `Dataset` object with the dataset. ```python theme={null} nf.save(path='./checkpoints/test_run/', model_index=None, overwrite=True, save_dataset=True) ``` For each model, two files are created and stored: * `[model_name]_[suffix].ckpt`: Pytorch Lightning checkpoint file with the model parameters and hyperparameters. * `[model_name]_[suffix].pkl`: Dictionary with configuration attributes. Where `model_name` corresponds to the name of the model in lowercase (eg. `nhits`). We use a numerical suffix to distinguish multiple models of each class. In this example the names will be `automlp_0`, `nbeats_0`, and `nhits_0`. > **Important** > > The `Auto` models will be stored as their base model. For example, the > `AutoMLP` trained above is stored as an `MLP` model, with the best > hyparparameters found during tuning. ## 5. Load models Load the saved models with the `load` method, specifying the `path`, and use the new `nf2` object to produce forecasts. ```python theme={null} nf2 = NeuralForecast.load(path='./checkpoints/test_run/') Y_hat_df2 = nf2.predict() Y_hat_df2.head() ``` ```text theme={null} Seed set to 1 Seed set to 1 Seed set to 1 ``` ```text theme={null} Predicting: | … ``` ```text theme={null} Predicting: | … ``` ```text theme={null} Predicting: | … ``` | | unique\_id | ds | NHITS | NBEATS | AutoMLP | | - | ---------- | ---------- | ---------- | ---------- | ---------- | | 0 | 1.0 | 1961-01-31 | 447.219238 | 446.882172 | 454.914154 | | 1 | 1.0 | 1961-02-28 | 464.558014 | 465.145813 | 430.188446 | | 2 | 1.0 | 1961-03-31 | 474.637238 | 469.978424 | 458.478577 | | 3 | 1.0 | 1961-04-30 | 502.670349 | 493.650665 | 477.244507 | | 4 | 1.0 | 1961-05-31 | 559.405212 | 537.569275 | 522.252991 | Finally, plot the forecasts to confirm they are identical to the original forecasts. ```python theme={null} plot_series(Y_df, Y_hat_df2) ``` ## References [https://pytorch-lightning.readthedocs.io/en/stable/common/checkpointing\_basic.html](https://pytorch-lightning.readthedocs.io/en/stable/common/checkpointing_basic.html) [Oreshkin, B. N., Carpov, D., Chapados, N., & Bengio, Y. (2019). N-BEATS: Neural basis expansion analysis for interpretable time series forecasting. ICLR 2020](https://arxiv.org/abs/1905.10437) [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). N-HiTS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Time Series Scaling Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/time_series_scaling.html Scaling time series data is an important preprocessing step when using neural forecasting methods for several reasons: 1. **Convergence speed**: Neural forecasting models tend to converge faster when the features are on a similar scale. 2. **Avoiding vanishing or exploding gradients**: some architectures, such as recurrent neural networks (RNNs), are sensitive to the scale of input data. If the input values are too large, it could lead to exploding gradients, where the gradients become too large and the model becomes unstable. Conversely, very small input values could lead to vanishing gradients, where weight updates during training are negligible and the training fails to converge. 3. **Ensuring consistent scale**: Neural forecasting models have shared global parameters for the all time series of the task. In cases where time series have different scale, scaling ensures that no particular time series dominates the learning process. 4. **Improving generalization**: time series with consistent scale can lead to smoother loss surfaces. Moreover, scaling helps to homogenize the distribution of the input data, which can also improve generalization by avoiding out-of-range values. The `Neuralforecast` library integrates two types of temporal scaling: * **Time Series Scaling**: scaling each time series using all its data on the train set before start training the model. This is done by using the `local_scaler_type` parameter of the `Neuralforecast` core class. * **Window scaling (TemporalNorm)**: scaling each input window separetly for each element of the batch at every training iteration. This is done by using the `scaler_type` parameter of each model class. In this notebook, we will demonstrate how to scale the time series data with both methods on an Eletricity Price Forecasting (EPF) task. You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Install `Neuralforecast` ```python theme={null} %%capture !pip install neuralforecast !pip install hyperopt ``` ## 2. Load Data The `df` dataframe contains the target and exogenous variables past information to train the model. The `unique_id` column identifies the markets, `ds` contains the datestamps, and `y` the electricity price. For future variables, we include a forecast of how much electricity will be produced (`gen_forecast`), and day of week (`week_day`). Both the electricity system demand and offer impact the price significantly, including these variables to the model greatly improve performance, as we demonstrate in Olivares et al. (2022). The `futr_df` dataframe includes the information of the future exogenous variables for the period we want to forecast (in this case, 24 hours after the end of the train dataset `df`). ```python theme={null} import pandas as pd import matplotlib.pyplot as plt ``` ```python theme={null} df = pd.read_csv( 'https://datasets-nixtla.s3.amazonaws.com/EPF_FR_BE.csv', parse_dates=['ds'], ) futr_df = pd.read_csv( 'https://datasets-nixtla.s3.amazonaws.com/EPF_FR_BE_futr.csv', parse_dates=['ds'], ) df.head() ``` | | unique\_id | ds | y | gen\_forecast | system\_load | week\_day | | - | ---------- | ------------------- | ----- | ------------- | ------------ | --------- | | 0 | FR | 2015-01-01 00:00:00 | 53.48 | 76905.0 | 74812.0 | 3 | | 1 | FR | 2015-01-01 01:00:00 | 51.93 | 75492.0 | 71469.0 | 3 | | 2 | FR | 2015-01-01 02:00:00 | 48.76 | 74394.0 | 69642.0 | 3 | | 3 | FR | 2015-01-01 03:00:00 | 42.27 | 72639.0 | 66704.0 | 3 | | 4 | FR | 2015-01-01 04:00:00 | 38.41 | 69347.0 | 65051.0 | 3 | We can see that `y` and the exogenous variables are on largely different scales. Next, we show two methods to scale the data. ## 3. Time Series Scaling with `Neuralforecast` class One of the most widely used approches for scaling time series is to treat it as a pre-processing step, where each time series and temporal exogenous variables are scaled based on their entire information in the train set. Models are then trained on the scaled data. To simplify pipelines, we added a scaling functionality to the `Neuralforecast` class. Each time series will be scaled before training the model with either `fit` or `cross_validation`, and scaling statistics are stored. The class then uses the stored statistics to scale the forecasts back to the original scale before returning the forecasts. ### 3.a. Instantiate model and `Neuralforecast` class In this example we will use the `TimesNet` model, recently proposed in [Wu, Haixu, et al. (2022)](https://arxiv.org/abs/2210.02186). First instantiate the model with the desired parameters. ```python theme={null} import logging from neuralforecast.models import TimesNet from neuralforecast.core import NeuralForecast ``` ```python theme={null} logging.getLogger("pytorch_lightning").setLevel(logging.WARNING) ``` ```python theme={null} horizon = 24 # day-ahead daily forecast model = TimesNet(h = horizon, # Horizon input_size = 5*horizon, # Length of input window max_steps = 100, # Training iterations top_k = 3, # Number of periods (for FFT). num_kernels = 3, # Number of kernels for Inception module batch_size = 2, # Number of time series per batch windows_batch_size = 32, # Number of windows per batch learning_rate = 0.001, # Learning rate futr_exog_list = ['gen_forecast', 'week_day'], # Future exogenous variables scaler_type = None) # We use the Core scaling method ``` ```text theme={null} Seed set to 1 ``` Fit the model by instantiating a `NeuralForecast` object and using the `fit` method. The `local_scaler_type` parameter is used to specify the type of scaling to be used. In this case, we will use `standard`, which scales the data to have zero mean and unit variance.Other supported scalers are `minmax`, `robust`, `robust-iqr`, `minmax`, and `boxcox`. ```python theme={null} nf = NeuralForecast(models=[model], freq='h', local_scaler_type='standard') nf.fit(df=df) ``` ```text theme={null} Sanity Checking: | … ``` ```text theme={null} Training: | … ``` ```text theme={null} Validation: | … ``` ### 3.b Forecast and plots Finally, use the `predict` method to forecast the day-ahead prices. The `Neuralforecast` class handles the inverse normalization, forecasts are returned in the original scale. ```python theme={null} Y_hat_df = nf.predict(futr_df=futr_df) Y_hat_df.head() ``` ```text theme={null} Predicting: | … ``` | | unique\_id | ds | TimesNet | | - | ---------- | ------------------- | --------- | | 0 | BE | 2016-11-01 00:00:00 | 39.523182 | | 1 | BE | 2016-11-01 01:00:00 | 33.386608 | | 2 | BE | 2016-11-01 02:00:00 | 27.978468 | | 3 | BE | 2016-11-01 03:00:00 | 28.143955 | | 4 | BE | 2016-11-01 04:00:00 | 32.332230 | ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} plot_series(df, Y_hat_df, max_insample_length=24*5) ``` > **Important** > > The inverse scaling is performed by the `Neuralforecast` class before > returning the final forecasts. Therefore, the hyperparmater selection > with `Auto` models and validation loss for early stopping or model > selection are performed on the scaled data. Different types of scaling > with the `Neuralforecast` class can’t be automatically compared with > `Auto` models. ## 4. Temporal Window normalization during training Temporal normalization scales each instance of the batch separately at the window level. It is performed at each training iteration for each window of the batch, for both target variable and temporal exogenous covariates. For more details, see [Olivares et al. (2023)](https://arxiv.org/abs/2305.07089) and [https://nixtlaverse.nixtla.io/neuralforecast/common.scalers.html](https://nixtlaverse.nixtla.io/neuralforecast/common.scalers.html). ### 4.a. Instantiate model and `Neuralforecast` class Temporal normalization is specified by the `scaler_type` argument. Currently, it is only supported for Windows-based models (`NHITS`, `NBEATS`, `MLP`, `TimesNet`, and all Transformers). In this example, we use the `TimesNet` model and `robust` scaler, recently proposed by Wu, Haixu, et al. (2022). First instantiate the model with the desired parameters. Visit [https://nixtlaverse.nixtla.io/neuralforecast/common.scalers.html](https://nixtlaverse.nixtla.io/neuralforecast/common.scalers.html) for a complete list of supported scalers. ```python theme={null} horizon = 24 # day-ahead daily forecast model = TimesNet(h = horizon, # Horizon input_size = 5*horizon, # Length of input window max_steps = 100, # Training iterations top_k = 3, # Number of periods (for FFT). num_kernels = 3, # Number of kernels for Inception module batch_size = 2, # Number of time series per batch windows_batch_size = 32, # Number of windows per batch learning_rate = 0.001, # Learning rate futr_exog_list = ['gen_forecast','week_day'], # Future exogenous variables scaler_type = 'robust') # Robust scaling ``` ```text theme={null} Seed set to 1 ``` Fit the model by instantiating a `NeuralForecast` object and using the `fit` method. Note that `local_scaler_type` has `None` as default to avoid scaling the data before training. ```python theme={null} nf = NeuralForecast(models=[model], freq='h') nf.fit(df=df) ``` ```text theme={null} Sanity Checking: | … ``` ```text theme={null} Training: | … ``` ```text theme={null} Validation: | … ``` ### 4.b Forecast and plots Finally, use the `predict` method to forecast the day-ahead prices. The forecasts are returned in the original scale. ```python theme={null} Y_hat_df = nf.predict(futr_df=futr_df) Y_hat_df.head() ``` ```text theme={null} Predicting: | … ``` | | unique\_id | ds | TimesNet | | - | ---------- | ------------------- | --------- | | 0 | BE | 2016-11-01 00:00:00 | 37.624653 | | 1 | BE | 2016-11-01 01:00:00 | 33.069824 | | 2 | BE | 2016-11-01 02:00:00 | 30.623751 | | 3 | BE | 2016-11-01 03:00:00 | 28.773439 | | 4 | BE | 2016-11-01 04:00:00 | 30.689444 | ```python theme={null} plot_series(df, Y_hat_df, max_insample_length=24*5) ``` > **Important** > > For most applications, models with temporal normalization (section 4) > produced more accurate forecasts than time series scaling (section 3). > However, with temporal normalization models lose the information of > the relative level between different windows. In some cases this > global information within time series is crucial, for instance when an > exogenous variables contains the dosage of a medication. In these > cases, time series scaling (section 3) is preferred. ## References * [Kin G. Olivares, David Luo, Cristian Challu, Stefania La Vattiata, Max Mergenthaler, Artur Dubrawski (2023). “HINT: Hierarchical Mixture Networks For Coherent Probabilistic Forecasting”. International Conference on Machine Learning (ICML). Workshop on Structured Probabilistic Inference & Generative Modeling. Available at https://arxiv.org/abs/2305.07089.](https://arxiv.org/abs/2305.07089) * [Wu, Haixu, Tengge Hu, Yong Liu, Hang Zhou, Jianmin Wang, and Mingsheng Long. “Timesnet: Temporal 2d-variation modeling for general time series analysis.”, ICLR 2023](https://openreview.net/forum?id=ju_Uqw384Oq) # Data Requirements Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/getting-started/datarequirements.html > Dataset input requirements In this example we will go through the dataset input requirements of the `core.NeuralForecast` class. The `core.NeuralForecast` methods operate as global models that receive a set of time series rather than single series. The class uses cross-learning technique to fit flexible-shared models such as neural networks improving its generalization capabilities as shown by the M4 international forecasting competition (Smyl 2019, Semenoglou 2021). While missing values are supported, we require data to be uniformly samples. This means that each consecutive timesteps must be evenly spaced. You can run these experiments using GPU with Google Colab. Open In Colab ## Long format ### Multiple time series Store your time series in a pandas dataframe in long format, that is, each row represents an observation for a specific series and timestamp. Let’s see an example using the `datasetsforecast` library. `Y_df = pd.concat( [series1, series2, ...])` ```python theme={null} %%capture !pip install datasetsforecast ``` ```python theme={null} import pandas as pd from datasetsforecast.m3 import M3 ``` ```python theme={null} Y_df, *_ = M3.load('./data', group='Yearly') ``` ```python theme={null} Y_df.groupby('unique_id').head(2) ``` | | unique\_id | ds | y | | ----- | ---------- | ---------- | ------- | | 0 | Y1 | 1975-12-31 | 940.66 | | 1 | Y1 | 1976-12-31 | 1084.86 | | 20 | Y10 | 1975-12-31 | 2160.04 | | 21 | Y10 | 1976-12-31 | 2553.48 | | 40 | Y100 | 1975-12-31 | 1424.70 | | ... | ... | ... | ... | | 18260 | Y97 | 1976-12-31 | 1618.91 | | 18279 | Y98 | 1975-12-31 | 1164.97 | | 18280 | Y98 | 1976-12-31 | 1277.87 | | 18299 | Y99 | 1975-12-31 | 1870.00 | | 18300 | Y99 | 1976-12-31 | 1307.20 | `Y_df` is a dataframe with three columns: `unique_id` with a unique identifier for each time series, a column `ds` with the datestamp and a column `y` with the values of the series. ### Single time series If you have only one time series, you have to include the `unique_id` column. Consider, for example, the [AirPassengers](https://github.com/Nixtla/transfer-learning-time-series/blob/main/datasets/air_passengers.csv) dataset. ```python theme={null} Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/air_passengers.csv') Y_df ``` | | timestamp | value | | --- | ---------- | ----- | | 0 | 1949-01-01 | 112 | | 1 | 1949-02-01 | 118 | | 2 | 1949-03-01 | 132 | | 3 | 1949-04-01 | 129 | | 4 | 1949-05-01 | 121 | | ... | ... | ... | | 139 | 1960-08-01 | 606 | | 140 | 1960-09-01 | 508 | | 141 | 1960-10-01 | 461 | | 142 | 1960-11-01 | 390 | | 143 | 1960-12-01 | 432 | In this example `Y_df` only contains two columns: `timestamp`, and `value`. To use `NeuralForecast` we have to include the `unique_id` column and rename the previous ones. ```python theme={null} Y_df['unique_id'] = 1. # We can add an integer as identifier Y_df = Y_df.rename(columns={'timestamp': 'ds', 'value': 'y'}) Y_df = Y_df[['unique_id', 'ds', 'y']] Y_df ``` | | unique\_id | ds | y | | --- | ---------- | ---------- | --- | | 0 | 1.0 | 1949-01-01 | 112 | | 1 | 1.0 | 1949-02-01 | 118 | | 2 | 1.0 | 1949-03-01 | 132 | | 3 | 1.0 | 1949-04-01 | 129 | | 4 | 1.0 | 1949-05-01 | 121 | | ... | ... | ... | ... | | 139 | 1.0 | 1960-08-01 | 606 | | 140 | 1.0 | 1960-09-01 | 508 | | 141 | 1.0 | 1960-10-01 | 461 | | 142 | 1.0 | 1960-11-01 | 390 | | 143 | 1.0 | 1960-12-01 | 432 | ## Missing values Missing values are supported as long as you provide the `available_mask` column to signal if the value is observed or not. We recommend using a finite placeholder for unobserved values instead of `NaN` to avoid issues during training. Here’s an example of a input dataset with missing values for `neuralforecast`. ```python theme={null} df = pd.DataFrame({ "unique_id": ["A", "A", "A", "A", "A"], "ds": pd.date_range("2024-01-01", periods=5, freq="D"), "y": [10.0, 12.0, 0.0, 15.0, 16.0], "available_mask": [1, 1, 0, 1, 1], }) df.head() ``` | | unique\_id | ds | y | available\_mask | | - | ---------- | ---------- | ---- | --------------- | | 0 | A | 2024-01-01 | 10.0 | 1 | | 1 | A | 2024-01-02 | 12.0 | 1 | | 2 | A | 2024-01-03 | 0.0 | 0 | | 3 | A | 2024-01-04 | 15.0 | 1 | | 4 | A | 2024-01-05 | 16.0 | 1 | ## References * [Slawek Smyl. (2019). “A hybrid method of exponential smoothing and recurrent networks for time series forecasting”. International Journal of Forecasting.](https://www.sciencedirect.com/science/article/pii/S0169207019301153) * [Artemios-Anargyros Semenoglou, Evangelos Spiliotis, Spyros Makridakis, and Vassilios Assimakopoulos. (2021). Investigating the accuracy of cross-learning time series forecasting methods”. International Journal of Forecasting.](https://www.sciencedirect.com/science/article/pii/S0169207020301850) # Installation Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/getting-started/installation.html > Install NeuralForecast with pip or conda You can install the *released version* of `NeuralForecast` from the [Python package index](https://pypi.org) with: ```shell theme={null} pip install neuralforecast ``` or ```shell theme={null} conda install -c conda-forge neuralforecast ``` > **Tip** > > Neural Forecasting methods profit from using GPU computation. Be sure > to have Cuda installed. > **Warning** > > We are constantly updating neuralforecast, so we suggest fixing the > version to avoid issues. `pip install neuralforecast=="1.0.0"` > **Tip** > > We recommend installing your libraries inside a python virtual or > [conda > environment](https://docs.conda.io/projects/conda/en/latest/user-guide/install/macos.html). ## Extras You can use the following extras to add optional functionality: * distributed training with spark: `pip install neuralforecast[spark]` * saving and loading from S3: `pip install neuralforecast[aws]` #### Use our env (optional) If you don’t have a Conda environment and need tools like Numba, Pandas, NumPy, Jupyter, Tune, and Nbdev you can use ours by following these steps: 1. Clone the NeuralForecast repo: ```bash theme={null} $ git clone https://github.com/Nixtla/neuralforecast.git && cd neuralforecast ``` 1. Create the environment using [astral’s uv](https://github.com/astral-sh/uv): ```bash theme={null} $ pip install uv $ uv venv --python 3.12 ``` 1. Activate the environment: * On Linux/MacOS: ```bash theme={null} $ source .venv/bin/activate ``` * On Windows: ```bash theme={null} $ .\.venv\Scripts\activate ``` 1. Install NeuralForecast Dev ```bash theme={null} $ uv pip install -e ".[dev]" --torch-backend cpu # for cpu backend $ uv pip install -e ".[dev]" --torch-backend cu118 # for CUDA 11.8 PyTorch backend ``` # About NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/getting-started/introduction.html > **NeuralForecast** offers a large collection of neural forecasting > models focused on their usability, and robustness. The models range > from classic networks like `MLP`, `RNN`s to novel proven contributions > like `NBEATS`, `NHITS`, `TFT` and other architectures. ## 🎊 Features * **Exogenous Variables**: Static, historic and future exogenous support. * **Forecast Interpretability**: Plot trend, seasonality and exogenous for `NBEATS`, `NHITS`, and `TFT` models. Compute feature attributions across all models with `IntegratedGradients` or `ShapleyValueSampling` methods. * **Probabilistic Forecasting**: Simple model adapters for quantile losses and parametric distributions. * **Train and Evaluation Losses** Scale-dependent, percentage and scale independent errors, and parametric likelihoods. * **Automatic Model Selection** Parallelized automatic hyperparameter tuning, that efficiently searches best validation configuration. * **Simple Interface** Unified SKLearn Interface for `StatsForecast` and `MLForecast` compatibility. * **Model Collection**: Out of the box implementation of `MLP`, `LSTM`, `RNN`, `TCN`, `DilatedRNN`, `NBEATS`, `NHITS`, `Informer`, `TFT`, `PatchTST`, `VanillaTransformer`, `StemGNN` and `HINT`. See the entire [collection here](../capabilities/overview.html). ## Why? There is a shared belief in Neural forecasting methods’ capacity to improve our pipeline’s accuracy and efficiency. Unfortunately, available implementations and published research are yet to realize neural networks’ potential. They are hard to use and continuously fail to improve over statistical methods while being computationally prohibitive. For this reason, we created `NeuralForecast`, a library favoring proven accurate and efficient models focusing on their usability. ## 💻 Installation ### PyPI You can install `NeuralForecast`’s *released version* from the Python package index [pip](https://pypi.org/project/neuralforecast/) with: ```python theme={null} pip install neuralforecast ``` (Installing inside a python virtualenvironment or a conda environment is recommended.) ### Conda Also you can install `NeuralForecast`’s *released version* from [conda](https://anaconda.org/conda-forge/neuralforecast) with: ```python theme={null} conda install -c conda-forge neuralforecast ``` (Installing inside a python virtualenvironment or a conda environment is recommended.) ### Dev Mode If you want to make some modifications to the code and see the effects in real time (without reinstalling), follow the steps below: ```bash theme={null} git clone https://github.com/Nixtla/neuralforecast.git cd neuralforecast pip install -e . ``` ## How to Use ```python theme={null} import logging import pandas as pd from utilsforecast.plotting import plot_series from neuralforecast import NeuralForecast from neuralforecast.models import NBEATS, NHITS from neuralforecast.utils import AirPassengersDF ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) ``` ```python theme={null} # Split data and declare panel dataset Y_df = AirPassengersDF Y_train_df = Y_df[Y_df.ds<='1959-12-31'] # 132 train Y_test_df = Y_df[Y_df.ds>'1959-12-31'] # 12 test # Fit and predict with NBEATS and NHITS models horizon = len(Y_test_df) models = [NBEATS(input_size=2 * horizon, h=horizon, max_steps=100, enable_progress_bar=False), NHITS(input_size=2 * horizon, h=horizon, max_steps=100, enable_progress_bar=False)] nf = NeuralForecast(models=models, freq='ME') nf.fit(df=Y_train_df) Y_hat_df = nf.predict() # Plot predictions plot_series(Y_train_df, Y_hat_df) ``` ```text theme={null} Seed set to 1 Seed set to 1 ``` ## 🙏 How to Cite If you enjoy or benefit from using these Python implementations, a citation to the repository will be greatly appreciated. ```text theme={null} @misc{olivares2022library_neuralforecast, author={Kin G. Olivares and Cristian Challú and Federico Garza and Max Mergenthaler Canseco and Artur Dubrawski}, title = {{NeuralForecast}: User friendly state-of-the-art neural forecasting models.}, year={2022}, howpublished={{PyCon} Salt Lake City, Utah, US 2022}, url={https://github.com/Nixtla/neuralforecast} } ``` # Quickstart Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/getting-started/quickstart.html > Fit an LSTM and NHITS model This notebook provides an example on how to start using the main functionalities of the NeuralForecast library. The `NeuralForecast` class allows users to easily interact with `NeuralForecast.models` PyTorch models. In this example we will forecast AirPassengers data with a classic `LSTM` and the recent `NHITS` models. The full list of available models is available [here](../capabilities/overview.html). You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing NeuralForecast ```python theme={null} %%capture !pip install neuralforecast ``` ## 2. Loading AirPassengers Data The `core.NeuralForecast` class contains shared, `fit`, `predict` and other methods that take as inputs pandas DataFrames with columns `['unique_id', 'ds', 'y']`, where `unique_id` identifies individual time series from the dataset, `ds` is the date, and `y` is the target variable. In this example dataset consists of a set of a single series, but you can easily fit your model to larger datasets in long format. ```python theme={null} from neuralforecast.utils import AirPassengersDF ``` ```python theme={null} Y_df = AirPassengersDF Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ----- | | 0 | 1.0 | 1949-01-31 | 112.0 | | 1 | 1.0 | 1949-02-28 | 118.0 | | 2 | 1.0 | 1949-03-31 | 132.0 | | 3 | 1.0 | 1949-04-30 | 129.0 | | 4 | 1.0 | 1949-05-31 | 121.0 | > **Important** > > DataFrames must include all `['unique_id', 'ds', 'y']` columns. Make > sure `y` column does not have missing or non-numeric values. ## 3. Model Training ### Fit the models Using the `NeuralForecast.fit` method you can train a set of models to your dataset. You can define the forecasting `horizon` (12 in this example), and modify the hyperparameters of the model. For example, for the `LSTM` we changed the default hidden size for both encoder and decoders. ```python theme={null} import logging from neuralforecast import NeuralForecast from neuralforecast.models import LSTM, NHITS, RNN ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) ``` ```python theme={null} %%capture horizon = 12 # Try different hyperparmeters to improve accuracy. models = [LSTM(input_size=2 * horizon, h=horizon, # Forecast horizon max_steps=500, # Number of steps to train scaler_type='standard', # Type of scaler to normalize data encoder_hidden_size=64, # Defines the size of the hidden state of the LSTM decoder_hidden_size=64,), # Defines the number of hidden units of each layer of the MLP decoder NHITS(h=horizon, # Forecast horizon input_size=2 * horizon, # Length of input sequence max_steps=100, # Number of steps to train n_freq_downsample=[2, 1, 1]) # Downsampling factors for each stack output ] nf = NeuralForecast(models=models, freq='ME') nf.fit(df=Y_df) ``` > **Tip** > > The performance of Deep Learning models can be very sensitive to the > choice of hyperparameters. Tuning the correct hyperparameters is an > important step to obtain the best forecasts. The `Auto` version of > these models, `AutoLSTM` and `AutoNHITS`, already perform > hyperparameter selection automatically. ### Predict using the fitted models Using the `NeuralForecast.predict` method you can obtain the `h` forecasts after the training data `Y_df`. ```python theme={null} Y_hat_df = nf.predict() ``` The `NeuralForecast.predict` method returns a DataFrame with the forecasts for each `unique_id`, `ds`, and model. ```python theme={null} Y_hat_df = Y_hat_df Y_hat_df.head() ``` | | unique\_id | ds | LSTM | NHITS | | - | ---------- | ---------- | ---------- | ---------- | | 0 | 1.0 | 1961-01-31 | 445.602112 | 447.531281 | | 1 | 1.0 | 1961-02-28 | 431.253510 | 439.081024 | | 2 | 1.0 | 1961-03-31 | 456.301270 | 481.924194 | | 3 | 1.0 | 1961-04-30 | 508.149750 | 501.501343 | | 4 | 1.0 | 1961-05-31 | 524.903870 | 514.664551 | ## 4. Plot Predictions Finally, we plot the forecasts of both models against the real values. ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} plot_series(Y_df, Y_hat_df) ``` > **Tip** > > For this guide we are using a simple `LSTM` model. More recent models, > such as `TSMixer`, `TFT` and `NHITS` achieve better accuracy than > `LSTM` in most settings. The full list of available models is > available [here](../capabilities/overview.html). ## References * [Boris N. Oreshkin, Dmitri Carpov, Nicolas Chapados, Yoshua Bengio (2020). “N-BEATS: Neural basis expansion analysis for interpretable time series forecasting”. International Conference on Learning Representations.](https://arxiv.org/abs/1905.10437)
* [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Adding Models to NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/adding_models.html > Tutorial on how to add new models to NeuralForecast > **Prerequisites** > > This Guide assumes advanced familiarity with NeuralForecast. > > We highly recommend reading first the Getting Started and the > NeuralForecast Map tutorials! > > Additionally, refer to the [CONTRIBUTING > guide](https://github.com/Nixtla/neuralforecast/blob/main/CONTRIBUTING.md) > for the basics of how to contribute to NeuralForecast. ## Introduction This tutorial is aimed at contributors who want to add a new model to the NeuralForecast library. The library’s existing modules handle optimization, training, selection, and evaluation of deep learning models. The `core` class simplifies building entire pipelines, both for industry and academia, on any dataset, with user-friendly methods such as `fit` and `predict`. Adding a new model to NeuralForecast is simpler than building a new PyTorch model from scratch. You only need to write the forward method. **It has the following additional advantages:** * Existing modules in NeuralForecast already implement the essential training and evaluating aspects for deep learning models. * Integrated with PyTorch-Lightning and Tune libraries for efficient optimization and distributed computation. * The `BaseModel` classes provide common optimization components, such as early stopping and learning rate schedulers. * Automatic performance tests are scheduled on Github to ensure quality standards. * Users can easily compare the performance and computation of the new model with existing models. * Opportunity for exposure to a large community of users and contributors. ### Example: simplified MLP model We will present the tutorial following an example on how to add a simplified version of the current `MLP` model, which does not include exogenous covariates. At a given timestamp $t$, the `MLP` model will forecast the next $h$ values of the univariate target time, $Y_{t+1:t+h}$, using as inputs the last $L$ historical values, given by $Y_{t-L:t}$. The following figure presents a diagram of the model.
Figure 1. Three layer MLP with autoregresive inputs.
Figure 1. Three layer MLP with autoregresive inputs.
## 0. Preliminaries Follow our tutorial on contributing [here](https://github.com/Nixtla/neuralforecast/blob/main/CONTRIBUTING.md) to set up your development environment. Here is a short list of the most important steps: 1. Create a fork of the `neuralforecast` library. 2. Clone the fork to your computer. 3. Set an environment with the `neuralforecast` library, core dependencies, and `nbdev` package to code your model in an interactive notebook. ## 1. Inherit the Base Class (`BaseModel`) The library contains a base model class: `BaseModel`. Using class attributes we can make this model recurrent or not, or multivariate or univariate, or allow the use of exogenous inputs. ### a. Sampling process During training, the base class receives a sample of time series of the dataset from the `TimeSeriesLoader` module. The `BaseModel` models will sample individual windows of size `input_size+h`, starting from random timestamps. ### b. `BaseModel`’ hyperparameters Get familiar with the hyperparameters specified in the base class, including `h` (horizon), `input_size`, and optimization hyperparameters such as `learning_rate`, `max_steps`, among others. The following list presents the hyperparameters related to the sampling of windows: * `h` (h): number of future values to predict. * `input_size` (L): number of historic values to use as input for the model. * `batch_size` (bs): number of time series sampled by the loader during training. * `valid_batch_size` (v\_bs): number of time series sampled by the loader during inference (validation and test). * `windows_batch_size` (w\_bs): number of individual windows sampled during training (from the previous time series) to form the batch. * `inference_windows_batch_size` (i\_bs): number of individual windows sampled during inference to form each batch. Used to control the GPU memory. ### c. Input and Output batch shapes The `forward` method receives a batch of data in a dictionary with the following keys: * `insample_y`: historic values of the time series. * `insample_mask`: mask indicating the available values of the time series (1 if available, 0 if missing). * `futr_exog`: future exogenous covariates (if any). * `hist_exog`: historic exogenous covariates (if any). * `stat_exog`: static exogenous covariates (if any). The following table presents the shape for each tensor if the attribute `MULTIVARIATE = False` is set: | `tensor` | `BaseModel` | | --------------- | ------------------------ | | `insample_y` | (`w_bs`, `L`, `1`) | | `insample_mask` | (`w_bs`, `L`) | | `futr_exog` | (`w_bs`, `L`+`h`, `n_f`) | | `hist_exog` | (`w_bs`, `L`, `n_h`) | | `stat_exog` | (`w_bs`,`n_s`) | The `forward` function should return a single tensor with the forecasts of the next `h` timestamps for each window. Use the attributes of the `loss` class to automatically parse the output to the correct shape (see the example below). > **Tip** > > Since we are using `nbdev`, you can easily add prints to the code and > see the shapes of the tensors during training. ### d. `BaseModel`’ methods The `BaseModel` class contains several common methods for all windows-based models, simplifying the development of new models by preventing code duplication. The most important methods of the class are: * `_create_windows`: parses the time series from the `TimeSeriesLoader` into individual windows of size `input_size+h`. * `_normalization`: normalizes each window based on the `scaler` type. * `_inv_normalization`: inverse normalization of the forecasts. * `training_step`: training step of the model, called by PyTorch-Lightning’s `Trainer` class during training (`fit` method). * `validation_step`: validation step of the model, called by PyTorch-Lightning’s `Trainer` class during validation. * `predict_step`: prediction step of the model, called by PyTorch-Lightning’s `Trainer` class during inference (`predict` method). ## 2. Create the model file and class Once familiar with the basics of the `BaseModel` class, the next step is creating your particular model. The main steps are: 1. Create the file in the `nbs` folder ([https://github.com/Nixtla/neuralforecast/tree/main/nbs](https://github.com/Nixtla/neuralforecast/tree/main/nbs)). It should be named `models.YOUR_MODEL_NAME.ipynb`. 2. Add the header of the `nbdev` file. 3. Import libraries in the file. 4. Define the `__init__` method with the model’s inherited and particular hyperparameters and instantiate the architecture. 5. Set the following model attributes: * `EXOGENOUS_FUTR`: if the model can handle future exogenous variables (True) or not (False) * `EXOGENOUS_HIST`: if the model can handle historical exogenous variables (True) or not (False) * `EXOGENOUS_STAT`: if the model can handle static exogenous variables (True) or not (False) * `MULTIVARIATE`: If the model produces multivariate forecasts (True) or univariate (False) * `RECURRENT`: If the model produces forecasts recursively (True) or direct (False) 6. Define the `forward` method, which receives the input batch dictionary and returns the forecast. ### a. Model class First, add the following **two cells** on top of the `nbdev` file. ```python theme={null} #| default_exp models.mlp ``` > **Important** > > Change `mlp` to your model’s name, using lowercase and underscores. > When you later run `nbdev_export`, it will create a `YOUR_MODEL.py` > script in the `neuralforecast/models/` directory. ```python theme={null} #| echo: false %load_ext autoreload %autoreload 2 ``` Next, add the dependencies of the model. ```python theme={null} #| export from typing import Optional import torch import torch.nn as nn from neuralforecast.losses.pytorch import MAE from neuralforecast.common._base_model import BaseModel ``` > **Tip** > > Don’t forget to add the `#| export` tag on this cell. Next, create the class with the `init` and `forward` methods. The following example shows the example for the simplified `MLP` model. We explain important details after the code. ```python theme={null} #| export class MLP(BaseModel): # <<---- Inherits from BaseModel # Set class attributes to determine this model's characteristics EXOGENOUS_FUTR = False # If the model can handle future exogenous variables EXOGENOUS_HIST = False # If the model can handle historical exogenous variables EXOGENOUS_STAT = False # If the model can handle static exogenous variables MULTIVARIATE = False # If the model produces multivariate forecasts (True) or univariate (False) RECURRENT = False # If the model produces forecasts recursively (True) or direct (False) def __init__(self, # Inhereted hyperparameters with no defaults h, input_size, # Model specific hyperparameters num_layers = 2, hidden_size = 1024, # Inhereted hyperparameters with defaults futr_exog_list = None, hist_exog_list = None, stat_exog_list = None, exclude_insample_y = False, loss = MAE(), valid_loss = None, max_steps: int = 1000, learning_rate: float = 1e-3, num_lr_decays: int = -1, early_stop_patience_steps: int =-1, val_check_steps: int = 100, batch_size: int = 32, valid_batch_size: Optional[int] = None, windows_batch_size = 1024, inference_windows_batch_size = -1, start_padding_enabled = False, step_size: int = 1, scaler_type: str = 'identity', random_seed: int = 1, drop_last_loader: bool = False, optimizer = None, optimizer_kwargs = None, lr_scheduler = None, lr_scheduler_kwargs = None, dataloader_kwargs = None, **trainer_kwargs): # Inherit BaseWindows class super(MLP, self).__init__(h=h, input_size=input_size, ..., # <<--- Add all inhereted hyperparameters random_seed=random_seed, **trainer_kwargs) # Architecture self.num_layers = num_layers self.hidden_size = hidden_size # MultiLayer Perceptron layers = [nn.Linear(in_features=input_size, out_features=hidden_size)] layers += [nn.ReLU()] for i in range(num_layers - 1): layers += [nn.Linear(in_features=hidden_size, out_features=hidden_size)] layers += [nn.ReLU()] self.mlp = nn.ModuleList(layers) # Adapter with Loss dependent dimensions self.out = nn.Linear(in_features=hidden_size, out_features=h * self.loss.outputsize_multiplier) ## <<--- Use outputsize_multiplier to adjust output size def forward(self, windows_batch): # <<--- Receives windows_batch dictionary # Parse windows_batch insample_y = windows_batch['insample_y'].squeeze(-1) # [batch_size, input_size] # MLP hidden = self.mlp(insample_y) # [batch_size, hidden_size] y_pred = self.out(hidden) # [batch_size, h * n_outputs] # Reshape y_pred = y_pred.reshape(batch_size, self.h, self.loss.outputsize_multiplier) # [batch_size, h, n_outputs] return y_pred ``` > **Tip** > > * Don’t forget to add the `#| export` tag on each cell. > * Larger architectures, such as Transformers, might require > splitting the `forward` by using intermediate functions. #### Important notes The base class has many hyperparameters, and models must have default values for all of them (except `h` and `input_size`). If you are unsure of what default value to use, we recommend copying the default values from existing models for most optimization and sampling hyperparameters. You can change the default values later at any time. The `reshape` method at the end of the `forward` step is used to adjust the output shape. The `loss` class contains an `outputsize_multiplier` attribute to automatically adjust the output size of the forecast depending on the `loss`. For example, for the Multi-quantile loss (`MQLoss`), the model needs to output each quantile for each horizon. ### b. Tests and documentation `nbdev` allows for testing and documenting the model during the development process. It allows users to iterate the development within the notebook, testing the code in the same environment. Refer to the existing MLP [implementation](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/models/mlp.py), [tests](https://github.com/Nixtla/neuralforecast/blob/main/tests/test_models/test_mlp.py), and [usage documentation](https://nixtlaverse.nixtla.io/neuralforecast/models.mlp.html) for a complete example of the model development process. ### c. Export the new model to the library with `nbdev` Following the CONTRIBUTING guide, the next step is to export the new model from the development notebook to the `neuralforecast` folder with the actual scripts. To export the model, run `nbdev_export` in your terminal. You should see a new file with your model in the `neuralforecast/models/` folder. ## 3. Core class and additional files Finally, add the model to the `core` class and additional files: 1. Manually add the model in the following [init file](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/models/__init__.py). 2. Add the model to the `core` class, using the source file [here](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/core.py): 1. Add the model to the initial model list: ```python theme={null} from neuralforecast.models import ( GRU, LSTM, RNN, TCN, DilatedRNN, MLP, NHITS, NBEATS, NBEATSx, TFT, VanillaTransformer, Informer, Autoformer, FEDformer, StemGNN, PatchTST ) ``` 1. Add the model to the `MODEL_FILENAME_DICT` dictionary (used for the `save` and `load` functions). ## 4. Add the model to the documentation It’s important to add the model to the necessary documentation pages so that everyone can find the documentation: 1. Add the model to the [model overview table](https://github.com/Nixtla/neuralforecast/blob/main/nbs/docs/capabilities/overview.ipynb). 2. Add the model to the [documentation navigation](https://github.com/Nixtla/neuralforecast/blob/main/docs/mintlify/docs.json) for the API reference. 3. Confirm the page is present in [docs.json](https://github.com/Nixtla/neuralforecast/blob/main/docs/mintlify/docs.json). ## 5. Upload to GitHub Congratulations! The model is ready to be used in the library following the steps above. Follow our contributing guide’s final steps to upload the model to GitHub: [here](https://github.com/Nixtla/neuralforecast/blob/main/CONTRIBUTING.md). One of the maintainers will review the PR, request changes if necessary, and merge it into the library. ## Quick Checklist * Get familiar with the `BaseModel` class hyperparameters and input/output shapes of the `forward` method. * Create the notebook with your model class in the `nbs` folder: `models.YOUR_MODEL_NAME.ipynb` * Add the header and import libraries. * Implement `init` and `forward` methods and set the class attributes. * Export model with `nbdev_export`. * Add model to this [init file](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/models/__init__.py). * Add the model to the `core` class [here](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/core.py). * Follow the CONTRIBUTING guide to create the PR to upload the model. # Statistical, Machine Learning and Neural Forecasting methods| NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/comparing_methods.html > In this notebook, you will make forecasts for the M5 dataset choosing > the best model for each time series using cross validation. Statistical, Machine Learning, and Neural Forecasting Methods In this tutorial, we will explore the process of forecasting on the M5 dataset by utilizing the most suitable model for each time series. We’ll accomplish this through an essential technique known as cross-validation. This approach helps us in estimating the predictive performance of our models, and in selecting the model that yields the best performance for each time series. The M5 dataset comprises of hierarchical sales data, spanning five years, from Walmart. The aim is to forecast daily sales for the next 28 days. The dataset is broken down into the 50 states of America, with 10 stores in each state. In the realm of time series forecasting and analysis, one of the more complex tasks is identifying the model that is optimally suited for a specific group of series. Quite often, this selection process leans heavily on intuition, which may not necessarily align with the empirical reality of our dataset. In this tutorial, we aim to provide a more structured, data-driven approach to model selection for different groups of series within the M5 benchmark dataset. This dataset, well-known in the field of forecasting, allows us to showcase the versatility and power of our methodology. We will train an assortment of models from various forecasting paradigms: *[StatsForecast](https://github.com/Nixtla/statsforecast)* * Baseline models: These models are simple yet often highly effective for providing an initial perspective on the forecasting problem. We will use `SeasonalNaive` and `HistoricAverage` models for this category. * Intermittent models: For series with sporadic, non-continuous demand, we will utilize models like `CrostonOptimized`, `IMAPA`, and `ADIDA`. These models are particularly suited for handling zero-inflated series. * State Space Models: These are statistical models that use mathematical descriptions of a system to make predictions. The `AutoETS` model from the statsforecast library falls under this category. *[MLForecast](https://github.com/Nixtla/mlforecast)* Machine Learning: Leveraging ML models like `LightGBM`, `XGBoost`, and `LinearRegression` can be advantageous due to their capacity to uncover intricate patterns in data. We’ll use the MLForecast library for this purpose. *[NeuralForecast](https://github.com/Nixtla/neuralforecast)* Deep Learning: DL models, such as Transformers (`AutoTFT`) and Neural Networks (`AutoNHITS`), allow us to handle complex non-linear dependencies in time series data. We’ll utilize the NeuralForecast library for these models. Using the Nixtla suite of libraries, we’ll be able to drive our model selection process with data, ensuring we utilize the most suitable models for specific groups of series in our dataset. Outline: * Reading Data: In this initial step, we load our dataset into memory, making it available for our subsequent analysis and forecasting. It is important to understand the structure and nuances of the dataset at this stage. * Forecasting Using Statistical and Deep Learning Methods: We apply a wide range of forecasting methods from basic statistical techniques to advanced deep learning models. The aim is to generate predictions for the next 28 days based on our dataset. * Model Performance Evaluation on Different Windows: We assess the performance of our models on distinct windows. * Selecting the Best Model for a Group of Series: Using the performance evaluation, we identify the optimal model for each group of series. This step ensures that the chosen model is tailored to the unique characteristics of each group. * Filtering the Best Possible Forecast: Finally, we filter the forecasts generated by our chosen models to obtain the most promising predictions. This is our final output and represents the best possible forecast for each series according to our models. > **Warning** > > This tutorial was originally executed using a `c5d.24xlarge` EC2 > instance. ## Installing Libraries ```python theme={null} %%capture !pip install statsforecast mlforecast neuralforecast pyarrow ``` ## Download and prepare data The example uses the [M5 dataset](https://github.com/Mcompetitions/M5-methods/blob/master/M5-Competitors-Guide.pdf). It consists of `30,490` bottom time series. ```python theme={null} import pandas as pd ``` ```python theme={null} # Load the training target dataset from the provided URL Y_df = pd.read_parquet('https://m5-benchmarks.s3.amazonaws.com/data/train/target.parquet') # Rename columns to match the Nixtlaverse's expectations # The 'item_id' becomes 'unique_id' representing the unique identifier of the time series # The 'timestamp' becomes 'ds' representing the time stamp of the data points # The 'demand' becomes 'y' representing the target variable we want to forecast Y_df = Y_df.rename( columns={ 'item_id': 'unique_id', 'timestamp': 'ds', 'demand': 'y' } ) # Convert the 'ds' column to datetime format to ensure proper handling of date-related operations in subsequent steps Y_df['ds'] = pd.to_datetime(Y_df['ds']) ``` For simplicity sake we will keep just one category ```python theme={null} Y_df = Y_df.query('unique_id.str.startswith("FOODS_3")').reset_index(drop=True) Y_df['unique_id'] = Y_df['unique_id'].astype(str) ``` # Basic Plotting Plot some series using the `plot_series` function from the `utilsforecast` library. This method prints 8 random series from the dataset and is useful for basic EDA. ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} # Feature: plot random series for EDA plot_series(Y_df) ``` ```python theme={null} # Feature: plot groups of series for EDA plot_series(Y_df, ids=["FOODS_3_432_TX_2"]) ``` # Create forecasts with Stats, Ml and Neural methods. ## StatsForecast `StatsForecast` is a comprehensive library providing a suite of popular univariate time series forecasting models, all designed with a focus on high performance and scalability. Here’s what makes StatsForecast a powerful tool for time series forecasting: * **Collection of Local Models**: StatsForecast provides a diverse collection of local models that can be applied to each time series individually, allowing us to capture unique patterns within each series. * **Simplicity**: With StatsForecast, training, forecasting, and backtesting multiple models become a straightforward process, requiring only a few lines of code. This simplicity makes it a convenient tool for both beginners and experienced practitioners. * **Optimized for Speed**: The implementation of the models in StatsForecast is optimized for speed, ensuring that large-scale computations are performed efficiently, thereby reducing the overall time for model training and prediction. * **Horizontal Scalability**: One of the distinguishing features of StatsForecast is its ability to scale horizontally. It is compatible with distributed computing frameworks such as Spark, Dask, and Ray. This feature allows it to handle large datasets by distributing the computations across multiple nodes in a cluster, making it a go-to solution for large-scale time series forecasting tasks. `StatsForecast` receives a list of models to fit each time series. Since we are dealing with Daily data, it would be benefitial to use 7 as seasonality. ```python theme={null} from statsforecast import StatsForecast # Import necessary models from the statsforecast library from statsforecast.models import ( # SeasonalNaive: A model that uses the previous season's data as the forecast SeasonalNaive, # Naive: A simple model that uses the last observed value as the forecast Naive, # HistoricAverage: This model uses the average of all historical data as the forecast HistoricAverage, # CrostonOptimized: A model specifically designed for intermittent demand forecasting CrostonOptimized, # ADIDA: Adaptive combination of Intermittent Demand Approaches, a model designed for intermittent demand ADIDA, # IMAPA: Intermittent Multiplicative AutoRegressive Average, a model for intermittent series that incorporates autocorrelation IMAPA, # AutoETS: Automated Exponential Smoothing model that automatically selects the best Exponential Smoothing model based on AIC AutoETS ) ``` We fit the models by instantiating a new StatsForecast object with the following parameters: * `models`: a list of models. Select the models you want from models and import them. * `freq`: a string indicating the frequency of the data. (See panda’s available frequencies.) * `n_jobs`: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model`: a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} horizon = 28 models = [ SeasonalNaive(season_length=7), Naive(), HistoricAverage(), CrostonOptimized(), ADIDA(), IMAPA(), AutoETS(season_length=7) ] ``` ```python theme={null} # Instantiate the StatsForecast class sf = StatsForecast( models=models, # A list of models to be used for forecasting freq='D', # The frequency of the time series data (in this case, 'D' stands for daily frequency) n_jobs=-1, # The number of CPU cores to use for parallel execution (-1 means use all available cores) verbose=True, # Show progress ) ``` The forecast method produces predictions for the next `h` periods. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values. This block of code times how long it takes to run the forecasting function of the StatsForecast class, which predicts the next 28 days (h=28). The time is calculated in minutes and printed out at the end. ```python theme={null} from time import time # Get the current time before forecasting starts, this will be used to measure the execution time init = time() # Call the forecast method of the StatsForecast instance to predict the next 28 days (h=28) fcst_df = sf.forecast(df=Y_df, h=28) # Get the current time after the forecasting ends end = time() # Calculate and print the total time taken for the forecasting in minutes print(f'Forecast Minutes: {(end - init) / 60}') ``` ```text theme={null} Forecast: 0%| | 0/2000 [Elapsed: 00:00] ``` ```text theme={null} Forecast Minutes: 4.009805858135223 ``` ```python theme={null} fcst_df.head() ``` | | unique\_id | ds | SeasonalNaive | Naive | HistoricAverage | CrostonOptimized | ADIDA | IMAPA | AutoETS | | - | -------------------- | ---------- | ------------- | ----- | --------------- | ---------------- | -------- | -------- | -------- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-05-23 | 1.0 | 2.0 | 0.448738 | 0.345192 | 0.345477 | 0.347249 | 0.381414 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-05-24 | 0.0 | 2.0 | 0.448738 | 0.345192 | 0.345477 | 0.347249 | 0.286933 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-05-25 | 0.0 | 2.0 | 0.448738 | 0.345192 | 0.345477 | 0.347249 | 0.334987 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-05-26 | 1.0 | 2.0 | 0.448738 | 0.345192 | 0.345477 | 0.347249 | 0.186851 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-05-27 | 0.0 | 2.0 | 0.448738 | 0.345192 | 0.345477 | 0.347249 | 0.308112 | ## MLForecast `MLForecast` is a powerful library that provides automated feature creation for time series forecasting, facilitating the use of global machine learning models. It is designed for high performance and scalability. Key features of MLForecast include: * **Support for sklearn models**: MLForecast is compatible with models that follow the scikit-learn API. This makes it highly flexible and allows it to seamlessly integrate with a wide variety of machine learning algorithms. * **Simplicity**: With MLForecast, the tasks of training, forecasting, and backtesting models can be accomplished in just a few lines of code. This streamlined simplicity makes it user-friendly for practitioners at all levels of expertise. * **Optimized for speed:** MLForecast is engineered to execute tasks rapidly, which is crucial when handling large datasets and complex models. * **Horizontal Scalability:** MLForecast is capable of horizontal scaling using distributed computing frameworks such as Spark, Dask, and Ray. This feature enables it to efficiently process massive datasets by distributing the computations across multiple nodes in a cluster, making it ideal for large-scale time series forecasting tasks. ```python theme={null} from mlforecast import MLForecast from mlforecast.lag_transforms import ExpandingMean from mlforecast.target_transforms import Differences from mlforecast.utils import PredictionIntervals ``` ```python theme={null} %%capture !pip install lightgbm xgboost ``` ```python theme={null} # Import the necessary models from various libraries # LGBMRegressor: A gradient boosting framework that uses tree-based learning algorithms from the LightGBM library from lightgbm import LGBMRegressor # XGBRegressor: A gradient boosting regressor model from the XGBoost library from xgboost import XGBRegressor # LinearRegression: A simple linear regression model from the scikit-learn library from sklearn.linear_model import LinearRegression ``` To use `MLForecast` for time series forecasting, we instantiate a new `MLForecast` object and provide it with various parameters to tailor the modeling process to our specific needs: * `models`: This parameter accepts a list of machine learning models you wish to use for forecasting. You can import your preferred models from scikit-learn, lightgbm and xgboost. * `freq`: This is a string indicating the frequency of your data (hourly, daily, weekly, etc.). The specific format of this string should align with pandas’ recognized frequency strings. * `target_transforms`: These are transformations applied to the target variable before model training and after model prediction. This can be useful when working with data that may benefit from transformations, such as log-transforms for highly skewed data. * `lags`: This parameter accepts specific lag values to be used as regressors. Lags represent how many steps back in time you want to look when creating features for your model. For example, if you want to use the previous day’s data as a feature for predicting today’s value, you would specify a lag of 1. * `lags_transforms`: These are specific transformations for each lag. This allows you to apply transformations to your lagged features. * `date_features`: This parameter specifies date-related features to be used as regressors. For instance, you might want to include the day of the week or the month as a feature in your model. * `num_threads`: This parameter controls the number of threads to use for parallelizing feature creation, helping to speed up this process when working with large datasets. All these settings are passed to the `MLForecast` constructor. Once the `MLForecast` object is initialized with these settings, we call its `fit` method and pass the historical data frame as the argument. The `fit` method trains the models on the provided historical data, readying them for future forecasting tasks. ```python theme={null} # Instantiate the MLForecast object mlf = MLForecast( models=[LGBMRegressor(verbosity=-1), XGBRegressor(), LinearRegression()], # List of models for forecasting: LightGBM, XGBoost and Linear Regression freq='D', # Frequency of the data - 'D' for daily frequency lags=list(range(1, 7)), # Specific lags to use as regressors: 1 to 6 days lag_transforms = { 1: [ExpandingMean()], # Apply expanding mean transformation to the lag of 1 day }, date_features=['year', 'month', 'day', 'dayofweek', 'quarter', 'week'], # Date features to use as regressors ) ``` Just call the `fit` models to train the select models. In this case we are generating conformal prediction intervals. ```python theme={null} # Start the timer to calculate the time taken for fitting the models init = time() # Fit the MLForecast models to the data mlf.fit(Y_df) # Calculate the end time after fitting the models end = time() # Print the time taken to fit the MLForecast models, in minutes print(f'MLForecast Minutes: {(end - init) / 60}') ``` ```text theme={null} MLForecast Minutes: 0.5360581119855244 ``` After that, just call `predict` to generate forecasts. ```python theme={null} fcst_mlf_df = mlf.predict(28) ``` ```python theme={null} fcst_mlf_df.head() ``` | | unique\_id | ds | LGBMRegressor | XGBRegressor | LinearRegression | | - | -------------------- | ---------- | ------------- | ------------ | ---------------- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-05-23 | 0.549520 | 0.560123 | 0.332693 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-05-24 | 0.553196 | 0.369337 | 0.055071 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-05-25 | 0.599668 | 0.374338 | 0.127144 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-05-26 | 0.638097 | 0.327176 | 0.101624 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-05-27 | 0.763305 | 0.331631 | 0.269863 | ## NeuralForecast `NeuralForecast` is a robust collection of neural forecasting models that focuses on usability and performance. It includes a variety of model architectures, from classic networks such as Multilayer Perceptrons (MLP) and Recurrent Neural Networks (RNN) to novel contributions like N-BEATS, N-HITS, Temporal Fusion Transformers (TFT), and more. Key features of `NeuralForecast` include: * A broad collection of global models. Out of the box implementation of MLP, LSTM, RNN, TCN, DilatedRNN, NBEATS, NHITS, ESRNN, TFT, Informer, PatchTST and HINT. * A simple and intuitive interface that allows training, forecasting, and backtesting of various models in a few lines of code. * Support for GPU acceleration to improve computational speed. This machine doesn’t have GPU, but Google Colabs offers some for free. Using [Colab’s GPU to train NeuralForecast](https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/intermittent_data.html). ```python theme={null} # Read the results from Colab fcst_nf_df = pd.read_parquet('https://m5-benchmarks.s3.amazonaws.com/data/forecast-nf.parquet') ``` ```python theme={null} fcst_nf_df.head() ``` | | unique\_id | ds | AutoNHITS | AutoNHITS-lo-90 | AutoNHITS-hi-90 | AutoTFT | AutoTFT-lo-90 | AutoTFT-hi-90 | | - | -------------------- | ---------- | --------- | --------------- | --------------- | ------- | ------------- | ------------- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-05-23 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-05-24 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-05-25 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 1.0 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-05-26 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-05-27 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | ```python theme={null} # Merge the forecasts from StatsForecast and NeuralForecast fcst_df = fcst_df.merge(fcst_nf_df, how='left', on=['unique_id', 'ds']) # Merge the forecasts from MLForecast into the combined forecast dataframe fcst_df = fcst_df.merge(fcst_mlf_df, how='left', on=['unique_id', 'ds']) ``` ```python theme={null} fcst_df.head() ``` | | unique\_id | ds | SeasonalNaive | Naive | HistoricAverage | CrostonOptimized | ADIDA | IMAPA | AutoETS | AutoNHITS | AutoNHITS-lo-90 | AutoNHITS-hi-90 | AutoTFT | AutoTFT-lo-90 | AutoTFT-hi-90 | LGBMRegressor | XGBRegressor | LinearRegression | | - | -------------------- | ---------- | ------------- | ----- | --------------- | ---------------- | -------- | -------- | -------- | --------- | --------------- | --------------- | ------- | ------------- | ------------- | ------------- | ------------ | ---------------- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-05-23 | 1.0 | 2.0 | 0.448738 | 0.345192 | 0.345477 | 0.347249 | 0.381414 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | 0.549520 | 0.560123 | 0.332693 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-05-24 | 0.0 | 2.0 | 0.448738 | 0.345192 | 0.345477 | 0.347249 | 0.286933 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | 0.553196 | 0.369337 | 0.055071 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-05-25 | 0.0 | 2.0 | 0.448738 | 0.345192 | 0.345477 | 0.347249 | 0.334987 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 1.0 | 0.599668 | 0.374338 | 0.127144 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-05-26 | 1.0 | 2.0 | 0.448738 | 0.345192 | 0.345477 | 0.347249 | 0.186851 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | 0.638097 | 0.327176 | 0.101624 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-05-27 | 0.0 | 2.0 | 0.448738 | 0.345192 | 0.345477 | 0.347249 | 0.308112 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | 0.763305 | 0.331631 | 0.269863 | ## Forecast plots ```python theme={null} plot_series(Y_df, fcst_df, max_insample_length=28 * 3) ``` Use the plot function to explore models and ID’s ```python theme={null} plot_series( Y_df, fcst_df, max_insample_length=28 * 3, models=['CrostonOptimized', 'AutoNHITS', 'SeasonalNaive', 'LGBMRegressor'], ) ``` # Validate Model’s Performance The three libraries - `StatsForecast`, `MLForecast`, and `NeuralForecast` - offer out-of-the-box cross-validation capabilities specifically designed for time series. This allows us to evaluate the model’s performance using historical data to obtain an unbiased assessment of how well each model is likely to perform on unseen data. ## Cross Validation in StatsForecast The `cross_validation` method from the `StatsForecast` class accepts the following arguments: * `df`: A DataFrame representing the training data. * `h` (int): The forecast horizon, represented as the number of steps into the future that we wish to predict. For example, if we’re forecasting hourly data, `h=24` would represent a 24-hour forecast. * `step_size` (int): The step size between each cross-validation window. This parameter determines how often we want to run the forecasting process. * `n_windows` (int): The number of windows used for cross validation. This parameter defines how many past forecasting processes we want to evaluate. These parameters allow us to control the extent and granularity of our cross-validation process. By tuning these settings, we can balance between computational cost and the thoroughness of the cross-validation. ```python theme={null} sf.verbose = False init = time() cv_df = sf.cross_validation(df=Y_df, h=horizon, n_windows=3, step_size=horizon) end = time() print(f'CV Minutes: {(end - init) / 60}') ``` ```text theme={null} CV Minutes: 10.829525109132131 ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id` series identifier * `ds`: datestamp or temporal index * `cutoff`: the last datestamp or temporal index for the n\_windows. If n\_windows=1, then one unique cuttoff value, if n\_windows=2 then two unique cutoff values. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_df.head() ``` | | unique\_id | ds | cutoff | y | SeasonalNaive | Naive | HistoricAverage | CrostonOptimized | ADIDA | IMAPA | AutoETS | | - | -------------------- | ---------- | ---------- | --- | ------------- | ----- | --------------- | ---------------- | -------- | -------- | -------- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-02-29 | 2016-02-28 | 0.0 | 2.0 | 0.0 | 0.449111 | 0.618472 | 0.618375 | 0.617998 | 0.655286 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-03-01 | 2016-02-28 | 1.0 | 0.0 | 0.0 | 0.449111 | 0.618472 | 0.618375 | 0.617998 | 0.568595 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-03-02 | 2016-02-28 | 1.0 | 0.0 | 0.0 | 0.449111 | 0.618472 | 0.618375 | 0.617998 | 0.618805 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-03-03 | 2016-02-28 | 0.0 | 1.0 | 0.0 | 0.449111 | 0.618472 | 0.618375 | 0.617998 | 0.455891 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-03-04 | 2016-02-28 | 0.0 | 1.0 | 0.0 | 0.449111 | 0.618472 | 0.618375 | 0.617998 | 0.591197 | ## MLForecast The `cross_validation` method from the `MLForecast` class takes the following arguments. * `df`: training data frame * `h` (int): represents the steps into the future that are being forecasted. In this case, 24 hours ahead. * `step_size` (int): step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows` (int): number of windows used for cross-validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} init = time() cv_mlf_df = mlf.cross_validation( df=Y_df, h=horizon, n_windows=3, ) end = time() print(f'CV Minutes: {(end - init) / 60}') ``` ```text theme={null} CV Minutes: 1.6215598344802857 ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id` series identifier * `ds`: datestamp or temporal index * `cutoff`: the last datestamp or temporal index for the n\_windows. If n\_windows=1, then one unique cuttoff value, if n\_windows=2 then two unique cutoff values. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_mlf_df.head() ``` | | unique\_id | ds | cutoff | y | LGBMRegressor | XGBRegressor | LinearRegression | | - | -------------------- | ---------- | ---------- | --- | ------------- | ------------ | ---------------- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-02-29 | 2016-02-28 | 0.0 | 0.435674 | 0.556261 | -0.353077 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-03-01 | 2016-02-28 | 1.0 | 0.639676 | 0.625807 | -0.088985 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-03-02 | 2016-02-28 | 1.0 | 0.792989 | 0.659651 | 0.217697 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-03-03 | 2016-02-28 | 0.0 | 0.806868 | 0.535121 | 0.438713 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-03-04 | 2016-02-28 | 0.0 | 0.829106 | 0.313354 | 0.637066 | ## NeuralForecast This machine doesn’t have GPU, but Google Colabs offers some for free. Using [Colab’s GPU to train NeuralForecast](https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/intermittent_data.html). ```python theme={null} cv_nf_df = pd.read_parquet('https://m5-benchmarks.s3.amazonaws.com/data/cross-validation-nf.parquet') ``` ```python theme={null} cv_nf_df.head() ``` | | unique\_id | ds | cutoff | AutoNHITS | AutoNHITS-lo-90 | AutoNHITS-hi-90 | AutoTFT | AutoTFT-lo-90 | AutoTFT-hi-90 | y | | - | -------------------- | ---------- | ---------- | --------- | --------------- | --------------- | ------- | ------------- | ------------- | --- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-02-29 | 2016-02-28 | 0.0 | 0.0 | 2.0 | 1.0 | 0.0 | 2.0 | 0.0 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-03-01 | 2016-02-28 | 0.0 | 0.0 | 2.0 | 1.0 | 0.0 | 2.0 | 1.0 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-03-02 | 2016-02-28 | 0.0 | 0.0 | 2.0 | 1.0 | 0.0 | 2.0 | 1.0 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-03-03 | 2016-02-28 | 0.0 | 0.0 | 2.0 | 1.0 | 0.0 | 2.0 | 0.0 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-03-04 | 2016-02-28 | 0.0 | 0.0 | 2.0 | 1.0 | 0.0 | 2.0 | 0.0 | ## Merge cross validation forecasts ```python theme={null} cv_df = cv_df.merge(cv_nf_df.drop(columns=['y']), how='left', on=['unique_id', 'ds', 'cutoff']) cv_df = cv_df.merge(cv_mlf_df.drop(columns=['y']), how='left', on=['unique_id', 'ds', 'cutoff']) ``` ## Plots CV ```python theme={null} cutoffs = cv_df['cutoff'].unique() ``` ```python theme={null} for cutoff in cutoffs: display( plot_series( Y_df, cv_df.query('cutoff == @cutoff').drop(columns=['y', 'cutoff']), max_insample_length=28 * 5, ids=['FOODS_3_001_CA_1'], ) ) ``` ### Aggregate Demand ```python theme={null} agg_cv_df = cv_df.loc[:,~cv_df.columns.str.contains('hi|lo')].groupby(['ds', 'cutoff']).sum(numeric_only=True).reset_index() agg_cv_df.insert(0, 'unique_id', 'agg_demand') ``` ```python theme={null} agg_Y_df = Y_df.groupby(['ds']).sum(numeric_only=True).reset_index() agg_Y_df.insert(0, 'unique_id', 'agg_demand') ``` ```python theme={null} for cutoff in cutoffs: display( plot_series( agg_Y_df, agg_cv_df.query('cutoff == @cutoff').drop(columns=['y', 'cutoff']), max_insample_length=28 * 5, ) ) ``` ## Evaluation per series and CV window In this section, we will evaluate the performance of each model for each time series. ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import mse, mae, smape ``` ```python theme={null} evaluation_df = evaluate(cv_df.drop(columns='cutoff'), metrics=[mse, mae, smape]) evaluation_df ``` | | unique\_id | metric | SeasonalNaive | Naive | HistoricAverage | CrostonOptimized | ADIDA | IMAPA | AutoETS | AutoNHITS | AutoTFT | LGBMRegressor | XGBRegressor | LinearRegression | | ----- | -------------------- | ------ | ------------- | -------- | --------------- | ---------------- | -------- | -------- | -------- | --------- | -------- | ------------- | ------------ | ---------------- | | 0 | FOODS\_3\_001\_CA\_1 | mse | 1.250000 | 0.892857 | 0.485182 | 0.507957 | 0.509299 | 0.516988 | 0.494235 | 0.630952 | 0.571429 | 0.648962 | 0.584722 | 0.529400 | | 1 | FOODS\_3\_001\_CA\_2 | mse | 6.273809 | 3.773809 | 3.477309 | 3.412580 | 3.432295 | 3.474050 | 3.426468 | 4.550595 | 3.607143 | 3.423646 | 3.856465 | 3.773264 | | 2 | FOODS\_3\_001\_CA\_3 | mse | 5.880952 | 4.357143 | 5.016396 | 4.173154 | 4.160645 | 4.176733 | 4.145148 | 4.005952 | 4.372024 | 4.928764 | 6.937792 | 5.317195 | | 3 | FOODS\_3\_001\_CA\_4 | mse | 1.071429 | 0.476190 | 0.402938 | 0.382559 | 0.380783 | 0.380877 | 0.380872 | 0.476190 | 0.476190 | 0.664270 | 0.424068 | 0.637221 | | 4 | FOODS\_3\_001\_TX\_1 | mse | 0.047619 | 0.047619 | 0.238824 | 0.261356 | 0.047619 | 0.047619 | 0.077575 | 0.047619 | 0.047619 | 0.718796 | 0.063564 | 0.187810 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 24685 | FOODS\_3\_827\_TX\_2 | smape | 0.083333 | 0.035714 | 0.989540 | 0.996362 | 0.987395 | 0.982847 | 0.981537 | 0.323810 | 0.335714 | 0.976356 | 0.994702 | 0.985058 | | 24686 | FOODS\_3\_827\_TX\_3 | smape | 0.708532 | 0.681495 | 0.662490 | 0.653057 | 0.655810 | 0.660161 | 0.649180 | 0.683947 | 0.712121 | 0.639518 | 0.856866 | 0.686547 | | 24687 | FOODS\_3\_827\_WI\_1 | smape | 0.608722 | 0.694328 | 0.470570 | 0.470846 | 0.480032 | 0.480032 | 0.466956 | 0.486852 | 0.475980 | 0.472336 | 0.484906 | 0.492277 | | 24688 | FOODS\_3\_827\_WI\_2 | smape | 0.531777 | 0.398156 | 0.433577 | 0.387718 | 0.388827 | 0.389371 | 0.389888 | 0.393774 | 0.374640 | 0.413559 | 0.430893 | 0.399131 | | 24689 | FOODS\_3\_827\_WI\_3 | smape | 0.643689 | 0.680178 | 0.588031 | 0.589143 | 0.599820 | 0.628673 | 0.591437 | 0.558201 | 0.567460 | 0.589870 | 0.698798 | 0.627255 | ```python theme={null} by_metric = evaluation_df.groupby('metric').mean(numeric_only=True) by_metric ``` | | SeasonalNaive | Naive | HistoricAverage | CrostonOptimized | ADIDA | IMAPA | AutoETS | AutoNHITS | AutoTFT | LGBMRegressor | XGBRegressor | LinearRegression | | ------ | ------------- | --------- | --------------- | ---------------- | --------- | --------- | --------- | --------- | --------- | ------------- | ------------ | ---------------- | | metric | | | | | | | | | | | | | | mae | 1.775415 | 2.045906 | 1.749080 | 1.634791 | 1.542097 | 1.543745 | 1.511545 | 1.438250 | 1.497647 | 1.697947 | 1.552061 | 1.592978 | | mse | 14.265773 | 20.453325 | 12.938136 | 11.484233 | 11.090195 | 11.094446 | 10.351927 | 9.606913 | 10.721251 | 10.502289 | 11.565916 | 10.830894 | | smape | 0.436414 | 0.446430 | 0.616884 | 0.613219 | 0.618910 | 0.619313 | 0.620084 | 0.400770 | 0.411018 | 0.579856 | 0.693615 | 0.641515 | Best models by metric ```python theme={null} by_metric.idxmin(axis=1) ``` ```text theme={null} metric mae AutoNHITS mse AutoNHITS smape AutoNHITS dtype: object ``` ### Distribution of errors ```python theme={null} %%capture !pip install seaborn ``` ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns ``` ```python theme={null} evaluation_df_long = pd.melt(evaluation_df, id_vars=['unique_id', 'metric'], var_name='model', value_name='error') ``` #### SMAPE ```python theme={null} sns.violinplot(evaluation_df_long.query('metric=="smape"'), x='error', y='model'); ``` ### Choose models for groups of series Feature: * A unified dataframe with forecasts for all different models * Easy Ensamble * E.g. Average predictions * Or MinMax (Choosing is ensembling) ```python theme={null} # Choose the best model for each time series, metric, and cross validation window evaluation_df['best_model'] = evaluation_df.idxmin(axis=1, numeric_only=True) # count how many times a model wins per metric and cross validation window count_best_model = evaluation_df.groupby(['metric', 'best_model']).size().rename('n').to_frame().reset_index() # plot results sns.barplot(count_best_model, x='n', y='best_model', hue='metric') ``` ### Et pluribus unum: an inclusive forecasting Pie. ```python theme={null} # For the mse, calculate how many times a model wins eval_series_df = evaluation_df.query('metric == "mse"').groupby(['unique_id']).mean(numeric_only=True) eval_series_df['best_model'] = eval_series_df.idxmin(axis=1) counts_series = eval_series_df.value_counts('best_model') plt.pie(counts_series, labels=counts_series.index, autopct='%.0f%%') plt.show() ``` ```python theme={null} plot_series( Y_df, cv_df.drop(columns=['cutoff', 'y']), max_insample_length=28 * 6, models=['AutoNHITS'], ) ``` # Choose Forecasting method for different groups of series ```python theme={null} # Merge the best model per time series dataframe # and filter the forecasts based on that dataframe # for each time series fcst_df = pd.melt(fcst_df.set_index('unique_id'), id_vars=['ds'], var_name='model', value_name='forecast', ignore_index=False) fcst_df = fcst_df.join(eval_series_df[['best_model']]) fcst_df[['model', 'pred-interval']] = fcst_df['model'].str.split('-', expand=True, n=1) fcst_df = fcst_df.query('model == best_model') fcst_df['name'] = [f'forecast-{x}' if x is not None else 'forecast' for x in fcst_df['pred-interval']] fcst_df = pd.pivot_table(fcst_df, index=['unique_id', 'ds'], values=['forecast'], columns=['name']).droplevel(0, axis=1).reset_index() ``` ```python theme={null} plot_series(Y_df, fcst_df, max_insample_length=28 * 3) ``` # Further materials * [Available Models StatsForecast](../../../statsforecast/src/core/models.html) * [Available Models NeuralForecast](../capabilities/overview.html) * [Loss Functions in NeuralForecast](../capabilities/objectives.html) * [Getting Started NeuralForecast](../getting-started/quickstart.html) * [Hierarchical Reconciliation](../../../hierarchicalforecast/examples/tourismsmall.html) * [Distributed ML Forecast (trees)](../../../mlforecast/docs/getting-started/quick_start_distributed.html) * [Using StatsForecast to train millions of time series](https://www.anyscale.com/blog/how-nixtla-uses-ray-to-accurately-predict-more-than-a-million-time-series) * [Intermittent Demand Forecasting With Nixtla on Databricks](https://www.databricks.com/blog/2022/12/06/intermittent-demand-forecasting-nixtla-databricks.html) # Modify the configure_optimizers() behavior of NeuralForecast models Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/configure_optimizers.html > Tutorial on how to achieve a full control of the > `configure_optimizers()` behavior of NeuralForecast models NeuralForecast models allow us to customize the default optimizer and learning rate scheduler behaviors via `optimizer`, `optimizer_kwargs`, `lr_scheduler`, `lr_scheduler_kwargs`. However this is not sufficient to support the use of [ReduceLROnPlateau](https://pytorch.org/docs/stable/generated/torch.optim.lr_scheduler.ReduceLROnPlateau.html), for instance, as it requires the specification of `monitor` parameter. This tutorial provides an example of how to support the use of `ReduceLROnPlateau`. ## Load libraries ```python theme={null} import numpy as np import pandas as pd import torch import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import NHITS from neuralforecast.utils import AirPassengersPanel from utilsforecast.plotting import plot_series ``` ```text theme={null} /root/miniconda3/envs/neuralforecast/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm 2025-02-25 15:57:21,708 INFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output. 2025-02-25 15:57:21,760 INFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output. ``` ## Data We use the AirPassengers dataset for the demonstration of conformal prediction. ```python theme={null} AirPassengersPanel_train = AirPassengersPanel[AirPassengersPanel['ds'] < AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) AirPassengersPanel_test = AirPassengersPanel[AirPassengersPanel['ds'] >= AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) AirPassengersPanel_test['y'] = np.nan AirPassengersPanel_test['y_[lag12]'] = np.nan ``` ## Model training We now train a NHITS model on the above dataset. We consider two different predictions: 1. Training using the default `configure_optimizers()`. 2. Training by overwriting the `configure_optimizers()` of the subclass of NHITS model. ```python theme={null} horizon = 12 input_size = 24 class CustomNHITS(NHITS): def configure_optimizers(self): optimizer = torch.optim.Adadelta(params=self.parameters(), rho=0.75) scheduler=torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer=optimizer, mode='min',factor=0.5, patience=2, ) scheduler_config = { 'scheduler': scheduler, 'interval': 'step', 'frequency': 1, 'monitor': 'train_loss', 'strict': True, 'name': None, } return {'optimizer': optimizer, 'lr_scheduler': scheduler_config} models = [ NHITS(h=horizon, input_size=input_size, max_steps=100, alias='NHITS-default-scheduler'), CustomNHITS(h=horizon, input_size=input_size, max_steps=100, alias='NHITS-ReduceLROnPlateau-scheduler'), ] nf = NeuralForecast(models=models, freq='ME') nf.fit(AirPassengersPanel_train) preds = nf.predict(futr_df=AirPassengersPanel_test) ``` ```text theme={null} Seed set to 1 Seed set to 1 GPU available: False, used: False TPU available: False, using: 0 TPU cores HPU available: False, using: 0 HPUs | Name | Type | Params | Mode ------------------------------------------------------- 0 | loss | MAE | 0 | train 1 | padder_train | ConstantPad1d | 0 | train 2 | scaler | TemporalNorm | 0 | train 3 | blocks | ModuleList | 2.4 M | train ------------------------------------------------------- 2.4 M Trainable params 0 Non-trainable params 2.4 M Total params 9.751 Total estimated model params size (MB) 34 Modules in train mode 0 Modules in eval mode `Trainer.fit` stopped: `max_steps=100` reached. GPU available: False, used: False TPU available: False, using: 0 TPU cores HPU available: False, using: 0 HPUs | Name | Type | Params | Mode ------------------------------------------------------- 0 | loss | MAE | 0 | train 1 | padder_train | ConstantPad1d | 0 | train 2 | scaler | TemporalNorm | 0 | train 3 | blocks | ModuleList | 2.4 M | train ------------------------------------------------------- 2.4 M Trainable params 0 Non-trainable params 2.4 M Total params 9.751 Total estimated model params size (MB) 34 Modules in train mode 0 Modules in eval mode `Trainer.fit` stopped: `max_steps=100` reached. GPU available: False, used: False TPU available: False, using: 0 TPU cores HPU available: False, using: 0 HPUs GPU available: False, used: False TPU available: False, using: 0 TPU cores HPU available: False, using: 0 HPUs ``` ```text theme={null} Epoch 99: 100%|██████████| 1/1 [00:00<00:00, 2.50it/s, v_num=85, train_loss_step=14.20, train_loss_epoch=14.20]Epoch 99: 100%|██████████| 1/1 [00:00<00:00, 2.49it/s, v_num=85, train_loss_step=14.20, train_loss_epoch=14.20] Epoch 99: 100%|██████████| 1/1 [00:00<00:00, 2.78it/s, v_num=86, train_loss_step=24.10, train_loss_epoch=24.10]Epoch 99: 100%|██████████| 1/1 [00:00<00:00, 2.77it/s, v_num=86, train_loss_step=24.10, train_loss_epoch=24.10] Predicting DataLoader 0: 100%|██████████| 1/1 [00:00<00:00, 33.39it/s] Predicting DataLoader 0: 100%|██████████| 1/1 [00:00<00:00, 246.29it/s] ``` ```python theme={null} plot_series(AirPassengersPanel_train, preds) ``` We can clearly notice the prediction outputs are different due to the change in `configure_optimizers()`. # Uncertainty quantification with Conformal Prediction Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/conformal_prediction.html > Tutorial on how to train neuralforecast models and obtain prediction > intervals using the conformal prediction methods Conformal prediction uses cross-validation on a model trained with a point loss function to generate prediction intervals. No additional training is needed, and the model is treated as a black box. The approach is compatible with any model. In this notebook, we demonstrate how to obtain prediction intervals using conformal prediction. ## Load libraries ```python theme={null} import logging import numpy as np import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import NHITS from neuralforecast.utils import AirPassengersPanel from neuralforecast.utils import PredictionIntervals from neuralforecast.losses.pytorch import DistributionLoss, MAE ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) ``` ## Data We use the AirPassengers dataset for the demonstration of conformal prediction. ```python theme={null} AirPassengersPanel_train = AirPassengersPanel[AirPassengersPanel['ds'] < AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) AirPassengersPanel_test = AirPassengersPanel[AirPassengersPanel['ds'] >= AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) AirPassengersPanel_test['y'] = np.nan AirPassengersPanel_test['y_[lag12]'] = np.nan ``` ## Model training We now train a NHITS model on the above dataset. To support conformal predictions, we must first instantiate the `PredictionIntervals` class and pass this to the `fit` method. By default, `PredictionIntervals` class employs `n_windows=2` for the corss-validation during the computation of conformity scores. We also train a MLP model using DistributionLoss to demonstate the difference between conformal prediction and quantiled outputs. By default, `PredictionIntervals` class employs `method=conformal_distribution` for the conformal predictions, but it also supports `method=conformal_error`. The `conformal_distribution` method calculates forecast paths using the absolute errors and based on them calculates quantiles. The `conformal_error` method calculates quantiles directly from errors. We consider two models below: 1. A model trained using a point loss function (`MAE`), where we quantify the uncertainty using conformal prediction. This case is labeled with `NHITS`. 2. A model trained using a `DistributionLoss('Normal')`, where we quantify the uncertainty by training the model to fit the parameters of a Normal distribution. This case is labeled with `NHITS1`. ```python theme={null} horizon = 12 input_size = 24 prediction_intervals = PredictionIntervals() models = [NHITS(h=horizon, input_size=input_size, max_steps=100, loss=MAE(), scaler_type="robust"), NHITS(h=horizon, input_size=input_size, max_steps=100, loss=DistributionLoss("Normal", level=[90]), scaler_type="robust")] nf = NeuralForecast(models=models, freq='ME') nf.fit(AirPassengersPanel_train, prediction_intervals=prediction_intervals) ``` ## Forecasting To generate conformal intervals, we specify the desired levels in the `predict` method. ```python theme={null} preds = nf.predict(futr_df=AirPassengersPanel_test, level=[90]) ``` ```python theme={null} fig, (ax1, ax2) = plt.subplots(2, 1, figsize = (20, 7)) plot_df = pd.concat([AirPassengersPanel_train, preds]) plot_df = plot_df[plot_df['unique_id']=='Airline1'].drop(['unique_id','trend','y_[lag12]'], axis=1).iloc[-50:] ax1.plot(plot_df['ds'], plot_df['y'], c='black', label='True') ax1.plot(plot_df['ds'], plot_df['NHITS'], c='blue', label='median') ax1.fill_between(x=plot_df['ds'][-12:], y1=plot_df['NHITS-lo-90'][-12:].values, y2=plot_df['NHITS-hi-90'][-12:].values, alpha=0.4, label='level 90') ax1.set_title('AirPassengers Forecast - Uncertainty quantification using Conformal Prediction', fontsize=18) ax1.set_ylabel('Monthly Passengers', fontsize=15) ax1.set_xticklabels([]) ax1.legend(prop={'size': 10}) ax1.grid() ax2.plot(plot_df['ds'], plot_df['y'], c='black', label='True') ax2.plot(plot_df['ds'], plot_df['NHITS1'], c='blue', label='median') ax2.fill_between(x=plot_df['ds'][-12:], y1=plot_df['NHITS1-lo-90'][-12:].values, y2=plot_df['NHITS1-hi-90'][-12:].values, alpha=0.4, label='level 90') ax2.set_title('AirPassengers Forecast - Uncertainty quantification using Normal distribution', fontsize=18) ax2.set_ylabel('Monthly Passengers', fontsize=15) ax2.set_xlabel('Timestamp [t]', fontsize=15) ax2.legend(prop={'size': 10}) ax2.grid() ``` # Converting Models to ONNX Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/converting_onnx.html It is possible to convert any NeuralForecast model to the Open Neural Network Exchange (ONNX) format. With ONNX, you get: - faster inference - hardware acceleration - easy deployment to edge devices - a broader device support In this tutorial, we show how you can convert a NeuralForecast model to ONNX. We show an example for a univariate and multivariate model using all types of exogenous features, and with multiple unique series, making it the most general case possible. You can run these experiments using GPU with Google Colab. Open In Colab ## Install dependencies ```python theme={null} %%capture !pip install neuralforecast onnxruntime onnxscript onnx ``` ## Key considerations There are some key elements to understand when converting a NeuralForecast model to ONNX. It comes from our the library works and it explains why directly `to_onnx()` doesn’t work. 1. The `forward` method in `neuralforecast` takes a dictionary (`windows_batch`), but that cannot be traced, so using `to_onnx()` directly fails. We must define a wrapper that takes tensors and rebuilds the dictionary internally. 2. Recall that `futr_exog` spans the history and the forecast horizon. When running inference, make sure to pass values that cover the input window and the horizon 3. The series order matters and it must match the order of training. Internally, series are sorted by `unique_id`. At the inference, the same order must be passed. 4. The scaler matters. With `scaler_type="identity"`, the output is in the same scale of the series. Any other scaler requires you to inverse-transform the predictions manually. 5. The batch size used when exporting to ONNX becomes fixed and you must use the same batch size at inference. To keep it flexible, we can use `torch.onnx.export(..., dynamo=True)`. 6. For multivariate models, `n_series` must be constant between training and inference. We set batch size to 1 because predictions are done in one joint window. ## Converting a univariate model Let’s see an example of converting the univariate MLP to ONNX. ### Import packages ```python theme={null} import numpy as np import pandas as pd import torch import torch.nn as nn import onnxruntime as ort from utilsforecast.data import generate_series from neuralforecast import NeuralForecast from neuralforecast.models import MLP, MLPMultivariate from neuralforecast.losses.pytorch import MAE ``` ### Set constants ```python theme={null} HIST_EXOG = ["hist_measure"] FUTR_EXOG = ["sin_doy", "cos_doy"] STAT_EXOG = ["static_0", "static_1"] ``` ### Function to create synthetic data ```python theme={null} def add_exog_features(df: pd.DataFrame) -> pd.DataFrame: """Add future (calendar) and historical exogenous columns.""" df = df.copy() doy = df["ds"].dt.dayofyear.to_numpy() # Future exog: deterministic calendar features, computable for any date. df["sin_doy"] = np.sin(2 * np.pi * doy / 365.25) df["cos_doy"] = np.cos(2 * np.pi * doy / 365.25) # Historical exog: a measurement we only have for the past. rng = np.random.default_rng(0) df["hist_measure"] = rng.normal(size=len(df)) return df def make_dataset(): series = generate_series( n_series=4, freq="D", min_length=120, max_length=120, n_static_features=2, static_as_categorical=False, equal_ends=True, ) # NeuralForecast requires static features in a separate static_df. static_df = series[["unique_id", *STAT_EXOG]].drop_duplicates("unique_id") df = add_exog_features(series.drop(columns=STAT_EXOG)) return df, static_df ``` ### Step1: Create an ONNX wrapper ```python theme={null} class MLPONNXWrapper(nn.Module): def __init__(self, model: MLP): super().__init__() self.model = model.eval() def forward(self, insample_y, futr_exog, hist_exog, stat_exog): windows_batch = { "insample_y": insample_y, "insample_mask": None, "futr_exog": futr_exog, "hist_exog": hist_exog, "stat_exog": stat_exog, } return self.model(windows_batch) ``` ### Step 2: Train the model in `neuralforecast` ```python theme={null} df, static_df = make_dataset() horizon = 14 model = MLP( h=horizon, input_size=30, hist_exog_list=HIST_EXOG, futr_exog_list=FUTR_EXOG, stat_exog_list=STAT_EXOG, max_steps=50, loss=MAE(), scaler_type="identity", # ONNX output stays in the raw data scale ) nf = NeuralForecast(models=[model], freq="D") nf.fit(df, static_df=static_df) model = nf.models[0] # extract fitted model ``` ### Step 3: Convert to ONNX ```python theme={null} def convert_to_onnx(model: MLP, n_windows, path="mlp_univariate.onnx") -> str: wrapper = MLPONNXWrapper(model) L, h = model.input_size, model.h n_futr, n_hist, n_stat = len(FUTR_EXOG), len(HIST_EXOG), len(STAT_EXOG) B = n_windows example = ( torch.randn(B, L, 1), # insample_y torch.randn(B, L + h, n_futr), # futr_exog (input window + horizon) torch.randn(B, L, n_hist), # hist_exog torch.randn(B, n_stat), # stat_exog ) input_names = ["insample_y", "futr_exog", "hist_exog", "stat_exog"] torch.onnx.export( wrapper, example, path, input_names=input_names, output_names=["forecast"], opset_version=17, ) return path n_windows = df["unique_id"].nunique() onnx_path = convert_to_onnx(model, n_windows) ``` ### Step 4: Prepare input for inference ```python theme={null} def build_inputs(df, static_df, futr_df, model: MLP): L, h = model.input_size, model.h ids = sorted(df["unique_id"].unique()) B = len(ids) insample_y = np.zeros((B, L, 1), np.float32) hist_exog = np.zeros((B, L, len(HIST_EXOG)), np.float32) futr_exog = np.zeros((B, L + h, len(FUTR_EXOG)), np.float32) stat_exog = np.zeros((B, len(STAT_EXOG)), np.float32) static_df = static_df.set_index("unique_id") for i, uid in enumerate(ids): win = df[df["unique_id"] == uid].iloc[-L:] fut = futr_df[futr_df["unique_id"] == uid] insample_y[i, :, 0] = win["y"].to_numpy() hist_exog[i] = win[HIST_EXOG].to_numpy() futr_exog[i] = np.concatenate( [win[FUTR_EXOG].to_numpy(), fut[FUTR_EXOG].to_numpy()], axis=0 ) stat_exog[i] = static_df.loc[uid, STAT_EXOG].to_numpy() feeds = { "insample_y": insample_y, "futr_exog": futr_exog, "hist_exog": hist_exog, "stat_exog": stat_exog, } return ids, feeds futr_df = nf.make_future_dataframe() futr_df = add_exog_features(futr_df) ids, feeds = build_inputs(df, static_df, futr_df, model) ``` ### Step 5: Predict ```python theme={null} def predict(path, feeds): sess = ort.InferenceSession(path, providers=["CPUExecutionProvider"]) return sess.run(["forecast"], feeds)[0] forecast = predict(onnx_path, feeds) # [n_series, h, 1] ``` ## Converting a multivariate model Now, let’s see an example of converting the multivariate MLP to ONNX. Note that we don’t repeat the steps to create the synthetic dataset. ### Step 1: Create the ONNX wrapper ```python theme={null} class MLPMultivariateONNXWrapper(nn.Module): def __init__(self, model: MLPMultivariate): super().__init__() self.model = model.eval() def forward(self, insample_y, futr_exog, hist_exog, stat_exog): windows_batch = { "insample_y": insample_y, "insample_mask": None, "futr_exog": futr_exog, "hist_exog": hist_exog, "stat_exog": stat_exog, } return self.model(windows_batch) ``` ### Step 2: Train the model ```python theme={null} df, static_df = make_dataset() horizon = 14 n_series = df["unique_id"].nunique() model = MLPMultivariate( h=horizon, input_size=30, n_series=n_series, hist_exog_list=HIST_EXOG, futr_exog_list=FUTR_EXOG, stat_exog_list=STAT_EXOG, max_steps=50, loss=MAE(), scaler_type="identity", ) nf = NeuralForecast(models=[model], freq="D") nf.fit(df, static_df=static_df) model = nf.models[0] ``` ### Step 3: Convert to ONNX Recall that all series are predicted in one joint window, so batch size is set to 1. Also, `n_series` is fixed by the trained model. ```python theme={null} def convert_to_onnx(model: MLPMultivariate, n_series, path="mlpmultivariate.onnx"): wrapper = MLPMultivariateONNXWrapper(model) L, h, N = model.input_size, model.h, n_series n_futr, n_hist, n_stat = len(FUTR_EXOG), len(HIST_EXOG), len(STAT_EXOG) example = ( torch.randn(1, L, N), # insample_y [1, L, N] torch.randn(1, n_futr, L + h, N), # futr_exog [1, F, L+h, N] torch.randn(1, n_hist, L, N), # hist_exog [1, X, L, N] torch.randn(N, n_stat), # stat_exog [N, S] ) input_names = ["insample_y", "futr_exog", "hist_exog", "stat_exog"] torch.onnx.export( wrapper, example, path, input_names=input_names, output_names=["forecast"], opset_version=17, ) return path onnx_path = convert_to_onnx(model, n_series) ``` ### Step 4: Prepare input for inference ```python theme={null} def build_inputs(df, static_df, futr_df, model: MLPMultivariate): L, h = model.input_size, model.h ids = sorted(df["unique_id"].unique()) N = len(ids) insample_y = np.zeros((1, L, N), np.float32) hist_exog = np.zeros((1, len(HIST_EXOG), L, N), np.float32) futr_exog = np.zeros((1, len(FUTR_EXOG), L + h, N), np.float32) stat_exog = np.zeros((N, len(STAT_EXOG)), np.float32) static_df = static_df.set_index("unique_id") for j, uid in enumerate(ids): # j indexes the series axis win = df[df["unique_id"] == uid].iloc[-L:] fut = futr_df[futr_df["unique_id"] == uid] insample_y[0, :, j] = win["y"].to_numpy() for k, col in enumerate(HIST_EXOG): hist_exog[0, k, :, j] = win[col].to_numpy() for k, col in enumerate(FUTR_EXOG): # future exog spans the input window AND the horizon futr_exog[0, k, :, j] = np.concatenate( [win[col].to_numpy(), fut[col].to_numpy()] ) stat_exog[j] = static_df.loc[uid, STAT_EXOG].to_numpy() feeds = { "insample_y": insample_y, "futr_exog": futr_exog, "hist_exog": hist_exog, "stat_exog": stat_exog, } return ids, feeds futr_df = nf.make_future_dataframe() futr_df = add_exog_features(futr_df) ids, feeds = build_inputs(df, static_df, futr_df, model) ``` ### Step 5: Predict ```python theme={null} def predict(path, feeds): sess = ort.InferenceSession(path, providers=["CPUExecutionProvider"]) return sess.run(["forecast"], feeds)[0] forecast = predict(onnx_path, feeds) # [1, h, N] ``` # Time Series Cross Validation Tutorial with NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/cross_validation.html Build a complete NeuralForecast cross validation workflow in Python, compare predictions with actual values, and evaluate models across rolling windows. > Implement cross-validation to evaluate models on historical data Time series cross-validation is a method for evaluating how a model would have performed on historical data. It works by defining a sliding window across past observations and predicting the period following it. It differs from standard cross-validation by maintaining the chronological order of the data instead of randomly splitting it. This method allows for a better estimation of our model’s predictive capabilities by considering multiple periods. When only one window is used, it resembles a standard train-test split, where the test data is the last set of observations, and the training set consists of the earlier data. The following graph showcases how time series cross-validation works. ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) In this tutorial we’ll explain how to perform cross-validation in `NeuralForecast`. **Outline:** 1. Install NeuralForecast 1. Load and plot the data 2. Train multiple models using cross-validation 3. Evaluate models and select the best for each series 4. Plot cross-validation results > **Prerequisites** > > This guide assumes basic familiarity with `neuralforecast`. For a > minimal example visit the [Quick > Start](../getting-started/quickstart.html) ## 1. Install NeuralForecast ```python theme={null} %%capture !pip install neuralforecast ``` ## 2. Load and plot the data We’ll use pandas to load the hourly dataset from the [M4 Forecasting Competition](https://www.sciencedirect.com/science/article/pii/S0169207019301128), which has been stored in a parquet file for efficiency. ```python theme={null} import os import logging import pandas as pd from IPython.display import display ``` ```python theme={null} os.environ['PL_TRAINER_ENABLE_PROGRESS_BAR'] = '0' logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) ``` ```python theme={null} Y_df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | The input to `neuralforecast` should be a data frame in long format with three columns: `unique_id`, `ds`, and `y`. * `unique_id` (string, int, or category): A unique identifier for each time series. * `ds` (int or timestamp): An integer indexing time or a timestamp in format YYYY-MM-DD or YYYY-MM-DD HH:MM:SS. * `y` (numeric): The target variable to forecast. This dataset contains 414 unique time series. To reduce the total execution time, we’ll use only the first 10. ```python theme={null} uids = Y_df['unique_id'].unique()[:10] # Select 10 ids to make the example run faster Y_df = Y_df.query('unique_id in @uids').reset_index(drop=True) ``` To plot the series, we’ll use the `plot_series` method from `utilsforecast.plotting`. `utilsforecast` is a dependency of `neuralforecast` so it should be already installed. ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} plot_series(Y_df) ``` ## 3. Train multiple models using cross-validation We’ll train different models from `neuralforecast` using the `cross-validation` method to decide which one perfoms best on the historical data. To do this, we need to import the `NeuralForecast` class and the models that we want to compare. ```python theme={null} from neuralforecast import NeuralForecast from neuralforecast.auto import MLP, NBEATS, NHITS from neuralforecast.losses.pytorch import MQLoss ``` In this tutorial, we will use `neuralforecast's` [MPL](../../models.mlp.html#mlp-2), [NBEATS](../../models.nbeats.html#nbeats), and [NHITS](../../models.nhits.html#nhits) models. First, we need to create a list of models and then instantiate the `NeuralForecast` class. For each model, we’ll define the following hyperparameters: * `h`: The forecast horizon. Here, we will use the same horizon as in the M4 competition, which was 48 steps ahead. * `input_size`: The number of historical observations (lags) that the model uses to make predictions. In this case, it will be twice the forecast horizon. * `loss`: The loss function to optimize. Here, we’ll use the Multi Quantile Loss (MQLoss) from `neuralforecast.losses.pytorch`. > **Warning** > > The Multi Quantile Loss (MQLoss) is the sum of the quantile losses for > each target quantile. The quantile loss for a single quantile measures > how well a model has predicted a specific quantile of the actual > distribution, penalizing overestimations and underestimations > asymmetrically based on the quantile’s value. For more details see > [here](../../losses.pytorch.html#mqloss). While there are other hyperparameters that can be defined for each model, we’ll use the default values for the purposes of this tutorial. To learn more about the hyperparameters of each model, please check out the corresponding documentation. ```python theme={null} horizon = 48 models = [MLP(h=horizon, input_size=2*horizon, loss=MQLoss()), NBEATS(h=horizon, input_size=2*horizon, loss=MQLoss()), NHITS(h=horizon, input_size=2*horizon, loss=MQLoss()),] nf = NeuralForecast(models=models, freq=1) ``` The `cross_validation` method takes the following arguments: * `df`: The data frame in the format described in section 2. * `n_windows` (int): The number of windows to evaluate. Default is 1 and here we’ll use 3. * `step_size` (int): The number of steps between consecutive windows to produce the forecasts. In this example, we’ll set `step_size=horizon` to produce non-overlapping forecasts. The following diagram shows how the forecasts are produced based on the `step_size` parameter and forecast horizon `h` of a model. In this diagram `step_size=2` and `h=4`. * `refit` (bool or int): Whether to retrain models for each cross-validation window. If `False`, the models are trained at the beginning and then used to predict each window. If a positive integer, the models are retrained every `refit` windows. Default is `False`, but here we’ll use `refit=1` so that the models are retrained after each window using the data with timestamps up to and including the cutoff. ```python theme={null} cv_df = nf.cross_validation(Y_df, n_windows=3, step_size=horizon, refit=1) ``` It’s worth mentioning that the default version of the `cross_validation` method in `neuralforecast` diverges from other libraries, where models are typically retrained at the start of each window. By default, it trains the models once and then uses them to generate predictions over all the windows, thus reducing the total execution time. For scenarios where the models need to be retrained, you can use the `refit` parameter to specify the number of windows after which the models should be retrained. ```python theme={null} cv_df.head() ``` | | unique\_id | ds | cutoff | MLP-median | MLP-lo-90 | MLP-lo-80 | MLP-hi-80 | MLP-hi-90 | NBEATS-median | NBEATS-lo-90 | NBEATS-lo-80 | NBEATS-hi-80 | NBEATS-hi-90 | NHITS-median | NHITS-lo-90 | NHITS-lo-80 | NHITS-hi-80 | NHITS-hi-90 | y | | - | ---------- | --- | ------ | ---------- | ---------- | ---------- | ---------- | ---------- | ------------- | ------------ | ------------ | ------------ | ------------ | ------------ | ----------- | ----------- | ----------- | ----------- | ----- | | 0 | H1 | 605 | 604 | 638.964111 | 528.127747 | 546.731812 | 714.415466 | 750.265259 | 623.230896 | 580.549744 | 587.317688 | 647.942505 | 654.148682 | 625.377930 | 556.786926 | 577.746765 | 657.901611 | 670.458069 | 622.0 | | 1 | H1 | 606 | 604 | 588.216370 | 445.395081 | 483.736542 | 684.394592 | 670.042358 | 552.829407 | 501.618988 | 529.007507 | 593.528564 | 603.152527 | 555.956177 | 511.696350 | 526.399597 | 604.318970 | 622.839722 | 558.0 | | 2 | H1 | 607 | 604 | 542.242737 | 419.206757 | 439.244476 | 617.775269 | 638.583923 | 495.155548 | 451.871613 | 467.183533 | 550.048950 | 574.697021 | 502.860077 | 462.284668 | 460.950287 | 555.336731 | 571.852722 | 513.0 | | 3 | H1 | 608 | 604 | 494.055573 | 414.775085 | 427.531647 | 583.965759 | 602.303772 | 465.182556 | 403.593140 | 410.033203 | 500.744019 | 518.277954 | 460.588684 | 406.762390 | 418.040710 | 501.833740 | 515.022095 | 476.0 | | 4 | H1 | 609 | 604 | 469.330688 | 361.437927 | 378.501373 | 557.875244 | 569.767273 | 441.072388 | 371.541504 | 401.923584 | 483.667877 | 485.047729 | 441.463043 | 393.917725 | 394.483337 | 475.985229 | 499.001373 | 449.0 | The output of the `cross-validation` method is a data frame that includes the following columns: * `unique_id`: The unique identifier for each time series. * `ds`: The timestamp or temporal index. * `cutoff`: The last timestamp or temporal index used in that cross-validation window. * `"model"`: Columns with the model’s point forecasts (median) and prediction intervals. By default, the 80 and 90% prediction intervals are included when using the MQLoss. * `y`: The actual value. ## 4. Evaluate models and select the best for each series To evaluate the point forecasts of the models, we’ll use the Root Mean Squared Error (RMSE), defined as the square root of the mean of the squared differences between the actual and the predicted values. For convenience, we’ll use the `evaluate` and the `rmse` functions from `utilsforecast`. ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import rmse ``` The `evaluate` function takes the following arguments: * `df`: The data frame with the forecasts to evaluate. * `metrics` (list): The metrics to compute. * `models` (list): Names of the models to evaluate. Default is `None`, which uses all columns after removing `id_col`, `time_col`, and `target_col`. * `id_col` (str): Column that identifies unique ids of the series. Default is `unique_id`. * `time_col` (str): Column with the timestamps or the temporal index. Default is `ds`. * `target_col` (str): Column with the target variable. Default is `y`. Notice that if we use the default value of `models`, then we need to exclude the `cutoff` column from the cross-validation data frame. ```python theme={null} evaluation_df = evaluate(cv_df.drop(columns='cutoff'), metrics=[rmse]) ``` For each unique id, we’ll select the model with the lowest RMSE. ```python theme={null} evaluation_df['best_model'] = evaluation_df.drop(columns=['metric', 'unique_id']).idxmin(axis=1) evaluation_df ``` | | unique\_id | metric | MLP-median | NBEATS-median | NHITS-median | best\_model | | - | ---------- | ------ | ----------- | ------------- | ------------ | ------------- | | 0 | H1 | rmse | 46.654390 | 49.595304 | 47.651201 | MLP-median | | 1 | H10 | rmse | 24.192081 | 21.580142 | 16.887989 | NHITS-median | | 2 | H100 | rmse | 171.958998 | 178.820952 | 170.452623 | NHITS-median | | 3 | H101 | rmse | 331.270162 | 260.021871 | 169.453119 | NHITS-median | | 4 | H102 | rmse | 440.470939 | 362.602167 | 326.571391 | NHITS-median | | 5 | H103 | rmse | 9069.937603 | 9267.925257 | 8578.535681 | NHITS-median | | 6 | H104 | rmse | 189.534415 | 169.017976 | 226.442403 | NBEATS-median | | 7 | H105 | rmse | 341.029706 | 284.038751 | 262.140145 | NHITS-median | | 8 | H106 | rmse | 203.723728 | 328.128422 | 298.377068 | MLP-median | | 9 | H107 | rmse | 212.384943 | 161.445838 | 231.303421 | NBEATS-median | We can summarize the results to see how many times each model won. ```python theme={null} summary_df = evaluation_df.groupby(['metric', 'best_model']).size().sort_values().to_frame() summary_df = summary_df.reset_index() summary_df.columns = ['metric', 'model', 'num. of unique_ids'] summary_df ``` | | metric | model | num. of unique\_ids | | - | ------ | ------------- | ------------------- | | 0 | rmse | MLP-median | 2 | | 1 | rmse | NBEATS-median | 2 | | 2 | rmse | NHITS-median | 6 | With this information, we now know which model performs best for each series in the historical data. ## 5. Plot cross-validation results To visualize the cross-validation results, we will use the `plot_series` method again. We’ll need to rename the `y` column in the cross-validation output to avoid duplicates with the original data frame. We’ll also exclude the `cutoff` column and use the `max_insample_length argument` to plot only the last 300 observations for better visualization. ```python theme={null} cv_df.rename(columns = {'y': 'actual'}, inplace=True) # rename actual values plot_series(Y_df, cv_df.drop(columns='cutoff'), max_insample_length=300) ``` To clarify the concept of cross-validation further, we’ll plot the forecasts generated at each cutoff for the series with `unique_id='H1'`. There are three cutoffs because we set `n_windows=3`. In this example, we used `refit=1`, so each model is retrained for each window using data with timestamps up to and including the respective cutoff. Additionally, since `step_size` is equal to the forecast horizon, the resulting forecasts are non-overlapping ```python theme={null} cutoff1, cutoff2, cutoff3 = cv_df['cutoff'].unique() for cutoff in cv_df['cutoff'].unique(): display( plot_series( Y_df, cv_df[cv_df['cutoff'] == cutoff].drop(columns='cutoff'), ids=['H1'], # use ids parameter to select specific series ) ) ``` # Distributed Training Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/distributed_neuralforecast.html ## Prerequisites This notebook was ran in databricks using the following configuration: * Databricks Runtime Version: 14.3 LTS ML (Spark 3.5, GPU, Scala 2.12) * Worker and executors instance type: g4dn.xlarge * Cluster libraries: * neuralforecast==1.7.0 * fugue * protobuf\<=3.20.1 * s3fs ## Load libraries ```python theme={null} import logging import numpy as np import pandas as pd from neuralforecast import NeuralForecast, DistributedConfig from neuralforecast.auto import AutoNHITS from neuralforecast.models import NHITS, LSTM from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae, rmse, smape from utilsforecast.plotting import plot_series ``` ```text theme={null} 2024-06-12 21:29:32.857491: I tensorflow/core/util/port.cc:111] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`. 2024-06-12 21:29:32.901906: E tensorflow/compiler/xla/stream_executor/cuda/cuda_dnn.cc:9342] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered 2024-06-12 21:29:32.901946: E tensorflow/compiler/xla/stream_executor/cuda/cuda_fft.cc:609] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered 2024-06-12 21:29:32.901973: E tensorflow/compiler/xla/stream_executor/cuda/cuda_blas.cc:1518] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered 2024-06-12 21:29:32.909956: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations. To enable the following instructions: AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags. ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) ``` ## Data ```python theme={null} df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') df['exog_0'] = np.random.rand(df.shape[0]) static = df.groupby('unique_id').head(1).copy() static['stat_0'] = static['unique_id'].astype('category').cat.codes static = static[['unique_id', 'stat_0']] valid = df.groupby('unique_id').tail(24) train = df.drop(valid.index) # save for loading in spark s3_prefix = 's3://nixtla-tmp/distributed' train.to_parquet(f'{s3_prefix}/train.parquet', index=False) valid.to_parquet(f'{s3_prefix}/valid.parquet', index=False) static.to_parquet(f'{s3_prefix}/static.parquet', index=False) # load in spark spark_train = spark.read.parquet(f'{s3_prefix}/train.parquet') spark_valid = spark.read.parquet(f'{s3_prefix}/valid.parquet') spark_static = spark.read.parquet(f'{s3_prefix}/static.parquet') ``` ## Configuration ```python theme={null} # Configuration required for distributed training dist_cfg = DistributedConfig( partitions_path=f'{s3_prefix}/partitions', # path where the partitions will be saved num_nodes=2, # number of nodes to use during training (machines) devices=1, # number of GPUs in each machine ) # pytorch lightning configuration # the executors don't have permission to write on the filesystem, so we disable saving artifacts distributed_kwargs = dict( accelerator='gpu', enable_progress_bar=False, logger=False, enable_checkpointing=False, ) # exogenous features exogs = { 'futr_exog_list': ['exog_0'], 'stat_exog_list': ['stat_0'], } # for the AutoNHITS def config(trial): return dict( input_size=48, max_steps=2_000, learning_rate=trial.suggest_float('learning_rate', 1e-4, 1e-1, log=True), **exogs, **distributed_kwargs ) ``` ## Model training ```python theme={null} nf = NeuralForecast( models=[ NHITS(h=24, input_size=48, max_steps=2_000, **exogs, **distributed_kwargs), AutoNHITS(h=24, config=config, backend='optuna', num_samples=2, alias='tuned_nhits'), LSTM(h=24, input_size=48, max_steps=2_000, **exogs, **distributed_kwargs), ], freq=1, ) nf.fit(spark_train, static_df=spark_static, distributed_config=dist_cfg, val_size=24) ``` ```text theme={null} [rank: 0] Seed set to 1 /local_disk0/.ephemeral_nfs/cluster_libraries/python/lib/python3.10/site-packages/pytorch_lightning/utilities/parsing.py:199: Attribute 'loss' is an instance of `nn.Module` and is already saved during checkpointing. It is recommended to ignore them using `self.save_hyperparameters(ignore=['loss'])`. [rank: 0] Seed set to 1 INFO:TorchDistributor:Started distributed training with 2 executor processes [rank: 1] Seed set to 1 [rank: 0] Seed set to 1 [rank: 1] Seed set to 1 Initializing distributed: GLOBAL_RANK: 1, MEMBER: 2/2 GPU available: True (cuda), used: True TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs [rank: 0] Seed set to 1 Initializing distributed: GLOBAL_RANK: 0, MEMBER: 1/2 ---------------------------------------------------------------------------------------------------- distributed_backend=nccl All distributed processes registered. Starting with 2 processes ---------------------------------------------------------------------------------------------------- LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] | Name | Type | Params ----------------------------------------------- 0 | loss | MAE | 0 1 | padder_train | ConstantPad1d | 0 2 | scaler | TemporalNorm | 0 3 | blocks | ModuleList | 2.6 M ----------------------------------------------- 2.6 M Trainable params 0 Non-trainable params 2.6 M Total params 10.341 Total estimated model params size (MB) LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] `Trainer.fit` stopped: `max_steps=2000` reached. INFO:TorchDistributor:Finished distributed training with 2 executor processes [I 2024-06-12 21:31:09,627] A new study created in memory with name: no-name-849c3a84-28d7-417b-a48d-f0feac64cbc3 [rank: 0] Seed set to 1 INFO:TorchDistributor:Started distributed training with 2 executor processes [rank: 1] Seed set to 1 [rank: 0] Seed set to 1 [rank: 1] Seed set to 1 Initializing distributed: GLOBAL_RANK: 1, MEMBER: 2/2 GPU available: True (cuda), used: True TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs [rank: 0] Seed set to 1 Initializing distributed: GLOBAL_RANK: 0, MEMBER: 1/2 ---------------------------------------------------------------------------------------------------- distributed_backend=nccl All distributed processes registered. Starting with 2 processes ---------------------------------------------------------------------------------------------------- LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] | Name | Type | Params ----------------------------------------------- 0 | loss | MAE | 0 1 | padder_train | ConstantPad1d | 0 2 | scaler | TemporalNorm | 0 3 | blocks | ModuleList | 2.6 M ----------------------------------------------- 2.6 M Trainable params 0 Non-trainable params 2.6 M Total params 10.341 Total estimated model params size (MB) `Trainer.fit` stopped: `max_steps=2000` reached. INFO:TorchDistributor:Finished distributed training with 2 executor processes [I 2024-06-12 21:32:26,716] Trial 0 finished with value: 240.63693237304688 and parameters: {'learning_rate': 0.0008137359313625077}. Best is trial 0 with value: 240.63693237304688. [rank: 0] Seed set to 1 INFO:TorchDistributor:Started distributed training with 2 executor processes [rank: 1] Seed set to 1 [rank: 0] Seed set to 1 [rank: 1] Seed set to 1 Initializing distributed: GLOBAL_RANK: 1, MEMBER: 2/2 GPU available: True (cuda), used: True TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs [rank: 0] Seed set to 1 Initializing distributed: GLOBAL_RANK: 0, MEMBER: 1/2 ---------------------------------------------------------------------------------------------------- distributed_backend=nccl All distributed processes registered. Starting with 2 processes ---------------------------------------------------------------------------------------------------- LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] | Name | Type | Params ----------------------------------------------- 0 | loss | MAE | 0 1 | padder_train | ConstantPad1d | 0 2 | scaler | TemporalNorm | 0 3 | blocks | ModuleList | 2.6 M ----------------------------------------------- 2.6 M Trainable params 0 Non-trainable params 2.6 M Total params 10.341 Total estimated model params size (MB) LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] `Trainer.fit` stopped: `max_steps=2000` reached. INFO:TorchDistributor:Finished distributed training with 2 executor processes [I 2024-06-12 21:33:43,744] Trial 1 finished with value: 269.3470153808594 and parameters: {'learning_rate': 0.0007824692588634985}. Best is trial 0 with value: 240.63693237304688. [rank: 0] Seed set to 1 INFO:TorchDistributor:Started distributed training with 2 executor processes [rank: 1] Seed set to 1 [rank: 0] Seed set to 1 [rank: 1] Seed set to 1 Initializing distributed: GLOBAL_RANK: 1, MEMBER: 2/2 GPU available: True (cuda), used: True TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs [rank: 0] Seed set to 1 Initializing distributed: GLOBAL_RANK: 0, MEMBER: 1/2 ---------------------------------------------------------------------------------------------------- distributed_backend=nccl All distributed processes registered. Starting with 2 processes ---------------------------------------------------------------------------------------------------- LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] | Name | Type | Params ----------------------------------------------- 0 | loss | MAE | 0 1 | padder_train | ConstantPad1d | 0 2 | scaler | TemporalNorm | 0 3 | blocks | ModuleList | 2.6 M ----------------------------------------------- 2.6 M Trainable params 0 Non-trainable params 2.6 M Total params 10.341 Total estimated model params size (MB) `Trainer.fit` stopped: `max_steps=2000` reached. INFO:TorchDistributor:Finished distributed training with 2 executor processes INFO:TorchDistributor:Started distributed training with 2 executor processes [rank: 0] Seed set to 1 [rank: 1] Seed set to 1 GPU available: True (cuda), used: True TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs [rank: 0] Seed set to 1 Initializing distributed: GLOBAL_RANK: 0, MEMBER: 1/2 ---------------------------------------------------------------------------------------------------- distributed_backend=nccl All distributed processes registered. Starting with 2 processes ---------------------------------------------------------------------------------------------------- [rank: 1] Seed set to 1 Initializing distributed: GLOBAL_RANK: 1, MEMBER: 2/2 LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] | Name | Type | Params -------------------------------------------------- 0 | loss | MAE | 0 1 | padder | ConstantPad1d | 0 2 | scaler | TemporalNorm | 0 3 | hist_encoder | LSTM | 484 K 4 | context_adapter | Linear | 54.0 K 5 | mlp_decoder | MLP | 2.6 K -------------------------------------------------- 541 K Trainable params 0 Non-trainable params 541 K Total params 2.166 Total estimated model params size (MB) `Trainer.fit` stopped: `max_steps=2000` reached. INFO:TorchDistributor:Finished distributed training with 2 executor processes ``` ## Forecasting When we’re done training the model in a distributed way we can predict using the stored dataset. If we have future exogenous features we can provide a spark dataframe as `futr_df`. Note that if you want to load the stored dataset you need to provide the spark session through the `engine` argument. ```python theme={null} saved_ds_preds = nf.predict(futr_df=spark_valid.drop("y"), engine=spark).toPandas() ``` We can also provide a spark dataframe as `df` as well as `static_df` and `futr_df` (if applicable) to compute predictions on different data or after loading a saved model. ```python theme={null} new_df_preds = nf.predict(df=spark_train, static_df=spark_static, futr_df=spark_valid.drop("y")).toPandas() ``` Either of the above methods will yield the same results. ```python theme={null} pd.testing.assert_frame_equal( saved_ds_preds.sort_values(['unique_id', 'ds']).reset_index(drop=True), new_df_preds.sort_values(['unique_id', 'ds']).reset_index(drop=True), atol=1e-3, ) ``` ## Saving for inference We can now persist the trained models ```python theme={null} save_path = f'{s3_prefix}/model-artifacts' nf.save(save_path, save_dataset=False, overwrite=True) ``` And load them back ```python theme={null} nf2 = NeuralForecast.load(save_path) ``` ```text theme={null} [rank: 0] Seed set to 1 [rank: 0] Seed set to 1 [rank: 0] Seed set to 1 ``` We can now use this object to compute forecasts. We can provide either local dataframes (pandas, polars) as well as spark dataframes ```python theme={null} preds = nf.predict(df=train, static_df=static, futr_df=valid.drop(columns='y')) preds2 = nf2.predict(df=train, static_df=static, futr_df=valid.drop(columns='y'))[preds.columns] pd.testing.assert_frame_equal(saved_ds_preds, preds) pd.testing.assert_frame_equal(preds, preds2) ``` ## Evaluation ```python theme={null} ( evaluate( preds.merge(valid.drop(columns='exog_0'), on=['unique_id', 'ds']), metrics=[mae, rmse, smape], ) .drop(columns='unique_id') .groupby('metric') .mean() ) ``` | | NHITS | tuned\_nhits | LSTM | | ------ | ---------- | ------------ | ---------- | | metric | | | | | mae | 417.075336 | 322.751522 | 270.423775 | | rmse | 485.304941 | 410.998659 | 330.579283 | | smape | 0.063995 | 0.066046 | 0.063975 | ## Plotting a sample ```python theme={null} plot_series(train, preds) ``` # Explainability for Deep Learning Forecasting Models Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/explainability.html In this detailed tutorial, we discover how to explain forecasts made with models from *neuralforecast*. Note that the functionality is currently in beta. It can only be applied on univariate models, but support for multivariate models is coming soon. ## Prerequisites * We assume you have *neuralforecast* already installed. * Explanations are obtained with [Captum](https://captum.ai/): an open-source library for model interpretability in PyTorch. Make sure to install the package with `pip install captum` to use the features demonstrated below. * You can optionally install [SHAP](https://shap.readthedocs.io/en/latest/) to access their visualizations capabilities. This can be done with `pip install shap`. ## Load libraries ```python theme={null} %%capture import matplotlib.pyplot as plt import numpy as np import pandas as pd import torch from neuralforecast.core import NeuralForecast from neuralforecast.models import MLPMultivariate, NHITS from neuralforecast.losses.pytorch import MQLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic ``` ```python theme={null} # Set random seeds for reproducibility np.random.seed(42); torch.manual_seed(42); ``` ## Load the data We demonstrate the explainability capabilities with the AirPassengers dataset. This dataset has: - 2 unique series - a future exogenous variable (`trend`) - a historical exogenous variable (`y_lag[12]`) - static exogenous variable (`Airline1`) That way, we see that we can handle attributions for all types of exogenous features. For more information on the types of exogenous features, read [this tutorial](https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/exogenous_variables.html). ```python theme={null} Y_train_df = AirPassengersPanel[AirPassengersPanel['ds'] < AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) Y_test_df = AirPassengersPanel[AirPassengersPanel['ds'] >= AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) futr_df = Y_test_df.drop(columns=["y", "y_[lag12]"]) ``` ## Basic usage ### Train a model Before explaining forecasts, we need to train a forecasting model. Here, we use the NHITS model, but you can use any univariate model. For now, we don’t support multivariate models just yet, this feature will be implemented soon. ```python theme={null} %%capture models = [ NHITS( h=12, input_size=24, hist_exog_list=["y_[lag12]"], futr_exog_list=["trend"], stat_exog_list=['airline1'], max_steps=20, scaler_type="robust", ), ] nf = NeuralForecast( models=models, freq="ME", ) nf.fit( df=Y_train_df, static_df=AirPassengersStatic ) ``` ### Get features attributions Once the model is trained, we can get feature attributions using the `nf.explain` method. This method takes the following parameters: - `horizons`: List of horizons to explain. If None, all horizons are explained. Defaults to None. - `outputs`: List of outputs to explain for models with multiple outputs. Defaults to \[0] (first output). This is useful when we have models trained with a probabilistic loss. We will explore that later in the tutorial. - `series` List of series indices to explain. If None, all series are explained. Defaults to None. Useful when a single model forecasts multiple series but you only want to explain a specific one. - `explainer`: Name of the explainer to use. Options are ‘IntegratedGradients’, ‘ShapleyValueSampling’, ‘Lime’, ‘KernelShap’, ‘InputXGradient’. Defaults to ‘IntegratedGradients’. - `df` (pandas, polars or spark DataFrame): DataFrame with columns \[`unique_id`, `ds`, `y`] and exogenous variables. If a DataFrame is passed, it is used to generate forecasts. Defaults to None. - `static_df` (pandas, polars or spark DataFrame): DataFrame with columns \[`unique_id`] and static exogenous. Defaults to None. Only use it if you trained your model with static exogenous features. - `futr_df` (pandas, polars or spark DataFrame): DataFrame with \[`unique_id`, `ds`] columns and `df`’s future exogenous. Defaults to None. Only use it if you trained your model with future exogenous features. - `verbose`: Print warnings. Defaults to True. - `engine`: Distributed engine for inference. Only used if df is a spark dataframe or if fit was called on a spark dataframe. - `level`: Confidence levels between 0 and 100. Defaults to None. - `quantiles`: Alternative to level, target quantiles to predict. Defaults to None. - `data_kwargs`: Extra arguments to be passed to the dataset within each model. Note that parameters from `df` and onwards act exactly the same way as in the `nf.predict()` method. In this case, let’s explain each horizon step, so we keep `horizons=None`. Since our model used a point loss, there is only one output, so we also keep the default value `outputs=[0]`. Finally, we choose the “IntegratedGradients” explainer, as it is one of the fastest method for interpretability in deep learning. ```python theme={null} %%capture preds_df, explanations = nf.explain( static_df=AirPassengersStatic, futr_df=futr_df, explainer="IntegratedGradients" ) ``` We can see that `nf.explain()` returns two values: 1. A dataframe with the forecasts from the fitted models 2. A dictionary with the feature attributions for each model Thus, you can access the attribution score of each features used for training the NHITS model by accessing `explanations["NHITS"]`. Note that if you used an alias when initializing the model, then the key is the value of the alias. ```python theme={null} explanations["NHITS"].keys() ``` ```text theme={null} dict_keys(['insample', 'futr_exog', 'hist_exog', 'stat_exog', 'baseline_predictions']) ``` From above, we can see that we have stored the attributions for each feature type as well as the baseline predictions. - `insample` contains the attributions for past lags and availability mask - `futr_exog` contains the attributions for future exogenous features - `hist_exog` contains the attributions for historical exogenous features - `stat_exog` contains the attributions for static exogenous features - `baseline_predictions` contains the baseline prediction of the model if none of the features above were available. Note that if the selected explainer does not have the additivity property, the value will be set to None. We will touch upon the additivity property in a later section. For now, just know that `IntegratedGradients` has the additive property, meaning that taking the sum of baseline predictions and feature attributions results in the final forecast made by the model. Now, because we are using Captum, we work directly with tensors, keeping the entire process fast, efficient, and allowing us to leverage GPUs when available. As such, the attributions are also stored as tensors as shown below. ```python theme={null} for key in list(explanations["NHITS"].keys()): print(f"Shape of {key}: {explanations['NHITS'][key].shape}") ``` ```text theme={null} Shape of insample: torch.Size([2, 12, 1, 1, 24, 2]) Shape of futr_exog: torch.Size([2, 12, 1, 1, 1, 36]) Shape of hist_exog: torch.Size([2, 12, 1, 1, 1, 24]) Shape of stat_exog: torch.Size([2, 12, 1, 1, 1]) Shape of baseline_predictions: torch.Size([2, 12, 1, 1]) ``` For each element above, the shape is defined as: * `insample`: \[batch\_size, horizon, n\_series, n\_output, input\_size, 2 (y attribution, mask attribution)] * `futr_exog`: \[batch\_size, horizon, n\_series, n\_output, n\_futr\_features, input\_size+horizon] * `hist_exog`: \[batch\_size, horizon, n\_series, n\_output, n\_hist\_features, input\_size] * `stat_exog`: \[batch\_size, horizon, n\_series, n\_output, n\_static\_features] * `baseline_predictions`: \[batch\_size, horizon, n\_series, n\_output] Here, `batch_size` is 2 for all, because we are explaining two different series. `n_series` however is 1 because NHITS is a univariate model. Also note that for `insample`, the last shape is always 2, because we score the attribution of the values of the past lags and their availability. At this point, we have all the information needed to analyze the attribution scores and make visualizations. ### Plotting feature attributions You can now use any method you want to plot feature attributions. You can make plots manually using any visualization library like `matplotlib` or `seaborn`, but `shap` has dedicated plots for explainability, so let’s see how we can use them. Basically, with the information we have, we can easily create a `shap.Explanation` object that can then be used to create different plots from the `shap` package. Specifically, a `shap.Explanation` object needs: - `values`: the attribution scores - `base_values`: the baseline predictions of the model - `feature_names`: a list to display nice feature names Here, let’s create a waterfall plot to visualize the attributions of each features, for the first series (Airline1), and for the first step in the horizon. ```python theme={null} import shap ``` ```python theme={null} batch_idx = 0 # Attributions for the first series (Airline1) horizon_idx = 0 # Attributions for the first horizon step output_idx = 0 attributions = [] feature_names = [] # Insample attributions y_attr = explanations["NHITS"]["insample"][batch_idx, horizon_idx, 0, output_idx, :, 0] mask_attr = explanations["NHITS"]["insample"][batch_idx, horizon_idx, 0, output_idx, :, 1] combined_insample = (y_attr + mask_attr).cpu().numpy() for i, attr in enumerate(combined_insample): attributions.append(attr) feature_names.append(f"y_lag{i+1}") # hist_exog attributions hist_attr = explanations["NHITS"]["hist_exog"][batch_idx, horizon_idx, 0, output_idx] hist_attr = hist_attr.cpu().numpy() # shape: (n_features, temporal) for t in range(hist_attr.shape[1]): # ← was shape[0] attributions.append(hist_attr[0, t]) # ← was hist_attr[t, 0] feature_names.append(f"y_lag12_t{t+1}") # futr_exog attributions futr_attr = explanations["NHITS"]["futr_exog"][batch_idx, horizon_idx, 0, output_idx] futr_attr = futr_attr.cpu().numpy() # shape: (n_features, temporal) for t in range(futr_attr.shape[1]): # ← was shape[0] attributions.append(futr_attr[0, t]) # ← was futr_attr[t, 0] if t < 24: feature_names.append(f"trend_hist_t{t+1}") else: feature_names.append(f"trend_futr_h{t-23}") # stat_exog attributions stat_attr = explanations["NHITS"]["stat_exog"][batch_idx, horizon_idx, 0, output_idx] attributions.append(float(stat_attr.cpu())) feature_names.append("airline1") shap_values = np.array(attributions) # baseline_predictions baseline = float(explanations["NHITS"]["baseline_predictions"][batch_idx, horizon_idx, 0, output_idx].cpu()) # Create SHAP Explanation shap_explanation = shap.Explanation( values=shap_values, base_values=baseline, feature_names=feature_names ) shap.plots.waterfall(shap_explanation) ``` As you can see, we now have a nice waterfall plot showing the baseline prediction, E\[f(X)] = 396.092, and how each features contributes to the final forecast f(x) = 412.804. Of course, we can do a wide variery of different plots from `shap`. For example, we can do a simple bar plot as shown below. ```python theme={null} shap.plots.bar(shap_explanation) ``` In both figures above, we have the breakdown of each feature at each timestep. This can make the plots crowded or it can be a level of granularity that is not necessary for analysis. So, you can also decide to combine all time steps together for a cleaner plot. ```python theme={null} batch_idx = 0 horizon_idx = 0 output_idx = 0 baseline = float(explanations["NHITS"]["baseline_predictions"][batch_idx, horizon_idx, output_idx, output_idx].cpu()) insample_sum = float(explanations["NHITS"]["insample"][batch_idx, horizon_idx, output_idx, output_idx, :, :].sum().cpu()) futr_exog_sum = 0 futr_exog_sum = float(explanations["NHITS"]["futr_exog"][batch_idx, horizon_idx, output_idx, output_idx, :, :].sum().cpu()) hist_exog_sum = 0 hist_exog_sum = float(explanations["NHITS"]["hist_exog"][batch_idx, horizon_idx, output_idx, output_idx, :, :].sum().cpu()) stat_exog_sum = 0 stat_exog_sum = float(explanations["NHITS"]["stat_exog"][batch_idx, horizon_idx, output_idx, output_idx, :].sum().cpu()) feature_names = [] shap_values = [] if insample_sum != 0: feature_names.append("Historical Y (all lags)") shap_values.append(insample_sum) if hist_exog_sum != 0: feature_names.append("Historical Exog (y_lag12)") shap_values.append(hist_exog_sum) if futr_exog_sum != 0: feature_names.append("Future Exog (trend)") shap_values.append(futr_exog_sum) if stat_exog_sum != 0: feature_names.append("Static (airline1)") shap_values.append(stat_exog_sum) shap_values = np.array(shap_values) # Create SHAP Explanation shap_explanation = shap.Explanation( values=shap_values, base_values=baseline, feature_names=feature_names ) shap.plots.waterfall(shap_explanation) ``` As you can see from the plot above, we have combined all inputs of each type of feature into a single category, so we can see how each overall feature contributes to the final forecast. ### Verifying additivity As mentioned above, “IntegratedGradients” has the additive property, meaning that when we sum the baseline predictions with the total attribution scores of each features, we get the final forecasts made by the model. ```python theme={null} attribution_predictions = [] # Process each series for batch_idx in range(2): # 2 series # Process each horizon for this series for horizon_idx in range(12): # horizon = 12 # Get baseline baseline = float(explanations["NHITS"]["baseline_predictions"][batch_idx, horizon_idx, 0, 0].cpu()) # Sum all attribution components total_attr = 0 # Insample (y + mask) insample_attr = explanations["NHITS"]["insample"][batch_idx, horizon_idx, 0, 0, :, :].sum() total_attr += float(insample_attr.cpu()) # Historical exogenous if explanations["NHITS"]["hist_exog"] is not None: hist_attr = explanations["NHITS"]["hist_exog"][batch_idx, horizon_idx, 0, 0, :, :].sum() total_attr += float(hist_attr.cpu()) # Future exogenous if explanations["NHITS"]["futr_exog"] is not None: futr_attr = explanations["NHITS"]["futr_exog"][batch_idx, horizon_idx, 0, 0, :, :].sum() total_attr += float(futr_attr.cpu()) # Static exogenous if explanations["NHITS"]["stat_exog"] is not None: stat_attr = explanations["NHITS"]["stat_exog"][batch_idx, horizon_idx, 0, 0, :].sum() total_attr += float(stat_attr.cpu()) # Compute final prediction from attributions pred_from_attr = baseline + total_attr attribution_predictions.append(pred_from_attr) # Add as new column to preds_df preds_df['NHITS_attribution'] = attribution_predictions ``` ```python theme={null} np.testing.assert_allclose( preds_df['NHITS'].values, preds_df['NHITS_attribution'].values, rtol=1e-3, err_msg="Attribution predictions do not match model predictions" ) ``` From the code above, we can see that reconstructed forecasts from the baseline predictions and attributions are within 0.1% of the original forecasts, so additivity is verified. ## Advanced concepts ### Choosing an explainer In this section, we outline the different explainers supported in *neuralforecast*. Different algorithms will produce different attribution scores, and so we must choose which applies best to our scenario. | Explainer | Local/Global | Additivity Property | Speed | | ---------------------- | ------------ | ------------------- | --------- | | Integrated Gradients | Local | Yes | Fast | | Shapley Value Sampling | Local | Yes | Very slow | | Input X Gradient | Local | No | Very fast | **Notes:** - **Local/Global**: All explainers are local, because they only explain how a specific input affects a specific forecast. - **Additivity Property**: Whether the sum of the feature attributions and baseline predictions result in the final forecast. - **Speed**: - Very fast: Single gradient computation - Fast: Multiple gradient computations (Integrated Gradients) - Medium: Multiple model evaluations - Slow: Many model evaluations for sampling-based methods - Very Slow: Exponential complexity in worst case (exact Shapley values) #### Integrated Gradients Integrated Gradients computes attributions by integrating gradients along the straight-line path from a chosen baseline input (e.g., black image, zero embedding) to the actual input. The method calculates the path integral, which is approximated using a Riemann sum with typically 20-300 gradient computations. Learn more in the [original paper](https://arxiv.org/pdf/1703.01365). **Advantages** - Theoretically grounded: Satisfies the axioms of sensitivity (features that affect the output get non-zero attribution) and implementation invariance (functionally equivalent networks produce identical attributions) - Has the additivity property **Disadvantages** - Relies on choosing an appropriate baseline that represents “absence of signal”. By default, we use a input only 0 values. #### Shapley Value Sampling Shapley Value Sampling approximates Shapley values using Monte Carlo sampling of feature permutations. The method randomly samples different orderings of features and computes how much each feature contributes by comparing model predictions when that feature is included versus excluded from the subset. The approach simulates “missing” features by drawing random values from the training data distribution. Learn more in the [original paper](https://www.sciencedirect.com/science/article/abs/pii/S0305054808000804). **Advantages** - All subsets of input features are perturbed, so interactions and redundancies between features are taken into account - Uses simple permutation sampling that is easy to understand **Disadvantages** - High computational cost: requires many model evaluations (typically hundreds to thousands) to achieve reasonable approximation accuracy - Very slow due to the high number of model evaluations - Simulates missing features by sampling from marginal distributions, which may create unrealistic data instances when features are correlated #### Input X Gradient Input X Gradient computes feature attribution by simply multiplying each input value by the gradient of the model output with respect to that input. This corresponds to a first-order Taylor approximation of how the output would change if the input were set to zero. This means each time step’s input values are multiplied by the gradients to show which historical observations most influence the prediction. Learn more in the [original paper](https://arxiv.org/pdf/1605.01713). **Advantages** - Computational efficiency: it requires only a single pass through the model - No approximations as it uses the gradient of the model **Disadvantages** - No additivity - A bit problematic with the ReLu function, because their gradient can be 0, but it can still carry some information - Functions like tanh or sigmoid can have very low gradients, even though the input is significant, so it’s problematic for LSTM and GRU models. ### Explaining models with different loss functions Currently, explanations are supported for models trained with: - Point loss functions (MAE, MSE, RMSE, etc.) - Non-parametric probabilistic losses (IQLoss, MQLoss, etc.) We do not support yet explaining models trained with parametric loss functions, like Normal, Student’s T, etc. For more information on the different loss functions supported in *neuralforecast*, read [here](https://nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/objectives.html). #### Explaning a model with a probablistic loss function If you are explaining a model with a non-parametric loss function, then by default, we only explain the median forecast. This is controlled by the `ouputs` parameter. Let’s see an example. ```python theme={null} %%capture # Initialize model models = [ NHITS( h=12, input_size=24, hist_exog_list=["y_[lag12]"], futr_exog_list=["trend"], stat_exog_list=['airline1'], loss=MQLoss(level=[80]), max_steps=20, scaler_type="robust", ), ] nf = NeuralForecast( models=models, freq="ME", ) # Fit model nf.fit( df=Y_train_df, static_df=AirPassengersStatic ) # Get predictions and attributions preds_df, explanations = nf.explain( outputs=[0], # Explain only the median forecast static_df=AirPassengersStatic, futr_df=futr_df, explainer="IntegratedGradients" ) ``` Above, by specifying `outputs=[0]`, which is the default value, we only explain the median forecast. However, we can explain up to three ouputs: 1. Median forecast 2. Lower bound 3. Upper bound As such, to explain all outputs, we must set `ouputs=[0,1,2]`. ```python theme={null} %%capture preds_df, explanations = nf.explain( outputs=[0, 1, 2], # Explain all outputs static_df=AirPassengersStatic, futr_df=futr_df, explainer="IntegratedGradients" ) ``` ```python theme={null} for key in list(explanations["NHITS"].keys()): print(f"Shape of {key}: {explanations['NHITS'][key].shape}") ``` ```text theme={null} Shape of insample: torch.Size([2, 12, 1, 3, 24, 2]) Shape of futr_exog: torch.Size([2, 12, 1, 3, 1, 36]) Shape of hist_exog: torch.Size([2, 12, 1, 3, 1, 24]) Shape of stat_exog: torch.Size([2, 12, 1, 3, 1]) Shape of baseline_predictions: torch.Size([2, 12, 1, 3]) ``` As you can see, the fourth dimension, which represents the number of ouputs, is now equal to 3, because we are explaining the median, the lower bound and upper bound. ### Explaining models with a scaler (`local_scaler_type`) If you specify a `local_scaler_type` in your `NeuralForecast` object, note that the attribution scores will be scaled. This is because the data is scaled before the training process. The relative importance is still relevant, but note that additivity will not hold. If additivtiy is important, then you must use `scaler_type` when initializing the model, as we do in this tutorial. This scales each window of data during training, so we can easily inverse transform the attribution scores. Again, no matter which approach you choose, the relative attribution scores are still valid and comparable. It’s only additivity that is impacted. If you specify a `local_scaler_type`, then a warning is issued about additivity. ### Explaining recurrent models You can explain recurrent models (LSTM, GRU). Just note that if you set `recurrent=True`, then the Integrated Gradients explainer is not supported. If `recurrent=False`, you can use any explainer. ### Explaining multivariate models You can also compute attribution scores for multivariate models. The particularity of this scenario is that all target series influence each other, so interpreting the results can be slightly more complex. Let’s go through an example using `MLPMultivariate`. ```python theme={null} %%capture # Initialize model models = [ MLPMultivariate( h=12, input_size=24, n_series=2, hist_exog_list=["y_[lag12]"], futr_exog_list=["trend"], stat_exog_list=['airline1'], max_steps=20, scaler_type="robust", ), ] nf = NeuralForecast( models=models, freq="ME", ) # Fit model nf.fit( df=Y_train_df, static_df=AirPassengersStatic ) # Get predictions and attributions preds_df, explanations = nf.explain( static_df=AirPassengersStatic, futr_df=futr_df, explainer="IntegratedGradients" ) ``` ```python theme={null} for key in list(explanations["MLPMultivariate"].keys()): print(f"Shape of {key}: {explanations['MLPMultivariate'][key].shape}") ``` ```text theme={null} Shape of insample: torch.Size([1, 12, 2, 1, 24, 2, 2]) Shape of futr_exog: torch.Size([1, 12, 2, 1, 1, 36, 2]) Shape of hist_exog: torch.Size([1, 12, 2, 1, 1, 24, 2]) Shape of stat_exog: torch.Size([1, 12, 2, 1, 2, 1]) Shape of baseline_predictions: torch.Size([1, 12, 2, 1]) ``` For each element above, the shape is defined as: * `insample`: \[batch\_size, horizon, n\_series, n\_output, input\_size, n\_series\_in, 2 (y attribution, mask attribution)] * `futr_exog`: \[batch\_size, horizon, n\_series, n\_output, n\_futr\_features, input\_size+horizon, n\_series\_in] * `hist_exog`: \[batch\_size, horizon, n\_series, n\_output, n\_hist\_features, input\_size, n\_series\_in] * `stat_exog`: \[batch\_size, horizon, n\_series, n\_output, n\_series\_in, n\_static\_features] * `baseline_predictions`: \[batch\_size, horizon, n\_series, n\_output] Note that in the multivariate case, we add a `n_series_in` dimension on every tensor, except `baseline_predictions` to indicate that we capture cross-series attribution. That way, we can see how each input series contribute to each output series. ```python theme={null} batch_idx = 0 horizon_idx = 0 output_idx = 0 model_name = "MLPMultivariate" expl = explanations[model_name] # Map series index to a readable name — adjust to your unique_ids series_names = ["Airline 1", "Airline 2"] for series_out_idx in range(expl["baseline_predictions"].shape[2]): baseline = float(expl["baseline_predictions"][batch_idx, horizon_idx, series_out_idx, output_idx].cpu()) feature_names = [] shap_values = [] # insample: [batch, h, n_series_out, n_outputs, input_size, n_series_in, 2] insample = expl["insample"][batch_idx, horizon_idx, series_out_idx, output_idx] for s_in in range(insample.shape[1]): # n_series_in val = float(insample[:, s_in, :].sum().cpu()) feature_names.append(f"Insample — {series_names[s_in]}") shap_values.append(val) # futr_exog: [batch, h, n_series_out, n_outputs, n_futr, L+h, n_series_in] if expl["futr_exog"] is not None: futr = expl["futr_exog"][batch_idx, horizon_idx, series_out_idx, output_idx] for s_in in range(futr.shape[2]): # n_series_in val = float(futr[:, :, s_in].sum().cpu()) feature_names.append(f"Future Exog — {series_names[s_in]}") shap_values.append(val) # hist_exog: [batch, h, n_series_out, n_outputs, n_hist, L, n_series_in] if expl["hist_exog"] is not None: hist = expl["hist_exog"][batch_idx, horizon_idx, series_out_idx, output_idx] for s_in in range(hist.shape[2]): # n_series_in val = float(hist[:, :, s_in].sum().cpu()) feature_names.append(f"Historical Exog — {series_names[s_in]}") shap_values.append(val) # stat_exog: [batch, h, n_series_out, n_outputs, n_series_in, S] if expl["stat_exog"] is not None: stat = expl["stat_exog"][batch_idx, horizon_idx, series_out_idx, output_idx] for s_in in range(stat.shape[0]): # n_series_in val = float(stat[s_in, :].sum().cpu()) feature_names.append(f"Static — {series_names[s_in]}") shap_values.append(val) shap_explanation = shap.Explanation( base_values=baseline, feature_names=feature_names, ) shap.plots.waterfall(shap_explanation, show=False) plt.title(f"Forecast attribution: {series_names[series_out_idx]} | Horizon {horizon_idx + 1}") plt.tight_layout() plt.show() ``` ## References M. Sundararajan, A. Taly, and Q. Yan, “Axiomatic Attribution for Deep Networks.” Available: [https://arxiv.org/pdf/1703.01365](https://arxiv.org/pdf/1703.01365) S. Lundberg, P. Allen, and S.-I. Lee, “A Unified Approach to Interpreting Model Predictions,” Nov. 2017. Available: [https://arxiv.org/pdf/1705.07874](https://arxiv.org/pdf/1705.07874) J. Castro, D. Gómez, and J. Tejada, “Polynomial calculation of the Shapley value based on sampling,” Computers & Operations Research, vol. 36, no. 5, pp. 1726–1730, May 2009, doi: [https://doi.org/10.1016/j.cor.2008.04.004](https://doi.org/10.1016/j.cor.2008.04.004). A. Shrikumar, P. Greenside, A. Shcherbina, and A. Kundaje, “Not Just a Black Box: Learning Important Features Through Propagating Activation Differences,” arXiv:1605.01713 \[cs], Apr. 2017, Available: [https://arxiv.org/abs/1605.01713](https://arxiv.org/abs/1605.01713) # Forecasting with TFT: Temporal Fusion Transformer Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/forecasting_tft.html Temporal Fusion Transformer (TFT) proposed by Lim et al. \[1] is one of the most popular transformer-based model for time-series forecasting. In summary, TFT combines gating layers, an LSTM recurrent encoder, with multi-head attention layers for a multi-step forecasting strategy decoder. For more details on the Nixtla’s TFT implementation visit [this link](https://nixtlaverse.nixtla.io/neuralforecast/models.tft.html). In this notebook we show how to train the TFT model on the Texas electricity market load data (ERCOT). Accurately forecasting electricity markets is of great interest, as it is useful for planning distribution and consumption. We will show you how to load the data, train the TFT performing automatic hyperparameter tuning, and produce forecasts. Then, we will show you how to perform multiple historical forecasts for cross validation. You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Libraries ```python theme={null} %%capture !pip install neuralforecast ``` ```python theme={null} import pandas as pd ``` ## 2. Load ERCOT Data The input to NeuralForecast is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestamp ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. We will rename the First, read the 2022 historic total demand of the ERCOT market. We processed the original data (available [here](https://www.ercot.com/gridinfo/load/load_hist)), by adding the missing hour due to daylight saving time, parsing the date to datetime format, and filtering columns of interest. ```python theme={null} Y_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/ERCOT-clean.csv') Y_df['ds'] = pd.to_datetime(Y_df['ds']) Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | ------------------- | ------------ | | 0 | ERCOT | 2021-01-01 00:00:00 | 43719.849616 | | 1 | ERCOT | 2021-01-01 01:00:00 | 43321.050347 | | 2 | ERCOT | 2021-01-01 02:00:00 | 43063.067063 | | 3 | ERCOT | 2021-01-01 03:00:00 | 43090.059203 | | 4 | ERCOT | 2021-01-01 04:00:00 | 43486.590073 | ## 3. Model training and forecast First, instantiate the `AutoTFT` model. The `AutoTFT` class will automatically perform hyperparameter tuning using [Tune library](https://docs.ray.io/en/latest/tune/index.html), exploring a user-defined or default search space. Models are selected based on the error on a validation set and the best model is then stored and used during inference. To instantiate `AutoTFT` you need to define: * `h`: forecasting horizon * `loss`: training loss * `config`: hyperparameter search space. If `None`, the `AutoTFT` class will use a pre-defined suggested hyperparameter space. * `num_samples`: number of configurations explored. ```python theme={null} from ray import tune from neuralforecast.auto import AutoTFT from neuralforecast.core import NeuralForecast from neuralforecast.losses.pytorch import MAE import logging logging.getLogger("pytorch_lightning").setLevel(logging.WARNING) ``` > **Tip** > > Increase the `num_samples` parameter to explore a wider set of > configurations for the selected models. As a rule of thumb choose it > to be bigger than `15`. > > With `num_samples=3` this example should run in around 20 minutes. ```python theme={null} horizon = 24 models = [AutoTFT(h=horizon, loss=MAE(), config=None, num_samples=3)] ``` > **Tip** > > All our models can be used for both point and probabilistic > forecasting. For producing probabilistic outputs, simply modify the > loss to one of our `DistributionLoss`. The complete list of losses is > available in [this > link](https://nixtlaverse.nixtla.io/neuralforecast/losses.pytorch.html) > **Important** > > TFT is a very large model and can require a lot of memory! If you are > running out of GPU memory, try declaring your config search space and > decrease the `hidden_size`, `n_heads`, and `windows_batch_size` > parameters. > > This are all the parameters of the config: > > ```python theme={null} > config = { > "input_size": tune.choice([horizon]), > "hidden_size": tune.choice([32]), > "n_head": tune.choice([2]), > "learning_rate": tune.loguniform(1e-4, 1e-1), > "scaler_type": tune.choice(['robust', 'standard']), > "max_steps": tune.choice([500, 1000]), > "windows_batch_size": tune.choice([32]), > "check_val_every_n_epoch": tune.choice([100]), > "random_seed": tune.randint(1, 20), > } > ``` The `NeuralForecast` class has built-in methods to simplify the forecasting pipelines, such as `fit`, `predit`, and `cross_validation`. Instantiate a `NeuralForecast` object with the following required parameters: * `models`: a list of models. * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) Then, use the `fit` method to train the `AutoTFT` model on the ERCOT data. The total training time will depend on the hardware and the explored configurations, it should take between 10 and 30 minutes. ```python theme={null} %%capture nf = NeuralForecast( models=models, freq='h') nf.fit(df=Y_df) ``` Finally, use the `predict` method to forecast the next 24 hours after the training data and plot the forecasts. ```python theme={null} Y_hat_df = nf.predict() Y_hat_df.head() ``` ```text theme={null} c:\Users\ospra\miniconda3\envs\neuralforecast\lib\site-packages\utilsforecast\processing.py:384: FutureWarning: 'H' is deprecated and will be removed in a future version, please use 'h' instead. freq = pd.tseries.frequencies.to_offset(freq) c:\Users\ospra\miniconda3\envs\neuralforecast\lib\site-packages\utilsforecast\processing.py:440: FutureWarning: 'H' is deprecated and will be removed in a future version, please use 'h' instead. freq = pd.tseries.frequencies.to_offset(freq) ``` ```text theme={null} Predicting: | | 0/? [00:00 ## 4. Cross validation for multiple historic forecasts The `cross_validation` method allows you to simulate multiple historic forecasts, greatly simplifying pipelines by replacing for loops with `fit` and `predict` methods. See [this tutorial](https://nixtlaverse.nixtla.io/statsforecast/docs/getting-started/getting_started_complete.html) for an animation of how the windows are defined. With time series data, cross validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The `cross_validation` method will use the validation set for hyperparameter selection, and will then produce the forecasts for the test set. Use the `cross_validation` method to produce all the daily forecasts for September. Set the validation and test sizes. To produce daily forecasts set the forecasting set the step size between windows as 24, to only produce one forecast per day. ```python theme={null} %%capture val_size = 90*24 # 90 days x 24 hours test_size = 30*24 # 30 days x 24 hours fcst_df = nf.cross_validation(df=Y_df, val_size=val_size, test_size=test_size, n_windows=None, step_size=horizon) ``` Finally, we merge the forecasts with the `Y_df` dataset and plot the forecasts. ```python theme={null} Y_hat_df = fcst_df.reset_index(drop=True) Y_hat_df = Y_hat_df.drop(columns=['y','cutoff']) ``` ```python theme={null} plot_df = Y_df.merge(Y_hat_df, on=['unique_id','ds'], how='outer').tail(test_size+24*7) plt.figure(figsize=(20,5)) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['AutoTFT'], c='blue', label='Forecast') plt.axvline(pd.to_datetime('2022-09-01'), color='red', linestyle='-.') plt.legend() plt.grid() plt.plot() ``` ## Next Steps In Challu et al \[2] we demonstrate that the N-HiTS model outperforms the latest transformers by more than 20% with 50 times less computation. Learn how to use the N-HiTS and the NeuralForecast library in [this tutorial](../use-cases/electricity_peak_forecasting.html). ## References \[1] [Lim, B., Arık, S. Ö., Loeff, N., & Pfister, T. (2021). Temporal fusion transformers for interpretable multi-horizon time series forecasting. International Journal of Forecasting, 37(4), 1748-1764.](https://www.sciencedirect.com/science/article/pii/S0169207021000637). \[2] [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). N-HiTS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # End to End Walkthrough | NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/getting_started_complete.html > Model training, evaluation and selection for multiple time series > **Prerequisites** > > This Guide assumes basic familiarity with NeuralForecast. For a > minimal example visit the [Quick > Start](../getting-started/quickstart.html) Follow this article for a step to step guide on building a production-ready forecasting pipeline for multiple time series. During this guide you will gain familiarity with the core `NeuralForecast`class and some relevant methods like `NeuralForecast.fit`, `NeuralForecast.predict`, and `StatsForecast.cross_validation.` We will use a classical benchmarking dataset from the M4 competition. The dataset includes time series from different domains like finance, economy and sales. In this example, we will use a subset of the Hourly dataset. We will model each time series globally Therefore, you will train a set of models for the whole dataset, and then select the best model for each individual time series. NeuralForecast focuses on speed, simplicity, and scalability, which makes it ideal for this task. **Outline:** 1. Install packages. 2. Read the data. 3. Explore the data. 4. Train many models globally for the entire dataset. 5. Evaluate the model’s performance using cross-validation. 6. Select the best model for every unique time series. > **Not Covered in this guide** > > * Using external regressors or exogenous variables > * Follow this tutorial to [include exogenous > variables](../capabilities/exogenous_variables.html) like > weather or holidays or static variables like category or > family. > * Probabilistic forecasting > * Follow this tutorial to [generate probabilistic > forecasts](../tutorials/uncertainty_quantification.html) > * Transfer Learning > * Train a model and use it to forecast on different data using > [this tutorial](../tutorials/transfer_learning.html) > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > > **Warning** > > To reduce the computation time, it is recommended to use GPU. Using > Colab, do not forget to activate it. Just go to > `Runtime>Change runtime type` and select GPU as hardware accelerator. ## 1. Install libraries We assume you have `NeuralForecast` already installed. Check this guide for instructions on [how to install NeuralForecast](../getting-started/installation.html). ```python theme={null} %%capture ! pip install neuralforecast ``` ## 2. Read the data We will use pandas to read the M4 Hourly data set stored in a parquet file for efficiency. You can use ordinary pandas operations to read your data in other formats likes `.csv`. The input to `NeuralForecast` is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestampe ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. We will rename the This data set already satisfies the requirement. Depending on your internet connection, this step should take around 10 seconds. ```python theme={null} import pandas as pd ``` ```python theme={null} Y_df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | This dataset contains 414 unique series with 900 observations on average. For this example and reproducibility’s sake, we will select only 10 unique IDs. Depending on your processing infrastructure feel free to select more or less series. > **Note** > > Processing time is dependent on the available computing resources. > Running this example with the complete dataset takes around 10 minutes > in a c5d.24xlarge (96 cores) instance from AWS. ```python theme={null} uids = Y_df['unique_id'].unique()[:10] # Select 10 ids to make the example faster Y_df = Y_df.query('unique_id in @uids').reset_index(drop=True) ``` ## 3. Explore Data with the plot\_series function Plot some series using the `plot_series` function from the `utilsforecast` library. This method prints 8 random series from the dataset and is useful for basic EDA. > **Note** > > The `plot_series` function uses matplotlib as a default engine. You > can change to plotly by setting `engine="plotly"`. ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} plot_series(Y_df) ``` ## 4. Train multiple models for many series `NeuralForecast` can train many models on many time series globally and efficiently. ```python theme={null} import logging import optuna import ray.tune as tune import torch from neuralforecast import NeuralForecast from neuralforecast.auto import AutoNHITS, AutoLSTM from neuralforecast.losses.pytorch import MQLoss ``` ```python theme={null} optuna.logging.set_verbosity(optuna.logging.WARNING) logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) torch.set_float32_matmul_precision('high') ``` Each `Auto` model contains a default search space that was extensively tested on multiple large-scale datasets. Additionally, users can define specific search spaces tailored for particular datasets and tasks. First, we create a custom search space for the `AutoNHITS` and `AutoLSTM` models. Search spaces are specified with dictionaries, where keys corresponds to the model’s hyperparameter and the value is a `Tune` function to specify how the hyperparameter will be sampled. For example, use `randint` to sample integers uniformly, and `choice` to sample values of a list. ```python theme={null} def config_nhits(trial): return { "input_size": trial.suggest_categorical( # Length of input window "input_size", (48, 48*2, 48*3) ), "start_padding_enabled": True, "n_blocks": 5 * [1], # Length of input window "mlp_units": 5 * [[64, 64]], # Length of input window "n_pool_kernel_size": trial.suggest_categorical( # MaxPooling Kernel size "n_pool_kernel_size", (5*[1], 5*[2], 5*[4], [8, 4, 2, 1, 1]) ), "n_freq_downsample": trial.suggest_categorical( # Interpolation expressivity ratios "n_freq_downsample", ([8, 4, 2, 1, 1], [1, 1, 1, 1, 1]) ), "learning_rate": trial.suggest_float( # Initial Learning rate "learning_rate", low=1e-4, high=1e-2, log=True, ), "scaler_type": None, # Scaler type "max_steps": 1000, # Max number of training iterations "batch_size": trial.suggest_categorical( # Number of series in batch "batch_size", (1, 4, 10), ), "windows_batch_size": trial.suggest_categorical( # Number of windows in batch "windows_batch_size", (128, 256, 512), ), "random_seed": trial.suggest_int( # Random seed "random_seed", low=1, high=20, ), } def config_lstm(trial): return { "input_size": trial.suggest_categorical( # Length of input window "input_size", (48, 48*2, 48*3) ), "encoder_hidden_size": trial.suggest_categorical( # Hidden size of LSTM cells "encoder_hidden_size", (64, 128), ), "encoder_n_layers": trial.suggest_categorical( # Number of layers in LSTM "encoder_n_layers", (2,4), ), "learning_rate": trial.suggest_float( # Initial Learning rate "learning_rate", low=1e-4, high=1e-2, log=True, ), "scaler_type": 'robust', # Scaler type "max_steps": trial.suggest_categorical( # Max number of training iterations "max_steps", (500, 1000) ), "batch_size": trial.suggest_categorical( # Number of series in batch "batch_size", (1, 4) ), "random_seed": trial.suggest_int( # Random seed "random_seed", low=1, high=20 ), } ``` To instantiate an `Auto` model you need to define: * `h`: forecasting horizon. * `loss`: training and validation loss from `neuralforecast.losses.pytorch`. * `config`: hyperparameter search space. If `None`, the `Auto` class will use a pre-defined suggested hyperparameter space. * `search_alg`: search algorithm * `num_samples`: number of configurations explored. In this example we set horizon `h` as 48, use the `MQLoss` distribution loss for training and validation, and use the default search algorithm. ```python theme={null} nf = NeuralForecast( models=[ AutoNHITS(h=48, config=config_nhits, loss=MQLoss(), backend='optuna', num_samples=5), AutoLSTM(h=48, config=config_lstm, loss=MQLoss(), backend='optuna', num_samples=2), ], freq=1, ) ``` > **Tip** > > The number of samples, `num_samples`, is a crucial parameter! Larger > values will usually produce better results as we explore more > configurations in the search space, but it will increase training > times. Larger search spaces will usually require more samples. As a > general rule, we recommend setting `num_samples` higher than 20. Next, we use the `Neuralforecast` class to train the `Auto` model. In this step, `Auto` models will automatically perform hyperparameter tuning training multiple models with different hyperparameters, producing the forecasts on the validation set, and evaluating them. The best configuration is selected based on the error on a validation set. Only the best model is stored and used during inference. ```python theme={null} %%capture nf.fit(df=Y_df) ``` Next, we use the `predict` method to forecast the next 48 days using the optimal hyperparameters. ```python theme={null} fcst_df = nf.predict() fcst_df.columns = fcst_df.columns.str.replace('-median', '') fcst_df.head() ``` ```python theme={null} plot_series(Y_df, fcst_df, plot_random=False, max_insample_length=48 * 3, level=[80, 90]) ``` The `plot_series` function allows for further customization. For example, plot the results of the different models and unique ids. ```python theme={null} # Plot to unique_ids and some selected models plot_series(Y_df, fcst_df, models=["AutoLSTM"], ids=["H107", "H104"], level=[80, 90]) ``` ```python theme={null} # Explore other models plot_series(Y_df, fcst_df, models=["AutoNHITS"], ids=["H10", "H105"], level=[80, 90]) ``` ## 5. Evaluate the model’s performance In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, **Cross Validation** is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) > **Tip** > > Setting `n_windows=1` mirrors a traditional train-test split with our > historical data serving as the training set and the last 48 hours > serving as the testing set. The `cross_validation` method from the `NeuralForecast` class takes the following arguments. * `df`: training data frame * `step_size` (int): step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows` (int): number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} from neuralforecast.auto import AutoNHITS, AutoLSTM ``` ```python theme={null} nf = NeuralForecast( models=[ AutoNHITS(h=48, config=config_nhits, loss=MQLoss(), num_samples=5, backend="optuna"), AutoLSTM(h=48, config=config_lstm, loss=MQLoss(), num_samples=2, backend="optuna"), ], freq=1, ) ``` ```python theme={null} %%capture cv_df = nf.cross_validation(Y_df, n_windows=2) ``` The `cv_df` object is a new data frame that includes the following columns: * `unique_id`: identifies each time series * `ds`: datestamp or temporal index * `cutoff`: the last datestamp or temporal index for the n\_windows. If n\_windows=1, then one unique cuttoff value, if n\_windows=2 then two unique cutoff values. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_df.columns = cv_df.columns.str.replace('-median', '') ``` ```python theme={null} cv_df.head() ``` | | unique\_id | ds | cutoff | AutoNHITS | AutoNHITS-lo-90 | AutoNHITS-lo-80 | AutoNHITS-hi-80 | AutoNHITS-hi-90 | AutoLSTM | AutoLSTM-lo-90 | AutoLSTM-lo-80 | AutoLSTM-hi-80 | AutoLSTM-hi-90 | y | | - | ---------- | --- | ------ | ---------- | --------------- | --------------- | --------------- | --------------- | ---------- | -------------- | -------------- | -------------- | -------------- | ----- | | 0 | H1 | 700 | 699 | 654.506348 | 615.993774 | 616.021851 | 693.879272 | 712.376587 | 777.396362 | 511.052124 | 585.006470 | 992.880249 | 1084.980957 | 684.0 | | 1 | H1 | 701 | 699 | 619.320068 | 573.836060 | 577.762695 | 663.133301 | 683.214478 | 691.002991 | 417.614349 | 488.192810 | 905.101135 | 1002.091919 | 619.0 | | 2 | H1 | 702 | 699 | 546.807922 | 486.383362 | 498.541748 | 599.284302 | 623.889038 | 569.914795 | 314.173462 | 389.398865 | 763.250244 | 852.974121 | 565.0 | | 3 | H1 | 703 | 699 | 483.149811 | 420.416351 | 435.613708 | 536.380005 | 561.349487 | 548.401917 | 305.305054 | 379.597839 | 732.263123 | 817.543152 | 532.0 | | 4 | H1 | 704 | 699 | 434.347931 | 381.605713 | 394.665619 | 481.329041 | 501.715546 | 511.798950 | 269.810272 | 346.146484 | 692.443542 | 776.531921 | 495.0 | ```python theme={null} from IPython.display import display ``` ```python theme={null} for cutoff in cv_df['cutoff'].unique(): display( plot_series( Y_df, cv_df.query('cutoff == @cutoff').drop(columns=['y', 'cutoff']), max_insample_length=48 * 4, ids=['H102'], ) ) ``` Now, let’s evaluate the models’ performance. ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import mse, mae, rmse ``` > **Warning** > > You can also use Mean Average Percentage Error (MAPE), however for > granular forecasts, MAPE values are extremely [hard to > judge](https://medium.com/@maltetichy/mean-absolute-percentage-error-mape-has-served-its-duty-and-should-now-retire-ff7d6dfb8a1d) > and not useful to assess forecasting quality. Create the data frame with the results of the evaluation of your cross-validation data frame using a Mean Squared Error metric. ```python theme={null} evaluation_df = evaluate(cv_df.drop(columns='cutoff'), metrics=[mse, mae, rmse]) evaluation_df['best_model'] = evaluation_df.drop(columns=['metric', 'unique_id']).idxmin(axis=1) evaluation_df.head() ``` | | unique\_id | metric | AutoNHITS | AutoLSTM | best\_model | | - | ---------- | ------ | ------------ | ------------ | ----------- | | 0 | H1 | mse | 2295.630068 | 1889.340182 | AutoLSTM | | 1 | H10 | mse | 724.468906 | 362.463659 | AutoLSTM | | 2 | H100 | mse | 62943.031250 | 17063.347107 | AutoLSTM | | 3 | H101 | mse | 48771.973540 | 12213.554997 | AutoLSTM | | 4 | H102 | mse | 30671.342050 | 84569.434859 | AutoNHITS | Create a summary table with a model column and the number of series where that model performs best. ```python theme={null} summary_df = evaluation_df.groupby(['metric', 'best_model']).size().sort_values().to_frame() summary_df = summary_df.reset_index() summary_df.columns = ['metric', 'model', 'nr. of unique_ids'] summary_df ``` | | metric | model | nr. of unique\_ids | | - | ------ | --------- | ------------------ | | 0 | mae | AutoNHITS | 3 | | 1 | mse | AutoNHITS | 4 | | 2 | rmse | AutoNHITS | 4 | | 3 | mse | AutoLSTM | 6 | | 4 | rmse | AutoLSTM | 6 | | 5 | mae | AutoLSTM | 7 | ```python theme={null} summary_df.query('metric == "mse"') ``` | | metric | model | nr. of unique\_ids | | - | ------ | --------- | ------------------ | | 1 | mse | AutoNHITS | 4 | | 3 | mse | AutoLSTM | 6 | You can further explore your results by plotting the unique\_ids where a specific model wins. ```python theme={null} nhits_ids = evaluation_df.query('best_model == "AutoNHITS" and metric == "mse"')['unique_id'].unique() plot_series(Y_df, fcst_df, ids=nhits_ids) ``` ## 6. Select the best model for every unique series Define a utility function that takes your forecast’s data frame with the predictions and the evaluation data frame and returns a data frame with the best possible forecast for every unique\_id. ```python theme={null} def get_best_model_forecast(forecasts_df, evaluation_df, metric): metric_eval = evaluation_df.loc[evaluation_df['metric'] == metric, ['unique_id', 'best_model']] with_best = forecasts_df.merge(metric_eval) res = with_best[['unique_id', 'ds']].copy() for suffix in ('', '-lo-90', '-hi-90'): res[f'best_model{suffix}'] = with_best.apply(lambda row: row[row['best_model'] + suffix], axis=1) return res ``` Create your production-ready data frame with the best forecast for every unique\_id. ```python theme={null} prod_forecasts_df = get_best_model_forecast(fcst_df, evaluation_df, metric='mse') prod_forecasts_df ``` | | unique\_id | ds | best\_model | best\_model-lo-90 | best\_model-hi-90 | | --- | ---------- | --- | ----------- | ----------------- | ----------------- | | 0 | H1 | 749 | 603.923767 | 437.270447 | 786.502686 | | 1 | H1 | 750 | 533.691284 | 383.289154 | 702.944397 | | 2 | H1 | 751 | 490.400085 | 349.417816 | 648.831299 | | 3 | H1 | 752 | 463.768066 | 327.452026 | 616.572144 | | 4 | H1 | 753 | 454.710266 | 320.023468 | 605.468018 | | ... | ... | ... | ... | ... | ... | | 475 | H107 | 792 | 4720.256348 | 4142.459961 | 5235.727051 | | 476 | H107 | 793 | 4394.605469 | 3952.059082 | 4992.124023 | | 477 | H107 | 794 | 4161.221191 | 3664.091553 | 4632.160645 | | 478 | H107 | 795 | 3945.432617 | 3453.011963 | 4437.968750 | | 479 | H107 | 796 | 3666.446045 | 3177.937744 | 4059.684570 | Plot the results. ```python theme={null} plot_series(Y_df, prod_forecasts_df, level=[90]) ``` # Hierarchical Forecast | NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/hierarchical_forecasting.html > A minimal example of using Hierarchical Forecast with NeuralForecast This notebook offers a step by step guide to create a hierarchical forecasting pipeline. In the pipeline we will use `NeuralForecast` and `HINT` class, to create fit, predict and reconcile forecasts. We will use the TourismL dataset that summarizes large Australian national visitor survey. Outline
1. Installing packages
2. Load hierarchical dataset
3\. Fit and Predict HINT
4. Benchmark methods
5. Forecast Evaluation You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing packages ```python theme={null} %%capture !pip install datasetsforecast hierarchicalforecast neuralforecast statsforecast ``` ## 2. Load hierarchical dataset This detailed Australian Tourism Dataset comes from the National Visitor Survey, managed by the Tourism Research Australia, it is composed of 555 monthly series from 1998 to 2016, it is organized geographically, and purpose of travel. The natural geographical hierarchy comprises seven states, divided further in 27 zones and 76 regions. The purpose of travel categories are holiday, visiting friends and relatives (VFR), business and other. The MinT (Wickramasuriya et al., 2019), among other hierarchical forecasting studies has used the dataset it in the past. The dataset can be accessed in the [MinT reconciliation webpage](https://robjhyndman.com/publications/mint/), although other sources are available. | Geographical Division | Number of series per division | Number of series per purpose | Total | | --------------------- | ----------------------------- | ---------------------------- | ----- | | Australia | 1 | 4 | 5 | | States | 7 | 28 | 35 | | Zones | 27 | 108 | 135 | | Regions | 76 | 304 | 380 | | Total | 111 | 444 | 555 | ```python theme={null} import pandas as pd from datasetsforecast.hierarchical import HierarchicalData from hierarchicalforecast.utils import aggregate, HierarchicalPlot from neuralforecast.utils import augment_calendar_df from utilsforecast.plotting import plot_series ``` ```python theme={null} # Load hierarchical dataset Y_df, S_df, tags = HierarchicalData.load('./data', 'TourismLarge') Y_df['ds'] = pd.to_datetime(Y_df['ds']) Y_df, _ = augment_calendar_df(df=Y_df, freq='M') S_df = S_df.reset_index(names="unique_id") ``` Mathematically a hierarchical multivariate time series can be denoted by the vector $\mathbf{y}_{[a,b],t}$ defined by the following aggregation constraint: $$ \mathbf{y}_{[a,b],t} = \mathbf{S}_{[a,b][b]} \mathbf{y}_{[b],t} \quad \Leftrightarrow \quad \begin{bmatrix}\mathbf{y}_{[a],t} \\ %\hline \mathbf{y}_{[b],t}\end{bmatrix} = \begin{bmatrix} \mathbf{A}_{[a][b]}\\ %\hline \mathbf{I}_{[b][b]} \end{bmatrix} \mathbf{y}_{[b],t} $$ where $\mathbf{y}_{[a],t}$ are the aggregate series, $\mathbf{y}_{[b],t}$ are the bottom level series and $\mathbf{S}_{[a,b][b]}$ are the hierarchical aggregation constraints. ```python theme={null} # Here we plot the hierarchical constraints matrix hplot = HierarchicalPlot(S=S_df, tags=tags) hplot.plot_summing_matrix() ``` ```python theme={null} plot_series(forecasts_df=Y_df[["unique_id", "ds", "y"]], ids=['TotalAll']) ``` ## 3. Fit and Predict HINT The Hierarchical Forecast Network (HINT) combines into an easy to use model three components:
1. SoTA neural forecast model.
2. An efficient and flexible multivariate probability distribution.
3. Builtin reconciliation capabilities.
```python theme={null} import logging import numpy as np from neuralforecast import NeuralForecast from neuralforecast.models import NHITS, HINT from neuralforecast.losses.pytorch import GMM, sCRPS ``` ```python theme={null} # Train test splits horizon = 12 Y_test_df = Y_df.groupby('unique_id', observed=True).tail(horizon) Y_train_df = Y_df.drop(Y_test_df.index) ``` ```python theme={null} # Horizon and quantiles level = np.arange(0, 100, 2) qs = [[50-lv/2, 50+lv/2] if lv!=0 else [50] for lv in level] quantiles = np.sort(np.concatenate(qs)/100) # HINT := BaseNetwork + Distribution + Reconciliation nhits = NHITS(h=horizon, input_size=24, loss=GMM(n_components=10, quantiles=quantiles), hist_exog_list=['month'], max_steps=2000, early_stop_patience_steps=10, val_check_steps=50, scaler_type='robust', learning_rate=1e-3, valid_loss=sCRPS(quantiles=quantiles)) model = HINT(h=horizon, S=S_df.drop(columns='unique_id').values, model=nhits, reconciliation='BottomUp') ``` ```text theme={null} INFO:lightning_fabric.utilities.seed:Seed set to 1 ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) ``` ```python theme={null} %%capture Y_df['y'] = Y_df['y'] * (Y_df['y'] > 0) nf = NeuralForecast(models=[model], freq='MS') nf.fit(df=Y_train_df, val_size=12) Y_hat_df = nf.predict() Y_hat_df = Y_hat_df.rename(columns=lambda x: x.replace('.0', '')) ``` ```python theme={null} plot_series( Y_df, Y_hat_df.drop(columns='NHITS-median'), ids=['TotalAll'], level=[90], max_insample_length=12*5, ) ``` ## 4. Benchmark methods We compare against AutoARIMA, a well-established traditional forecasting method from the [StatsForecast](../../../statsforecast/index.html) package, for which we reconcile the forecasts using [HierarchicalForecast](../../../hierarchicalforecast/index.html). ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoARIMA from hierarchicalforecast.methods import BottomUp, MinTrace from hierarchicalforecast.core import HierarchicalReconciliation ``` We define the model, and create the forecasts. ```python theme={null} sf = StatsForecast(models=[AutoARIMA()], freq='MS', n_jobs=-1) Y_hat_df_arima = sf.forecast(df=Y_train_df, h=12, fitted=True, X_df=Y_test_df.drop(columns="y"), level = np.arange(2, 100, 2)) Y_fitted_df_arima = sf.forecast_fitted_values() ``` Next, we reconcile the forecasts using `BottomUp` and `MinTrace` reconciliation techniques: ```python theme={null} reconcilers = [ BottomUp(), MinTrace(method='mint_shrink'), ] hrec = HierarchicalReconciliation(reconcilers=reconcilers) Y_rec_df = hrec.reconcile(Y_hat_df=Y_hat_df_arima, Y_df=Y_fitted_df_arima, S=S_df, tags=tags, level = np.arange(2, 100, 2), intervals_method="bootstrap") ``` ## 5. Forecast Evaluation To evaluate the coherent probabilistic predictions we use the scaled Continuous Ranked Probability Score (sCRPS), defined as follows: $$ \mathrm{CRPS}(\hat{F}_{[a,b],\tau},\mathbf{y}_{[a,b],\tau}) = \frac{2}{N_{a}+N_{b}} \sum_{i} \int^{1}_{0} \mathrm{QL}(\hat{F}_{i,\tau}, y_{i,\tau})_{q} dq $$ $$ \mathrm{sCRPS}(\hat{F}_{[a,b\,],\tau},\mathbf{y}_{[a,b\,],\tau}) = \frac{\mathrm{CRPS}(\hat{F}_{[a,b\,],\tau},\mathbf{y}_{[a,b\,],\tau})}{\sum_{i} | y_{i,\tau} |} $$ As you can see the HINT model (using NHITS as base model) efficiently achieves state of the art accuracy under minimal tuning. ```python theme={null} from utilsforecast.losses import scaled_crps from hierarchicalforecast.evaluation import evaluate ``` ```python theme={null} df_metrics = Y_hat_df.merge(Y_test_df.drop(columns="month"), on=['unique_id', 'ds']) df_metrics = df_metrics.merge(Y_rec_df, on=['unique_id', 'ds']) metrics = evaluate(df = df_metrics, tags = tags, metrics = [scaled_crps], models= ["NHITS", "AutoARIMA"], level = np.arange(2, 100, 2), train_df = Y_train_df.drop(columns="month"), ) metrics ``` | | level | metric | NHITS | AutoARIMA | | - | --------------------------------- | ------------ | -------- | --------- | | 0 | Country | scaled\_crps | 0.044431 | 0.131136 | | 1 | Country/State | scaled\_crps | 0.063411 | 0.147516 | | 2 | Country/State/Zone | scaled\_crps | 0.106060 | 0.174071 | | 3 | Country/State/Zone/Region | scaled\_crps | 0.151988 | 0.205654 | | 4 | Country/Purpose | scaled\_crps | 0.075821 | 0.133664 | | 5 | Country/State/Purpose | scaled\_crps | 0.114674 | 0.181850 | | 6 | Country/State/Zone/Purpose | scaled\_crps | 0.180491 | 0.244324 | | 7 | Country/State/Zone/Region/Purpose | scaled\_crps | 0.245466 | 0.310656 | | 8 | Overall | scaled\_crps | 0.122793 | 0.191109 | ## References * [Kin G. Olivares, David Luo, Cristian Challu, Stefania La Vattiata, Max Mergenthaler, Artur Dubrawski (2023). “HINT: Hierarchical Mixture Networks For Coherent Probabilistic Forecasting”. International Conference on Machine Learning (ICML). Workshop on Structured Probabilistic Inference & Generative Modeling. Available at https://arxiv.org/abs/2305.07089.](https://arxiv.org/abs/2305.07089)
* [Kin G. Olivares, O. Nganba Meetei, Ruijun Ma, Rohan Reddy, Mengfei Cao, Lee Dicker (2023).”Probabilistic Hierarchical Forecasting with Deep Poisson Mixtures”. International Journal Forecasting, accepted paper. URL https://arxiv.org/pdf/2110.13179.pdf.](https://arxiv.org/pdf/2110.13179.pdf)
* [Kin G. Olivares, Federico Garza, David Luo, Cristian Challu, Max Mergenthaler, Souhaib Ben Taieb, Shanika Wickramasuriya, and Artur Dubrawski (2023). “HierarchicalForecast: A reference framework for hierarchical forecasting”. Journal of Machine Learning Research, submitted. URL https://arxiv.org/abs/2207.03517](https://arxiv.org/abs/2207.03517) # Intermittent Data Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/intermittent_data.html > In this notebook, we’ll implement models for intermittent or sparse > data using the M5 dataset. Intermittent or sparse data has very few non-zero observations. This type of data is hard to forecast because the zero values increase the uncertainty about the underlying patterns in the data. Furthermore, once a non-zero observation occurs, there can be considerable variation in its size. Intermittent time series are common in many industries, including finance, retail, transportation, and energy. Given the ubiquity of this type of series, special methods have been developed to forecast them. The first was from [Croston (1972)](#ref), followed by several variants and by different aggregation frameworks. The models of [NeuralForecast](https://nixtlaverse.nixtla.io/statsforecast/) can be trained to model sparse or intermittent time series using a `Poisson` distribution loss. By the end of this tutorial, you’ll have a good understanding of these models and how to use them. **Outline:** 1. Install libraries 2. Load and explore the data 3. Train models for intermittent data 4. Perform Cross Validation > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > > **Warning** > > To reduce the computation time, it is recommended to use GPU. Using > Colab, do not forget to activate it. Just go to > `Runtime>Change runtime type` and select GPU as hardware accelerator. ## 1. Install libraries We assume that you have NeuralForecast already installed. If not, check this guide for instructions on [how to install NeuralForecast](https://nixtlaverse.nixtla.io/neuralforecast/docs/getting-started/installation.html) Install the necessary packages using `pip install neuralforecast` ```python theme={null} %%capture !pip install statsforecast s3fs fastparquet neuralforecast ``` ## 2. Load and explore the data For this example, we’ll use a subset of the [M5 Competition](https://www.sciencedirect.com/science/article/pii/S0169207021001187#:~:text=The%20objective%20of%20the%20M5,the%20uncertainty%20around%20these%20forecasts) dataset. Each time series represents the unit sales of a particular product in a given Walmart store. At this level (product-store), most of the data is intermittent. We first need to import the data. ```python theme={null} import pandas as pd from utilsforecast.plotting import plot_series ``` ```python theme={null} Y_df = pd.read_parquet('https://m5-benchmarks.s3.amazonaws.com/data/train/target.parquet') Y_df = Y_df.rename(columns={ 'item_id': 'unique_id', 'timestamp': 'ds', 'demand': 'y' }) Y_df['ds'] = pd.to_datetime(Y_df['ds']) ``` For simplicity sake we will keep just one category ```python theme={null} Y_df = Y_df.query('unique_id.str.startswith("FOODS_3")') Y_df['unique_id'] = Y_df['unique_id'].astype(str) Y_df = Y_df.reset_index(drop=True) ``` Plot some series using the plot method from the `StatsForecast` class. This method prints 8 random series from the dataset and is useful for basic [EDA](https://nixtlaverse.nixtla.io/statsforecast/src/core/core.html#statsforecast.plot). ```python theme={null} plot_series(Y_df) ``` ## 3. Train models for intermittent data ```python theme={null} from ray import tune from neuralforecast import NeuralForecast from neuralforecast.auto import AutoNHITS, AutoTFT from neuralforecast.losses.pytorch import DistributionLoss ``` Each `Auto` model contains a default search space that was extensively tested on multiple large-scale datasets. Additionally, users can define specific search spaces tailored for particular datasets and tasks. First, we create a custom search space for the `AutoNHITS` and `AutoTFT` models. Search spaces are specified with dictionaries, where keys corresponds to the model’s hyperparameter and the value is a `Tune` function to specify how the hyperparameter will be sampled. For example, use `randint` to sample integers uniformly, and `choice` to sample values of a list. ```python theme={null} config_nhits = { "input_size": tune.choice([28, 28*2, 28*3, 28*5]), # Length of input window "n_blocks": 5*[1], # Length of input window "mlp_units": 5 * [[512, 512]], # Length of input window "n_pool_kernel_size": tune.choice([5*[1], 5*[2], 5*[4], [8, 4, 2, 1, 1]]), # MaxPooling Kernel size "n_freq_downsample": tune.choice([[8, 4, 2, 1, 1], [1, 1, 1, 1, 1]]), # Interpolation expressivity ratios "learning_rate": tune.loguniform(1e-4, 1e-2), # Initial Learning rate "scaler_type": tune.choice([None]), # Scaler type "max_steps": tune.choice([1000]), # Max number of training iterations "batch_size": tune.choice([32, 64, 128, 256]), # Number of series in batch "windows_batch_size": tune.choice([128, 256, 512, 1024]), # Number of windows in batch "random_seed": tune.randint(1, 20), # Random seed } config_tft = { "input_size": tune.choice([28, 28*2, 28*3]), # Length of input window "hidden_size": tune.choice([64, 128, 256]), # Size of embeddings and encoders "learning_rate": tune.loguniform(1e-4, 1e-2), # Initial learning rate "scaler_type": tune.choice([None]), # Scaler type "max_steps": tune.choice([500, 1000]), # Max number of training iterations "batch_size": tune.choice([32, 64, 128, 256]), # Number of series in batch "windows_batch_size": tune.choice([128, 256, 512, 1024]), # Number of windows in batch "random_seed": tune.randint(1, 20), # Random seed } ``` To instantiate an `Auto` model you need to define: * `h`: forecasting horizon. * `loss`: training and validation loss from `neuralforecast.losses.pytorch`. * `config`: hyperparameter search space. If `None`, the `Auto` class will use a pre-defined suggested hyperparameter space. * `search_alg`: search algorithm (from `tune.search`), default is random search. Refer to [https://docs.ray.io/en/latest/tune/api\_docs/suggestion.html](https://docs.ray.io/en/latest/tune/api_docs/suggestion.html) for more information on the different search algorithm options. * `num_samples`: number of configurations explored. In this example we set horizon `h` as 28, use the `Poisson` distribution loss (ideal for count data) for training and validation, and use the default search algorithm. ```python theme={null} nf = NeuralForecast( models=[ AutoNHITS(h=28, config=config_nhits, loss=DistributionLoss(distribution='Poisson', level=[80, 90]), num_samples=5), AutoTFT(h=28, config=config_tft, loss=DistributionLoss(distribution='Poisson', level=[80, 90]), num_samples=2), ], freq='D' ) ``` > **Tip** > > The number of samples, `num_samples`, is a crucial parameter! Larger > values will usually produce better results as we explore more > configurations in the search space, but it will increase training > times. Larger search spaces will usually require more samples. As a > general rule, we recommend setting `num_samples` higher than 20. Next, we use the `Neuralforecast` class to train the `Auto` model. In this step, `Auto` models will automatically perform hyperparameter tuning, training multiple models with different hyperparameters, producing the forecasts on the validation set, and evaluating them. The best configuration is selected based on the error on a validation set. Only the best model is stored and used during inference. ```python theme={null} %%capture nf.fit(df=Y_df) ``` Next, we use the `predict` method to forecast the next 28 days using the optimal hyperparameters. ```python theme={null} fcst_df = nf.predict() ``` ```text theme={null} GPU available: True (cuda), used: True TPU available: False, using: 0 TPU cores HPU available: False, using: 0 HPUs LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] GPU available: True (cuda), used: True TPU available: False, using: 0 TPU cores HPU available: False, using: 0 HPUs LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] ``` ```text theme={null} Predicting: | | 0/? [00:00 ## 4. Cross Validation Time series cross-validation is a method for evaluating how a model would have performed in the past. It works by defining a sliding window across the historical data and predicting the period following it. ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) [NeuralForecast](https://nixtlaverse.nixtla.io/neuralforecast/) has an implementation of time series cross-validation that is fast and easy to use. The `cross_validation` method from the `NeuralForecast` class takes the following arguments. * `df`: training data frame * `step_size` (int): step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows` (int): number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} nf = NeuralForecast( models=[ AutoNHITS(h=28, config=config_nhits, loss=DistributionLoss(distribution='Poisson', level=[80, 90]), num_samples=5), AutoTFT(h=28, config=config_tft, loss=DistributionLoss(distribution='Poisson', level=[80, 90]), num_samples=2), ], freq='D' ) ``` ```python theme={null} %%capture cv_df = nf.cross_validation(Y_df, n_windows=3, step_size=28) ``` The `cv_df` object is a new data frame that includes the following columns: * `unique_id`: contains the id corresponding to the time series * `ds`: datestamp or temporal index * `cutoff`: the last datestamp or temporal index for the n\_windows. If n\_windows=1, then one unique cuttoff value, if n\_windows=2 then two unique cutoff values. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_df.head() ``` | | unique\_id | ds | cutoff | AutoNHITS | AutoNHITS-median | AutoNHITS-lo-90 | AutoNHITS-lo-80 | AutoNHITS-hi-80 | AutoNHITS-hi-90 | AutoTFT | AutoTFT-median | AutoTFT-lo-90 | AutoTFT-lo-80 | AutoTFT-hi-80 | AutoTFT-hi-90 | y | | - | -------------------- | ---------- | ---------- | --------- | ---------------- | --------------- | --------------- | --------------- | --------------- | ------- | -------------- | ------------- | ------------- | ------------- | ------------- | --- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-02-29 | 2016-02-28 | 0.550 | 0.0 | 0.0 | 0.0 | 2.0 | 2.0 | 0.775 | 1.0 | 0.0 | 0.0 | 2.0 | 2.0 | 0.0 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-03-01 | 2016-02-28 | 0.611 | 0.0 | 0.0 | 0.0 | 2.0 | 2.0 | 0.746 | 1.0 | 0.0 | 0.0 | 2.0 | 2.0 | 1.0 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-03-02 | 2016-02-28 | 0.567 | 0.0 | 0.0 | 0.0 | 2.0 | 2.0 | 0.750 | 1.0 | 0.0 | 0.0 | 2.0 | 2.0 | 1.0 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-03-03 | 2016-02-28 | 0.554 | 0.0 | 0.0 | 0.0 | 2.0 | 2.0 | 0.750 | 1.0 | 0.0 | 0.0 | 2.0 | 2.0 | 0.0 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-03-04 | 2016-02-28 | 0.627 | 0.0 | 0.0 | 0.0 | 2.0 | 2.0 | 0.788 | 1.0 | 0.0 | 0.0 | 2.0 | 3.0 | 0.0 | ```python theme={null} for cutoff in cv_df['cutoff'].unique(): display(plot_series(Y_df, cv_df.query('cutoff == @cutoff').drop(columns=['cutoff', 'y', 'AutoNHITS-median', 'AutoTFT-median']), max_insample_length=28*4, ids=['FOODS_3_001_CA_1'], level=[90])) ``` ### Evaluate In this section we will evaluate the performance of each model each cross validation window using the MSE metric. ```python theme={null} from utilsforecast.losses import mse, mae from utilsforecast.evaluation import evaluate ``` ```python theme={null} metrics = pd.DataFrame() for cutoff in cv_df["cutoff"].unique(): metrics_per_cutoff = evaluate(cv_df.query("cutoff == @cutoff"), metrics=[mse, mae], models=['AutoNHITS', 'AutoTFT'], level=[80, 90], agg_fn="mean") metrics_per_cutoff = metrics_per_cutoff.assign(cutoff=cutoff) metrics = pd.concat([metrics, metrics_per_cutoff]) metrics ``` | | metric | AutoNHITS | AutoTFT | cutoff | | - | ------ | --------- | --------- | ---------- | | 0 | mse | 10.059308 | 10.909020 | 2016-02-28 | | 1 | mae | 1.485914 | 1.554572 | 2016-02-28 | | 0 | mse | 9.590549 | 10.253903 | 2016-03-27 | | 1 | mae | 1.494229 | 1.561868 | 2016-03-27 | | 0 | mse | 9.596170 | 10.300666 | 2016-04-24 | | 1 | mae | 1.501949 | 1.564157 | 2016-04-24 | ## References * [Croston, J. D. (1972). Forecasting and stock control for intermittent demands. Journal of the Operational Research Society, 23(3), 289-303.](https://link.springer.com/article/10.1057/jors.1972.50) * [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). N-HiTS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Interpretable Decompositions Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/interpretable_decompositions.html [Time series signal decomposition](https://en.wikipedia.org/wiki/Decomposition_of_time_series) involves breaking down an original time series into its constituent components. By decomposing the time series, we can gain insights into underlying patterns, trends-cycles, and seasonal effects, enabling improved understanding and forecasting accuracy. This notebook will show how to use the `NHITS`/`NBEATSx` to extract these series’ components. We will:
- Installing NeuralForecast.
- Simulate a Harmonic Signal.
- NHITS’ forecast decomposition.
- NBEATSx’ forecast decomposition.
You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing NeuralForecast ```python theme={null} %%capture !pip install neuralforecast ``` ## 2. Simulate a Harmonic Signal In this example, we will consider a Harmonic signal comprising two frequencies: one low-frequency and one high-frequency. ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} N = 10_000 T = 1.0 / 800.0 # sample spacing x = np.linspace(0.0, N*T, N, endpoint=False) y1 = np.sin(10.0 * 2.0*np.pi*x) y2 = 0.5 * np.sin(100 * 2.0*np.pi*x) y = y1 + y2 ``` ```python theme={null} import matplotlib.pyplot as plt plt.rcParams["axes.grid"]=True ``` ```python theme={null} fig, ax = plt.subplots(figsize=(6, 2.5)) plt.plot(y[-80:], label='True') plt.plot(y1[-80:], label='Low Frequency', alpha=0.4) plt.plot(y2[-80:], label='High Frequency', alpha=0.4) plt.ylabel('Harmonic Signal') plt.xlabel('Time') plt.legend() plt.show() plt.close() ``` ```python theme={null} # Split dataset into train/test # Last horizon observations for test horizon = 96 Y_df = pd.DataFrame(dict(unique_id=1, ds=np.arange(len(x)), y=y)) Y_train_df = Y_df.groupby('unique_id').head(len(Y_df)-horizon) Y_test_df = Y_df.groupby('unique_id').tail(horizon) Y_test_df ``` | | unique\_id | ds | y | | ---- | ---------- | ---- | --------- | | 9904 | 1 | 9904 | -0.951057 | | 9905 | 1 | 9905 | -0.570326 | | 9906 | 1 | 9906 | -0.391007 | | 9907 | 1 | 9907 | -0.499087 | | 9908 | 1 | 9908 | -0.809017 | | ... | ... | ... | ... | | 9995 | 1 | 9995 | -0.029130 | | 9996 | 1 | 9996 | -0.309017 | | 9997 | 1 | 9997 | -0.586999 | | 9998 | 1 | 9998 | -0.656434 | | 9999 | 1 | 9999 | -0.432012 | ## 3. NHITS decomposition We will employ `NHITS` stack-specialization to recover the latent harmonic functions. `NHITS`, a Wavelet-inspired algorithm, allows for breaking down a time series into various scales or resolutions, aiding in the identification of localized patterns or features. The expressivity ratios for each layer enable control over the model’s stack specialization. ```python theme={null} from neuralforecast.models import NHITS, NBEATSx from neuralforecast import NeuralForecast from neuralforecast.losses.pytorch import HuberLoss, MQLoss ``` ```python theme={null} %%capture models = [NHITS(h=horizon, # Forecast horizon input_size=2 * horizon, # Length of input sequence loss=HuberLoss(), # Robust Huber Loss max_steps=1000, # Number of steps to train dropout_prob_theta=0.5, interpolation_mode='linear', stack_types=['identity']*2, n_blocks=[1, 1], mlp_units=[[64, 64],[64, 64]], n_freq_downsample=[10, 1], # Inverse expressivity ratios for NHITS' stacks specialization val_check_steps=10, # Frequency of validation signal (affects early stopping) ) ] nf = NeuralForecast(models=models, freq=1) nf.fit(df=Y_train_df) ``` ```python theme={null} from neuralforecast.tsdataset import TimeSeriesDataset # NHITS decomposition plot model = nf.models[0] dataset, *_ = TimeSeriesDataset.from_df(df = Y_train_df) y_hat = model.decompose(dataset=dataset) ``` ```text theme={null} GPU available: True (cuda), used: True TPU available: False, using: 0 TPU cores HPU available: False, using: 0 HPUs LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] ``` ```text theme={null} Predicting: | | 0/? [00:00 ## 4. NBEATSx decomposition Here we will employ `NBEATSx` interpretable basis projection to recover the latent harmonic functions. `NBEATSx`, this network in its interpretable variant sequentially projects the signal into polynomials and harmonic basis to learn trend $T$ and seasonality $S$ components: $\hat{y}_{[t+1:t+H]} = \theta_{1} T + \theta_{2} S$ In contrast to `NHITS`’ wavelet-like projections the basis heavily determine the behavior of the projections. And the Fourier projections are not capable of being immediately decomposed into individual frequencies. ```python theme={null} %%capture models = [NBEATSx(h=horizon, # Forecast horizon input_size=2 * horizon, # Length of input sequence loss=HuberLoss(), # Robust Huber Loss max_steps=1000, # Number of steps to train dropout_prob_theta=0.5, stack_types=['trend', 'seasonality'], # Harmonic/Trend projection basis n_polynomials=0, # Lower frequencies can be captured by polynomials n_blocks=[1, 1], mlp_units=[[64, 64],[64, 64]], val_check_steps=10, # Frequency of validation signal (affects early stopping) ) ] nf = NeuralForecast(models=models, freq=1) nf.fit(df=Y_train_df) ``` ```python theme={null} # NBEATSx decomposition plot model = nf.models[0] dataset, *_ = TimeSeriesDataset.from_df(df = Y_train_df) y_hat = model.decompose(dataset=dataset) ``` ```text theme={null} GPU available: True (cuda), used: True TPU available: False, using: 0 TPU cores HPU available: False, using: 0 HPUs LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] ``` ```text theme={null} Predicting: | | 0/? [00:00 ## References * [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2023). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting.](https://arxiv.org/abs/2201.12886)
* [Boris N. Oreshkin, Dmitri Carpov, Nicolas Chapados, Yoshua Bengio (2019). “N-BEATS: Neural basis expansion analysis for interpretable time series forecasting”.](https://arxiv.org/abs/1905.10437)
* [Kin G. Olivares, Cristian Challu, Grzegorz Marcjasz, Rafał Weron, Artur Dubrawski (2021). “Neural basis expansion analysis with exogenous variables: Forecasting electricity prices with NBEATSx”.](https://arxiv.org/abs/2104.05522) # Using Large Datasets Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/large_datasets.html > Tutorial on how to train neuralforecast models on datasets that cannot > fit into memory The standard DataLoader class used by NeuralForecast expects the dataset to be represented by a single DataFrame, which is entirely loaded into memory when fitting the model. However, when the dataset is too large for this, we can instead use the custom large-scale DataLoader. This custom loader assumes that each timeseries is split across a collection of Parquet files, and ensure that only one batch is ever loaded into memory at a given time. In this notebook, we will demonstrate the expected format of these files, how to train the model and how to perform inference using this large-scale DataLoader. ## Load libraries ```python theme={null} import logging import os import tempfile import pandas as pd from neuralforecast import NeuralForecast from neuralforecast.models import NHITS from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae, rmse, smape from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) ``` ## Data Each timeseries should be stored in a directory named **unique\_id=timeseries\_id**. Within this directory, the timeseries can be entirely contained in a single Parquet file or split across multiple Parquet files. Regardless of the format, the timeseries must be ordered by time. For example, the following code splits the AirPassengers DataFrame (of which each timeseries is already sorted by time) into the below format:

**>**  data\     **>**  unique\_id=Airline1\          -  a59945617fdb40d1bc6caa4aadad881c-0.parquet\     **>**  unique\_id=Airline2\          -  a59945617fdb40d1bc6caa4aadad881c-0.parquet
We then simply input a list of the paths to these directories. ```python theme={null} Y_df = AirPassengersPanel.copy() Y_df ``` | | unique\_id | ds | y | trend | y\_\[lag12] | | --- | ---------- | ---------- | ----- | ----- | ----------- | | 0 | Airline1 | 1949-01-31 | 112.0 | 0 | 112.0 | | 1 | Airline1 | 1949-02-28 | 118.0 | 1 | 118.0 | | 2 | Airline1 | 1949-03-31 | 132.0 | 2 | 132.0 | | 3 | Airline1 | 1949-04-30 | 129.0 | 3 | 129.0 | | 4 | Airline1 | 1949-05-31 | 121.0 | 4 | 121.0 | | ... | ... | ... | ... | ... | ... | | 283 | Airline2 | 1960-08-31 | 906.0 | 283 | 859.0 | | 284 | Airline2 | 1960-09-30 | 808.0 | 284 | 763.0 | | 285 | Airline2 | 1960-10-31 | 761.0 | 285 | 707.0 | | 286 | Airline2 | 1960-11-30 | 690.0 | 286 | 662.0 | | 287 | Airline2 | 1960-12-31 | 732.0 | 287 | 705.0 | ```python theme={null} valid = Y_df.groupby('unique_id').tail(72) # from now on we will use the id_col as the unique identifier for the timeseries (this is because we are using the unique_id column to partition the data into parquet files) valid = valid.rename(columns={'unique_id': 'id_col'}) train = Y_df.drop(valid.index) train['id_col'] = train['unique_id'].copy() # we generate the files using a temporary directory here to demonstrate the expected file structure tmpdir = tempfile.TemporaryDirectory() train.to_parquet(tmpdir.name, partition_cols=['unique_id'], index=False) files_list = [f"{tmpdir.name}/{dir}" for dir in os.listdir(tmpdir.name)] files_list ``` ```text theme={null} ['C:\\Users\\ospra\\AppData\\Local\\Temp\\tmpxe__gjoo/unique_id=Airline1', 'C:\\Users\\ospra\\AppData\\Local\\Temp\\tmpxe__gjoo/unique_id=Airline2'] ``` You can also create this directory structure with a spark dataframe using the following: ```python theme={null} spark.conf.set("spark.sql.parquet.outputTimestampType", "TIMESTAMP_MICROS") ( spark_df .repartition(id_col) .sortWithinPartitions(id_col, time_col) .write .partitionBy(id_col) .parquet(out_dir) ) ``` The DataLoader class still expects the static data to be passed in as a single DataFrame with one row per timeseries. ```python theme={null} static = AirPassengersStatic.rename(columns={'unique_id': 'id_col'}) static ``` | | id\_col | airline1 | airline2 | | - | -------- | -------- | -------- | | 0 | Airline1 | 0 | 1 | | 1 | Airline2 | 1 | 0 | ## Model training We now train a NHITS model on the above dataset. It is worth noting that NeuralForecast currently does not support scaling when using this DataLoader. If you want to scale the timeseries this should be done before passing it in to the `fit` method. ```python theme={null} horizon = 12 stacks = 3 models = [NHITS(input_size=5 * horizon, h=horizon, futr_exog_list=['trend', 'y_[lag12]'], stat_exog_list=['airline1', 'airline2'], max_steps=100, stack_types = stacks*['identity'], n_blocks = stacks*[1], mlp_units = [[256,256] for _ in range(stacks)], n_pool_kernel_size = stacks*[1], interpolation_mode="nearest")] nf = NeuralForecast(models=models, freq='ME') nf.fit(df=files_list, static_df=static, id_col='id_col') ``` ```text theme={null} Seed set to 1 ``` ```text theme={null} Sanity Checking: | | 0/? [00:00 Open In Colab ## 1. Installing NeuralForecast ```python theme={null} %%capture !pip install neuralforecast datasetsforecast utilsforecast ``` ## 2. Load ETTm2 Data The `LongHorizon` class will automatically download the complete ETTm2 dataset and process it. It return three Dataframes: `Y_df` contains the values for the target variables, `X_df` contains exogenous calendar features and `S_df` contains static features for each time-series (none for ETTm2). For this example we will only use `Y_df`. If you want to use your own data just replace `Y_df`. Be sure to use a long format and have a similar structure to our data set. ```python theme={null} import pandas as pd from datasetsforecast.long_horizon import LongHorizon # Change this to your own data to try the model Y_df, _, _ = LongHorizon.load(directory='./', group='ETTm2') Y_df['ds'] = pd.to_datetime(Y_df['ds']) # For this excercise we are going to take 20% of the DataSet n_time = len(Y_df.ds.unique()) val_size = int(.2 * n_time) test_size = int(.2 * n_time) Y_df.groupby('unique_id').head(2) ``` | | unique\_id | ds | y | | ------ | ---------- | ------------------- | --------- | | 0 | HUFL | 2016-07-01 00:00:00 | -0.041413 | | 1 | HUFL | 2016-07-01 00:15:00 | -0.185467 | | 57600 | HULL | 2016-07-01 00:00:00 | 0.040104 | | 57601 | HULL | 2016-07-01 00:15:00 | -0.214450 | | 115200 | LUFL | 2016-07-01 00:00:00 | 0.695804 | | 115201 | LUFL | 2016-07-01 00:15:00 | 0.434685 | | 172800 | LULL | 2016-07-01 00:00:00 | 0.434430 | | 172801 | LULL | 2016-07-01 00:15:00 | 0.428168 | | 230400 | MUFL | 2016-07-01 00:00:00 | -0.599211 | | 230401 | MUFL | 2016-07-01 00:15:00 | -0.658068 | | 288000 | MULL | 2016-07-01 00:00:00 | -0.393536 | | 288001 | MULL | 2016-07-01 00:15:00 | -0.659338 | | 345600 | OT | 2016-07-01 00:00:00 | 1.018032 | | 345601 | OT | 2016-07-01 00:15:00 | 0.980124 | ```python theme={null} import matplotlib.pyplot as plt # We are going to plot the temperature of the transformer # and marking the validation and train splits u_id = 'HUFL' x_plot = pd.to_datetime(Y_df[Y_df.unique_id==u_id].ds) y_plot = Y_df[Y_df.unique_id==u_id].y.values x_val = x_plot[n_time - val_size - test_size] x_test = x_plot[n_time - test_size] fig = plt.figure(figsize=(10, 5)) fig.tight_layout() plt.plot(x_plot, y_plot) plt.xlabel('Date', fontsize=17) plt.ylabel('HUFL [15 min temperature]', fontsize=17) plt.axvline(x_val, color='black', linestyle='-.') plt.axvline(x_test, color='black', linestyle='-.') plt.text(x_val, 5, ' Validation', fontsize=12) plt.text(x_test, 5, ' Test', fontsize=12) plt.grid() ``` ## 3. Hyperparameter selection and forecasting The `AutoNHITS` class will automatically perform hyperparameter tuning using [Tune library](https://docs.ray.io/en/latest/tune/index.html), exploring a user-defined or default search space. Models are selected based on the error on a validation set and the best model is then stored and used during inference. The `AutoNHITS.default_config` attribute contains a suggested hyperparameter space. Here, we specify a different search space following the paper’s hyperparameters. Notice that *1000 Stochastic Gradient Steps* are enough to achieve SoTA performance. Feel free to play around with this space. ```python theme={null} from ray import tune from neuralforecast.auto import AutoNHITS from neuralforecast.core import NeuralForecast ``` ```python theme={null} horizon = 96 # 24hrs = 4 * 15 min. # Use your own config or AutoNHITS.default_config nhits_config = { "learning_rate": tune.choice([1e-3]), # Initial Learning rate "max_steps": tune.choice([1000]), # Number of SGD steps "input_size": tune.choice([5 * horizon]), # input_size = multiplier * horizon "batch_size": tune.choice([7]), # Number of series in windows "windows_batch_size": tune.choice([256]), # Number of windows in batch "n_pool_kernel_size": tune.choice([[2, 2, 2], [16, 8, 1]]), # MaxPool's Kernelsize "n_freq_downsample": tune.choice([[168, 24, 1], [24, 12, 1], [1, 1, 1]]), # Interpolation expressivity ratios "activation": tune.choice(['ReLU']), # Type of non-linear activation "n_blocks": tune.choice([[1, 1, 1]]), # Blocks per each 3 stacks "mlp_units": tune.choice([[[512, 512], [512, 512], [512, 512]]]), # 2 512-Layers per block for each stack "interpolation_mode": tune.choice(['linear']), # Type of multi-step interpolation "val_check_steps": tune.choice([100]), # Compute validation every 100 epochs "random_seed": tune.randint(1, 10), } ``` > **Tip** > > Refer to [https://docs.ray.io/en/latest/tune/index.html](https://docs.ray.io/en/latest/tune/index.html) for more > information on the different space options, such as lists and > continous intervals.m To instantiate `AutoNHITS` you need to define: * `h`: forecasting horizon * `loss`: training loss. Use the `DistributionLoss` to produce probabilistic forecasts. * `config`: hyperparameter search space. If `None`, the `AutoNHITS` class will use a pre-defined suggested hyperparameter space. * `num_samples`: number of configurations explored. ```python theme={null} models = [AutoNHITS(h=horizon, config=nhits_config, num_samples=5)] ``` Fit the model by instantiating a `NeuralForecast` object with the following required parameters: * `models`: a list of models. * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) The `cross_validation` method allows you to simulate multiple historic forecasts, greatly simplifying pipelines by replacing for loops with `fit` and `predict` methods. With time series data, cross validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The `cross_validation` method will use the validation set for hyperparameter selection, and will then produce the forecasts for the test set. ```python theme={null} %%capture nf = NeuralForecast( models=models, freq='15min') Y_hat_df = nf.cross_validation(df=Y_df, val_size=val_size, test_size=test_size, n_windows=None) ``` ## 4. Evaluate Results The `AutoNHITS` class contains a `results` tune attribute that stores information of each configuration explored. It contains the validation loss and best validation hyperparameter. ```python theme={null} nf.models[0].results.get_best_result().config ``` ```text theme={null} {'learning_rate': 0.001, 'max_steps': 1000, 'input_size': 480, 'batch_size': 7, 'windows_batch_size': 256, 'n_pool_kernel_size': [2, 2, 2], 'n_freq_downsample': [24, 12, 1], 'activation': 'ReLU', 'n_blocks': [1, 1, 1], 'mlp_units': [[512, 512], [512, 512], [512, 512]], 'interpolation_mode': 'linear', 'val_check_steps': 100, 'random_seed': 8, 'h': 96, 'loss': MAE(), 'valid_loss': MAE()} ``` ```python theme={null} from utilsforecast.plotting import plot_series series = ['HUFL','HULL','LUFL','LULL','MUFL','MULL','OT'] series_id = series[3] # 'LULL' series_cutoffs = Y_hat_df.loc[Y_hat_df['unique_id'] == "LULL", 'cutoff'].unique() for w_idx in [200, 300, 400]: cutoff = series_cutoffs[w_idx] plot_df = Y_hat_df.loc[Y_hat_df['cutoff'] == cutoff, ['unique_id', 'ds', 'y', 'AutoNHITS']] fig = plot_series( df=plot_df[['unique_id', 'ds', 'y']], forecasts_df=plot_df[['unique_id', 'ds', 'AutoNHITS']], ids=[series_id], models=['AutoNHITS'], max_insample_length=96 ) display(fig) ``` Finally, we compute the test errors for the two metrics of interest: $\qquad MAE = \frac{1}{Windows * Horizon} \sum_{\tau} |y_{\tau} - \hat{y}_{\tau}| \qquad$ and $\qquad MSE = \frac{1}{Windows * Horizon} \sum_{\tau} (y_{\tau} - \hat{y}_{\tau})^{2} \qquad$ ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae, mse eval_df = evaluate( df=Y_hat_df.drop(columns=["cutoff"]), metrics=[mae, mse], agg_fn="mean" ) print('MAE: ', eval_df.iloc[0]["AutoNHITS"]) print('MSE: ', eval_df.iloc[1]["AutoNHITS"]) ``` ```text theme={null} MAE: 0.24862242128243706 MSE: 0.17257850996828134 ``` For reference we can check the performance when compared to previous ‘state-of-the-art’ long-horizon Transformer-based forecasting methods from the [NHITS paper](https://arxiv.org/abs/2201.12886). To recover or improve the paper results try setting `hyperopt_max_evals=30` in [Hyperparameter Tuning](#cell-4). Mean Absolute Error (MAE): | Horizon | NHITS | AutoFormer | InFormer | ARIMA | | ------- | --------- | ---------- | -------- | ----- | | 96 | **0.249** | 0.339 | 0.453 | 0.301 | | 192 | 0.305 | 0.340 | 0.563 | 0.345 | | 336 | 0.346 | 0.372 | 0.887 | 0.386 | | 720 | 0.426 | 0.419 | 1.388 | 0.445 | Mean Squared Error (MSE): | Horizon | NHITS | AutoFormer | InFormer | ARIMA | | ------- | --------- | ---------- | -------- | ----- | | 96 | **0.173** | 0.255 | 0.365 | 0.225 | | 192 | 0.245 | 0.281 | 0.533 | 0.298 | | 336 | 0.295 | 0.339 | 1.363 | 0.370 | | 720 | 0.401 | 0.422 | 3.379 | 0.478 | ## References [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Long-Horizon Probabilistic Forecasting Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/longhorizon_probabilistic.html Long-horizon forecasting is challenging because of the *volatility* of the predictions and the *computational complexity*. To solve this problem we created the [NHITS](https://arxiv.org/abs/2201.12886) model and made the code available [NeuralForecast library](https://nixtlaverse.nixtla.io/neuralforecast/models.nhits.html). `NHITS` specializes its partial outputs in the different frequencies of the time series through hierarchical interpolation and multi-rate input processing. We model the target time-series with Student’s t-distribution. The `NHITS` will output the distribution parameters for each timestamp. In this notebook we show how to use `NHITS` on the [ETTm2](https://github.com/zhouhaoyi/ETDataset) benchmark dataset for probabilistic forecasting. This data set includes data points for 2 Electricity Transformers at 2 stations, including load, oil temperature. We will show you how to load data, train, and perform automatic hyperparameter tuning, **to achieve SoTA performance**, outperforming even the latest Transformer architectures for a fraction of their computational cost (50x faster). You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Libraries ```python theme={null} %%capture !pip install neuralforecast datasetsforecast ``` ## 2. Load ETTm2 Data The `LongHorizon` class will automatically download the complete ETTm2 dataset and process it. It return three Dataframes: `Y_df` contains the values for the target variables, `X_df` contains exogenous calendar features and `S_df` contains static features for each time-series (none for ETTm2). For this example we will only use `Y_df`. If you want to use your own data just replace `Y_df`. Be sure to use a long format and have a similar structure to our data set. ```python theme={null} import pandas as pd from datasetsforecast.long_horizon import LongHorizon ``` ```python theme={null} # Change this to your own data to try the model Y_df, _, _ = LongHorizon.load(directory='./', group='ETTm2') Y_df['ds'] = pd.to_datetime(Y_df['ds']) # For this excercise we are going to take 960 timestamps as validation and test n_time = len(Y_df.ds.unique()) val_size = 96*10 test_size = 96*10 Y_df.groupby('unique_id').head(2) ``` | | unique\_id | ds | y | | ------ | ---------- | ------------------- | --------- | | 0 | HUFL | 2016-07-01 00:00:00 | -0.041413 | | 1 | HUFL | 2016-07-01 00:15:00 | -0.185467 | | 57600 | HULL | 2016-07-01 00:00:00 | 0.040104 | | 57601 | HULL | 2016-07-01 00:15:00 | -0.214450 | | 115200 | LUFL | 2016-07-01 00:00:00 | 0.695804 | | 115201 | LUFL | 2016-07-01 00:15:00 | 0.434685 | | 172800 | LULL | 2016-07-01 00:00:00 | 0.434430 | | 172801 | LULL | 2016-07-01 00:15:00 | 0.428168 | | 230400 | MUFL | 2016-07-01 00:00:00 | -0.599211 | | 230401 | MUFL | 2016-07-01 00:15:00 | -0.658068 | | 288000 | MULL | 2016-07-01 00:00:00 | -0.393536 | | 288001 | MULL | 2016-07-01 00:15:00 | -0.659338 | | 345600 | OT | 2016-07-01 00:00:00 | 1.018032 | | 345601 | OT | 2016-07-01 00:15:00 | 0.980124 | > **Important** > > DataFrames must include all `['unique_id', 'ds', 'y']` columns. Make > sure `y` column does not have missing or non-numeric values. Next, plot the `HUFL` variable marking the validation and train splits. ```python theme={null} import matplotlib.pyplot as plt from utilsforecast.plotting import plot_series ``` ```python theme={null} u_id = 'HUFL' fig = plot_series(Y_df, ids=[u_id]) ax = fig.axes[0] x_plot = pd.to_datetime(Y_df[Y_df.unique_id==u_id].ds) y_plot = Y_df[Y_df.unique_id==u_id].y.values x_val = x_plot[n_time - val_size - test_size] x_test = x_plot[n_time - test_size] ax.axvline(x_val, color='black', linestyle='-.') ax.axvline(x_test, color='black', linestyle='-.') ax.text(x_val, 5, ' Validation', fontsize=12) ax.text(x_test, 3, ' Test', fontsize=12) fig ``` ## 3. Hyperparameter selection and forecasting The `AutoNHITS` class will automatically perform hyperparameter tuning using [Tune library](https://docs.ray.io/en/latest/tune/index.html), exploring a user-defined or default search space. Models are selected based on the error on a validation set and the best model is then stored and used during inference. The `AutoNHITS.default_config` attribute contains a suggested hyperparameter space. Here, we specify a different search space following the paper’s hyperparameters. Notice that *1000 Stochastic Gradient Steps* are enough to achieve SoTA performance. Feel free to play around with this space. ```python theme={null} import logging import torch from neuralforecast.auto import AutoNHITS from neuralforecast.core import NeuralForecast from neuralforecast.losses.pytorch import DistributionLoss from ray import tune ``` ```python theme={null} logging.getLogger("pytorch_lightning").setLevel(logging.WARNING) torch.set_float32_matmul_precision('high') ``` ```python theme={null} horizon = 96 # 24hrs = 4 * 15 min. # Use your own config or AutoNHITS.default_config nhits_config = { "learning_rate": tune.choice([1e-3]), # Initial Learning rate "max_steps": tune.choice([1000]), # Number of SGD steps "input_size": tune.choice([5 * horizon]), # input_size = multiplier * horizon "batch_size": tune.choice([7]), # Number of series in windows "windows_batch_size": tune.choice([256]), # Number of windows in batch "n_pool_kernel_size": tune.choice([[2, 2, 2], [16, 8, 1]]), # MaxPool's Kernelsize "n_freq_downsample": tune.choice([[168, 24, 1], [24, 12, 1], [1, 1, 1]]), # Interpolation expressivity ratios "activation": tune.choice(['ReLU']), # Type of non-linear activation "n_blocks": tune.choice([[1, 1, 1]]), # Blocks per each 3 stacks "mlp_units": tune.choice([[[512, 512], [512, 512], [512, 512]]]), # 2 512-Layers per block for each stack "interpolation_mode": tune.choice(['linear']), # Type of multi-step interpolation "random_seed": tune.randint(1, 10), "scaler_type": tune.choice(['robust']), "val_check_steps": tune.choice([100]) } ``` > **Tip** > > Refer to [https://docs.ray.io/en/latest/tune/index.html](https://docs.ray.io/en/latest/tune/index.html) for more > information on the different space options, such as lists and > continous intervals.m To instantiate `AutoNHITS` you need to define: * `h`: forecasting horizon * `loss`: training loss. Use the `DistributionLoss` to produce probabilistic forecasts. * `config`: hyperparameter search space. If `None`, the `AutoNHITS` class will use a pre-defined suggested hyperparameter space. * `num_samples`: number of configurations explored. ```python theme={null} models = [AutoNHITS(h=horizon, loss=DistributionLoss(distribution='StudentT', level=[80, 90]), config=nhits_config, num_samples=5)] ``` Fit the model by instantiating a `NeuralForecast` object with the following required parameters: * `models`: a list of models. * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) ```python theme={null} # Fit and predict nf = NeuralForecast(models=models, freq='15min') ``` The `cross_validation` method allows you to simulate multiple historic forecasts, greatly simplifying pipelines by replacing for loops with `fit` and `predict` methods. With time series data, cross validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The `cross_validation` method will use the validation set for hyperparameter selection, and will then produce the forecasts for the test set. ```python theme={null} %%capture Y_hat_df = nf.cross_validation(df=Y_df, val_size=val_size, test_size=test_size, n_windows=None) ``` ## 4. Visualization Finally, we merge the forecasts with the `Y_df` dataset and plot the forecasts. ```python theme={null} Y_hat_df ``` | | unique\_id | ds | cutoff | AutoNHITS | AutoNHITS-median | AutoNHITS-lo-90 | AutoNHITS-lo-80 | AutoNHITS-hi-80 | AutoNHITS-hi-90 | y | | ------ | ---------- | ------------------- | ------------------- | --------- | ---------------- | --------------- | --------------- | --------------- | --------------- | --------- | | 0 | HUFL | 2018-02-11 00:00:00 | 2018-02-10 23:45:00 | -0.922304 | -0.914175 | -1.217987 | -1.138274 | -0.708157 | -0.617799 | -0.849571 | | 1 | HUFL | 2018-02-11 00:15:00 | 2018-02-10 23:45:00 | -0.954299 | -0.957198 | -1.403932 | -1.263984 | -0.618467 | -0.442688 | -1.049700 | | 2 | HUFL | 2018-02-11 00:30:00 | 2018-02-10 23:45:00 | -0.987538 | -0.972558 | -1.512509 | -1.310191 | -0.621673 | -0.444359 | -1.185730 | | 3 | HUFL | 2018-02-11 00:45:00 | 2018-02-10 23:45:00 | -1.067760 | -1.063188 | -1.614276 | -1.475302 | -0.665729 | -0.521775 | -1.329785 | | 4 | HUFL | 2018-02-11 01:00:00 | 2018-02-10 23:45:00 | -1.001276 | -1.001494 | -1.508795 | -1.390156 | -0.629212 | -0.470608 | -1.369715 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 581275 | OT | 2018-02-20 22:45:00 | 2018-02-19 23:45:00 | -1.200041 | -1.200862 | -1.591271 | -1.490571 | -0.907190 | -0.779424 | -1.581325 | | 581276 | OT | 2018-02-20 23:00:00 | 2018-02-19 23:45:00 | -1.237206 | -1.225333 | -1.618691 | -1.518204 | -0.960075 | -0.838512 | -1.581325 | | 581277 | OT | 2018-02-20 23:15:00 | 2018-02-19 23:45:00 | -1.232434 | -1.229675 | -1.591164 | -1.481251 | -0.989993 | -0.870404 | -1.581325 | | 581278 | OT | 2018-02-20 23:30:00 | 2018-02-19 23:45:00 | -1.259237 | -1.258848 | -1.659239 | -1.536979 | -0.985581 | -0.822370 | -1.562328 | | 581279 | OT | 2018-02-20 23:45:00 | 2018-02-19 23:45:00 | -1.247161 | -1.251899 | -1.631909 | -1.520350 | -0.949529 | -0.832602 | -1.562328 | ```python theme={null} Y_hat_df = Y_hat_df.reset_index(drop=True) Y_hat_df = Y_hat_df[(Y_hat_df['unique_id']=='OT') & (Y_hat_df['cutoff']=='2018-02-11 12:00:00')] Y_hat_df = Y_hat_df.drop(columns=['y','cutoff']) ``` ```python theme={null} plot_df = Y_df.merge(Y_hat_df, on=['unique_id','ds'], how='outer').tail(96*10+50+96*4).head(96*2+96*4) plot_series(forecasts_df=plot_df.drop(columns='AutoNHITS').rename(columns={'AutoNHITS-median': 'AutoNHITS'}), level=[90]) ``` ## References [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Long-Horizon Forecasting with Transformer models Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/longhorizon_transformers.html > Tutorial on how to train and forecast Transformer models. Transformer models, originally proposed for applications in natural language processing, have seen increasing adoption in the field of time series forecasting. The transformative power of these models lies in their novel architecture that relies heavily on the self-attention mechanism, which helps the model to focus on different parts of the input sequence to make predictions, while capturing long-range dependencies within the data. In the context of time series forecasting, Transformer models leverage this self-attention mechanism to identify relevant information across different periods in the time series, making them exceptionally effective in predicting future values for complex and noisy sequences. Long horizon forecasting consists of predicting a large number of timestamps. It is a challenging task because of the *volatility* of the predictions and the *computational complexity*. To solve this problem, recent studies proposed a variety of Transformer-based models. The Neuralforecast library includes implementations of the following popular recent models: `Informer` (Zhou, H. et al. 2021), `Autoformer` (Wu et al. 2021), `FEDformer` (Zhou, T. et al. 2022), and `PatchTST` (Nie et al. 2023). Our implementation of all these models are univariate, meaning that only autoregressive values of each feature are used for forecasting. **We observed that these unvivariate models are more accurate and faster than their multivariate couterpart**. In this notebook we will show how to: \* Load the [ETTm2](https://github.com/zhouhaoyi/ETDataset) benchmark dataset, used in the academic literature. \* Train models \* Forecast the test set **The results achieved in this notebook outperform the original self-reported results in the respective original paper, with a fraction of the computational cost. Additionally, all models are trained with the default recommended parameters, results can be further improved using our `auto` models with automatic hyperparameter selection.** You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing libraries ```python theme={null} %%capture !pip install neuralforecast datasetsforecast utilsforecast ``` ## 2. Load ETTm2 Data The `LongHorizon` class will automatically download the complete ETTm2 dataset and process it. It return three Dataframes: `Y_df` contains the values for the target variables, `X_df` contains exogenous calendar features and `S_df` contains static features for each time-series (none for ETTm2). For this example we will only use `Y_df`. If you want to use your own data just replace `Y_df`. Be sure to use a long format and have a similar structure to our data set. ```python theme={null} import pandas as pd from datasetsforecast.long_horizon import LongHorizon ``` ```python theme={null} # Change this to your own data to try the model Y_df, _, _ = LongHorizon.load(directory='./', group='ETTm2') Y_df['ds'] = pd.to_datetime(Y_df['ds']) n_time = len(Y_df.ds.unique()) val_size = int(.2 * n_time) test_size = int(.2 * n_time) Y_df.groupby('unique_id').head(2) ``` | | unique\_id | ds | y | | ------ | ---------- | ------------------- | --------- | | 0 | HUFL | 2016-07-01 00:00:00 | -0.041413 | | 1 | HUFL | 2016-07-01 00:15:00 | -0.185467 | | 57600 | HULL | 2016-07-01 00:00:00 | 0.040104 | | 57601 | HULL | 2016-07-01 00:15:00 | -0.214450 | | 115200 | LUFL | 2016-07-01 00:00:00 | 0.695804 | | 115201 | LUFL | 2016-07-01 00:15:00 | 0.434685 | | 172800 | LULL | 2016-07-01 00:00:00 | 0.434430 | | 172801 | LULL | 2016-07-01 00:15:00 | 0.428168 | | 230400 | MUFL | 2016-07-01 00:00:00 | -0.599211 | | 230401 | MUFL | 2016-07-01 00:15:00 | -0.658068 | | 288000 | MULL | 2016-07-01 00:00:00 | -0.393536 | | 288001 | MULL | 2016-07-01 00:15:00 | -0.659338 | | 345600 | OT | 2016-07-01 00:00:00 | 1.018032 | | 345601 | OT | 2016-07-01 00:15:00 | 0.980124 | ## 3. Train models We will train models using the `cross_validation` method, which allows users to automatically simulate multiple historic forecasts (in the test set). The `cross_validation` method will use the validation set for hyperparameter selection and early stopping, and will then produce the forecasts for the test set. First, instantiate each model in the `models` list, specifying the `horizon`, `input_size`, and training iterations. (NOTE: The `FEDformer` model was excluded due to extremely long training times.) ```python theme={null} %%capture from neuralforecast.core import NeuralForecast from neuralforecast.models import Informer, Autoformer, FEDformer, PatchTST ``` ```text theme={null} INFO:torch.distributed.nn.jit.instantiator:Created a temporary directory at /tmp/tmpopb2vyyt INFO:torch.distributed.nn.jit.instantiator:Writing /tmp/tmpopb2vyyt/_remote_module_non_scriptable.py ``` ```python theme={null} %%capture horizon = 96 # 24hrs = 4 * 15 min. models = [Informer(h=horizon, # Forecasting horizon input_size=horizon, # Input size max_steps=1000, # Number of training iterations val_check_steps=100, # Compute validation loss every 100 steps early_stop_patience_steps=3), # Stop training if validation loss does not improve Autoformer(h=horizon, input_size=horizon, max_steps=1000, val_check_steps=100, early_stop_patience_steps=3), PatchTST(h=horizon, input_size=horizon, max_steps=1000, val_check_steps=100, early_stop_patience_steps=3), ] ``` ```text theme={null} INFO:lightning_fabric.utilities.seed:Global seed set to 1 INFO:lightning_fabric.utilities.seed:Global seed set to 1 INFO:lightning_fabric.utilities.seed:Global seed set to 1 ``` > **Tip** > > Check our `auto` models for automatic hyperparameter optimization. Instantiate a `NeuralForecast` object with the following required parameters: * `models`: a list of models. * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) Second, use the `cross_validation` method, specifying the dataset (`Y_df`), validation size and test size. ```python theme={null} %%capture nf = NeuralForecast( models=models, freq='15min') Y_hat_df = nf.cross_validation(df=Y_df, val_size=val_size, test_size=test_size, n_windows=None) ``` The `cross_validation` method will return the forecasts for each model on the test set. ```python theme={null} Y_hat_df.head() ``` | | unique\_id | ds | cutoff | Informer | Autoformer | PatchTST | y | | - | ---------- | ------------------- | ------------------- | --------- | ---------- | --------- | --------- | | 0 | HUFL | 2017-10-24 00:00:00 | 2017-10-23 23:45:00 | -1.055062 | -0.861487 | -0.860189 | -0.977673 | | 1 | HUFL | 2017-10-24 00:15:00 | 2017-10-23 23:45:00 | -1.021247 | -0.873399 | -0.865730 | -0.865620 | | 2 | HUFL | 2017-10-24 00:30:00 | 2017-10-23 23:45:00 | -1.057297 | -0.900345 | -0.944296 | -0.961624 | | 3 | HUFL | 2017-10-24 00:45:00 | 2017-10-23 23:45:00 | -0.886652 | -0.867466 | -0.974849 | -1.049700 | | 4 | HUFL | 2017-10-24 01:00:00 | 2017-10-23 23:45:00 | -1.000431 | -0.887454 | -1.008530 | -0.953600 | ## 4. Evaluate Results Next, we plot the forecasts on the test set for the `OT` variable for all models. ```python theme={null} import matplotlib.pyplot as plt ``` ```python theme={null} Y_plot = Y_hat_df[Y_hat_df['unique_id']=='OT'] # OT dataset cutoffs = Y_hat_df['cutoff'].unique()[::horizon] Y_plot = Y_plot[Y_hat_df['cutoff'].isin(cutoffs)] plt.figure(figsize=(20,5)) plt.plot(Y_plot['ds'], Y_plot['y'], label='True') plt.plot(Y_plot['ds'], Y_plot['Informer'], label='Informer') plt.plot(Y_plot['ds'], Y_plot['Autoformer'], label='Autoformer') plt.plot(Y_plot['ds'], Y_plot['PatchTST'], label='PatchTST') plt.xlabel('Datestamp') plt.ylabel('OT') plt.grid() plt.legend() ``` Finally, we compute the test errors using the Mean Absolute Error (MAE): $\qquad MAE = \frac{1}{Windows * Horizon} \sum_{\tau} |y_{\tau} - \hat{y}_{\tau}| \qquad$ ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae ``` ```python theme={null} eval_df = evaluate( df=Y_hat_df.drop(columns=["cutoff"]), metrics=[mae], agg_fn="mean" ) print('Informer: ', eval_df.iloc[0]["Informer"]) print('Autoformer: ', eval_df.iloc[0]["Autoformer"]) print('PatchTST: ', eval_df.iloc[0]["PatchTST"]) ``` ```text theme={null} Informer: 0.339 Autoformer: 0.316 PatchTST: 0.251 ``` For reference, we can check the performance when compared to self-reported performance in their respective papers. | Horizon | PatchTST | AutoFormer | Informer | ARIMA | | ------- | --------- | ---------- | -------- | ----- | | 96 | **0.256** | 0.339 | 0.453 | 0.301 | | 192 | 0.296 | 0.340 | 0.563 | 0.345 | | 336 | 0.329 | 0.372 | 0.887 | 0.386 | | 720 | 0.385 | 0.419 | 1.388 | 0.445 | ## Next steps We proposed an alternative model for long-horizon forecasting, the `NHITS`, based on feed-forward networks in (Challu et al. 2023). It achieves on par performance with `PatchTST`, with a fraction of the computational cost. The `NHITS` tutorial is available [here](https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/longhorizon_nhits.html). ## References [Zhou, H., Zhang, S., Peng, J., Zhang, S., Li, J., Xiong, H., & Zhang, W. (2021, May). Informer: Beyond efficient transformer for long sequence time-series forecasting. In Proceedings of the AAAI conference on artificial intelligence (Vol. 35, No. 12, pp. 11106-11115)](https://ojs.aaai.org/index.php/AAAI/article/view/17325) [Wu, H., Xu, J., Wang, J., & Long, M. (2021). Autoformer: Decomposition transformers with auto-correlation for long-term series forecasting. Advances in Neural Information Processing Systems, 34, 22419-22430.](https://proceedings.neurips.cc/paper/2021/hash/bcc0d400288793e8bdcd7c19a8ac0c2b-Abstract.html) [Zhou, T., Ma, Z., Wen, Q., Wang, X., Sun, L., & Jin, R. (2022, June). Fedformer: Frequency enhanced decomposed transformer for long-term series forecasting. In International Conference on Machine Learning (pp. 27268-27286). PMLR.](https://proceedings.mlr.press/v162/zhou22g.html) [Nie, Y., Nguyen, N. H., Sinthong, P., & Kalagnanam, J. (2022). A Time Series is Worth 64 Words: Long-term Forecasting with Transformers.](https://arxiv.org/pdf/2211.14730.pdf) [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Multivariate Forecasting with TSMixer Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/multivariate_tsmixer.html > Tutorial on how to do multivariate forecasting using TSMixer models. In *multivariate* forecasting, we use the information from every time series to produce all forecasts for all time series jointly. In contrast, in *univariate* forecasting we only consider the information from every individual time series and produce forecasts for every time series separately. Multivariate forecasting methods thus use more information to produce every forecast, and thus should be able to provide better forecasting results. However, multivariate forecasting methods also scale with the number of time series, which means these methods are commonly less well suited for large-scale problems (i.e. forecasting many, many time series). In this notebook, we will demonstrate the performance of a state-of-the-art multivariate forecasting architecture `TSMixer` / `TSMixerx` when compared to a univariate forecasting method (`NHITS`) and a simple MLP-based multivariate method (`MLPMultivariate`). We will show how to: \* Load the [ETTm2](https://github.com/zhouhaoyi/ETDataset) benchmark dataset, used in the academic literature. \* Train a `TSMixer`, `TSMixerx` and `MLPMultivariate` model \* Forecast the test set \* Optimize the hyperparameters You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing libraries ```python theme={null} %%capture !pip install neuralforecast datasetsforecast ``` ## 2. Load ETTm2 Data The `LongHorizon` class will automatically download the complete ETTm2 dataset and process it. It return three Dataframes: `Y_df` contains the values for the target variables, `X_df` contains exogenous calendar features and `S_df` contains static features for each time-series (none for ETTm2). For this example we will use `Y_df` and `X_df`. In `TSMixerx`, we can make use of the additional exogenous features contained in `X_df`. In `TSMixer`, there is *no* support for exogenous features. Hence, if you want to use exogenous features, you should use `TSMixerx`. If you want to use your own data just replace `Y_df` and `X_df`. Be sure to use a long format and make sure to have a similar structure as our data set. ```python theme={null} import pandas as pd from datasetsforecast.long_horizon import LongHorizon ``` ```python theme={null} # Change this to your own data to try the model Y_df, X_df, _ = LongHorizon.load(directory='./', group='ETTm2') Y_df['ds'] = pd.to_datetime(Y_df['ds']) # X_df contains the exogenous features, which we add to Y_df X_df['ds'] = pd.to_datetime(X_df['ds']) Y_df = Y_df.merge(X_df, on=['unique_id', 'ds'], how='left') # We make validation and test splits n_time = len(Y_df.ds.unique()) val_size = int(.2 * n_time) test_size = int(.2 * n_time) ``` ```python theme={null} Y_df ``` | | unique\_id | ds | y | ex\_1 | ex\_2 | ex\_3 | ex\_4 | | ------ | ---------- | ------------------- | --------- | --------- | --------- | --------- | --------- | | 0 | HUFL | 2016-07-01 00:00:00 | -0.041413 | -0.500000 | 0.166667 | -0.500000 | -0.001370 | | 1 | HUFL | 2016-07-01 00:15:00 | -0.185467 | -0.500000 | 0.166667 | -0.500000 | -0.001370 | | 2 | HUFL | 2016-07-01 00:30:00 | -0.257495 | -0.500000 | 0.166667 | -0.500000 | -0.001370 | | 3 | HUFL | 2016-07-01 00:45:00 | -0.577510 | -0.500000 | 0.166667 | -0.500000 | -0.001370 | | 4 | HUFL | 2016-07-01 01:00:00 | -0.385501 | -0.456522 | 0.166667 | -0.500000 | -0.001370 | | ... | ... | ... | ... | ... | ... | ... | ... | | 403195 | OT | 2018-02-20 22:45:00 | -1.581325 | 0.456522 | -0.333333 | 0.133333 | -0.363014 | | 403196 | OT | 2018-02-20 23:00:00 | -1.581325 | 0.500000 | -0.333333 | 0.133333 | -0.363014 | | 403197 | OT | 2018-02-20 23:15:00 | -1.581325 | 0.500000 | -0.333333 | 0.133333 | -0.363014 | | 403198 | OT | 2018-02-20 23:30:00 | -1.562328 | 0.500000 | -0.333333 | 0.133333 | -0.363014 | | 403199 | OT | 2018-02-20 23:45:00 | -1.562328 | 0.500000 | -0.333333 | 0.133333 | -0.363014 | ## 3. Train models We will train models using the `cross_validation` method, which allows users to automatically simulate multiple historic forecasts (in the test set). The `cross_validation` method will use the validation set for hyperparameter selection and early stopping, and will then produce the forecasts for the test set. First, instantiate each model in the `models` list, specifying the `horizon`, `input_size`, and training iterations. In this notebook, we compare against the univariate `NHITS` and multivariate `MLPMultivariate` models. ```python theme={null} import logging import torch from neuralforecast.core import NeuralForecast from neuralforecast.models import TSMixer, TSMixerx, NHITS, MLPMultivariate from neuralforecast.losses.pytorch import MAE ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) torch.set_float32_matmul_precision('high') ``` ```python theme={null} horizon = 96 input_size = 512 models = [ TSMixer(h=horizon, input_size=input_size, n_series=7, max_steps=1000, val_check_steps=100, early_stop_patience_steps=5, scaler_type='identity', valid_loss=MAE(), random_seed=12345678, ), TSMixerx(h=horizon, input_size=input_size, n_series=7, max_steps=1000, val_check_steps=100, early_stop_patience_steps=5, scaler_type='identity', dropout=0.7, valid_loss=MAE(), random_seed=12345678, futr_exog_list=['ex_1', 'ex_2', 'ex_3', 'ex_4'], ), MLPMultivariate(h=horizon, input_size=input_size, n_series=7, max_steps=1000, val_check_steps=100, early_stop_patience_steps=5, scaler_type='standard', hidden_size=256, valid_loss=MAE(), random_seed=12345678, ), NHITS(h=horizon, input_size=horizon, max_steps=1000, val_check_steps=100, early_stop_patience_steps=5, scaler_type='robust', valid_loss=MAE(), random_seed=12345678, ), ] ``` > **Tip** > > Check our `auto` models for automatic hyperparameter optimization, and > see the end of this tutorial for an example of hyperparameter tuning. Instantiate a `NeuralForecast` object with the following required parameters: * `models`: a list of models. * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) Second, use the `cross_validation` method, specifying the dataset (`Y_df`), validation size and test size. ```python theme={null} %%capture nf = NeuralForecast( models=models, freq='15min', ) Y_hat_df = nf.cross_validation( df=Y_df, val_size=val_size, test_size=test_size, n_windows=None, ) ``` The `cross_validation` method will return the forecasts for each model on the test set. ## 4. Evaluate Results Next, we plot the forecasts on the test set for the `OT` variable for all models. ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} cutoffs = Y_hat_df['cutoff'].unique()[::horizon] Y_plot = Y_hat_df[Y_hat_df['cutoff'].isin(cutoffs)].drop(columns='cutoff') plot_series(forecasts_df=Y_plot, ids=['OT']) ``` Finally, we compute the test errors using the Mean Absolute Error (MAE) and Mean Squared Error (MSE): $\qquad MAE = \frac{1}{Windows * Horizon} \sum_{\tau} |y_{\tau} - \hat{y}_{\tau}| \qquad$ and $\qquad MSE = \frac{1}{Windows * Horizon} \sum_{\tau} (y_{\tau} - \hat{y}_{\tau})^{2} \qquad$ ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae, mse ``` ```python theme={null} evaluate(Y_hat_df.drop(columns='cutoff'), metrics=[mae, mse], agg_fn='mean') ``` | | metric | TSMixer | TSMixerx | MLPMultivariate | NHITS | | - | ------ | -------- | -------- | --------------- | -------- | | 0 | mae | 0.245435 | 0.249727 | 0.263579 | 0.251008 | | 1 | mse | 0.162566 | 0.163098 | 0.176594 | 0.178864 | For reference, we can check the performance when compared to self-reported performance in the paper. We find that `TSMixer` provides better results than the *univariate* method `NHITS`. Also, our implementation of `TSMixer` very closely tracks the results of the original paper. Finally, it seems that there is little benefit of using the additional exogenous variables contained in the dataframe `X_df` as `TSMixerx` performs worse than `TSMixer`, especially on longer horizons. Note also that `MLPMultivariate` clearly underperforms as compared to the other methods, which can be somewhat expected given its relative simplicity. Mean Absolute Error (MAE) | Horizon | TSMixer
(this notebook) | TSMixer
(paper) | TSMixerx
(this notebook) | NHITS
(this notebook) | NHITS
(paper) | MLPMultivariate
(this notebook) | | ------- | ----------------------------- | --------------------- | ------------------------------ | --------------------------- | ------------------- | ------------------------------------- | | 96 | **0.245** | 0.252 | 0.250 | 0.251 | 0.251 | 0.263 | | 192 | **0.288** | 0.290 | 0.300 | 0.291 | 0.305 | 0.361 | | 336 | **0.323** | 0.324 | 0.380 | 0.344 | 0.346 | 0.390 | | 720 | **0.377** | 0.422 | 0.464 | 0.417 | 0.413 | 0.608 | Mean Squared Error (MSE) | Horizon | TSMixer
(this notebook) | TSMixer
(paper) | TSMixerx
(this notebook) | NHITS
(this notebook) | NHITS
(paper) | MLPMultivariate
(this notebook) | | ------- | ----------------------------- | --------------------- | ------------------------------ | --------------------------- | ------------------- | ------------------------------------- | | 96 | **0.163** | **0.163** | 0.163 | 0.179 | 0.179 | 0.177 | | 192 | 0.220 | **0.216** | 0.231 | 0.239 | 0.245 | 0.330 | | 336 | 0.272 | **0.268** | 0.361 | 0.311 | 0.295 | 0.376 | | 720 | **0.356** | 0.420 | 0.493 | 0.451 | 0.401 | 3.421 | Note that for the table above, we use the same hyperparameters for all methods for all horizons, whereas the original papers tune the hyperparameters for each horizon. ## 5. Tuning the hyperparameters The `AutoTSMixer` / `AutoTSMixerx` class will automatically perform hyperparameter tuning using the [Tune library](https://docs.ray.io/en/latest/tune/index.html), exploring a user-defined or default search space. Models are selected based on the error on a validation set and the best model is then stored and used during inference. The `AutoTSMixer.default_config` / `AutoTSMixerx.default_config` attribute contains a suggested hyperparameter space. Here, we specify a different search space following the paper’s hyperparameters. Feel free to play around with this space. For this example, we will optimize the hyperparameters for `horizon = 96`. ```python theme={null} from ray import tune from ray.tune.search.hyperopt import HyperOptSearch from neuralforecast.auto import AutoTSMixer, AutoTSMixerx ``` ```python theme={null} horizon = 96 # 24hrs = 4 * 15 min. tsmixer_config = { "input_size": input_size, # Size of input window "max_steps": tune.choice([500, 1000, 2000]), # Number of training iterations "val_check_steps": 100, # Compute validation every x steps "early_stop_patience_steps": 5, # Early stopping steps "learning_rate": tune.loguniform(1e-4, 1e-2), # Initial Learning rate "n_block": tune.choice([1, 2, 4, 6, 8]), # Number of mixing layers "dropout": tune.uniform(0.0, 0.99), # Dropout "ff_dim": tune.choice([32, 64, 128]), # Dimension of the feature linear layer "scaler_type": 'identity', } tsmixerx_config = tsmixer_config.copy() tsmixerx_config['futr_exog_list'] = ['ex_1', 'ex_2', 'ex_3', 'ex_4'] ``` To instantiate `AutoTSMixer` and `AutoTSMixerx` you need to define: * `h`: forecasting horizon * `n_series`: number of time series in the multivariate time series problem. In addition, we define the following parameters (if these are not given, the `AutoTSMixer`/`AutoTSMixerx` class will use a pre-defined value): \* `loss`: training loss. Use the `DistributionLoss` to produce probabilistic forecasts. \* `config`: hyperparameter search space. If `None`, the `AutoTSMixer` class will use a pre-defined suggested hyperparameter space. \* `num_samples`: number of configurations explored. For this example, we only use a limited amount of `10`. \* `search_alg`: type of search algorithm used for selecting parameter values within the hyperparameter space. \* `backend`: the backend used for the hyperparameter optimization search, either `ray` or `optuna`. \* `valid_loss`: the loss used for the validation sets in the optimization procedure. ```python theme={null} model = AutoTSMixer(h=horizon, n_series=7, loss=MAE(), config=tsmixer_config, num_samples=10, search_alg=HyperOptSearch(), backend='ray', valid_loss=MAE()) modelx = AutoTSMixerx(h=horizon, n_series=7, loss=MAE(), config=tsmixerx_config, num_samples=10, search_alg=HyperOptSearch(), backend='ray', valid_loss=MAE()) ``` Now, we fit the model by instantiating a `NeuralForecast` object with the following required parameters: * `models`: a list of models. * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) The `cross_validation` method allows you to simulate multiple historic forecasts, greatly simplifying pipelines by replacing for loops with `fit` and `predict` methods. With time series data, cross validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The `cross_validation` method will use the validation set for hyperparameter selection, and will then produce the forecasts for the test set. ```python theme={null} %%capture nf = NeuralForecast(models=[model, modelx], freq='15min') Y_hat_df = nf.cross_validation(df=Y_df, val_size=val_size, test_size=test_size, n_windows=None) ``` ## 6. Evaluate Results The `AutoTSMixer`/`AutoTSMixerx` class contains a `results` attribute that stores information of each configuration explored. It contains the validation loss and best validation hyperparameter. The result dataframe `Y_hat_df` that we obtained in the previous step is based on the best config of the hyperparameter search. For `AutoTSMixer`, the best config is: ```python theme={null} nf.models[0].results.get_best_result().config ``` ```text theme={null} {'input_size': 512, 'max_steps': 2000, 'val_check_steps': 100, 'early_stop_patience_steps': 5, 'learning_rate': 0.00034884229033995355, 'n_block': 4, 'dropout': 0.7592667651473878, 'ff_dim': 128, 'scaler_type': 'identity', 'n_series': 7, 'h': 96, 'loss': MAE(), 'valid_loss': MAE()} ``` and for `AutoTSMixerx`: ```python theme={null} nf.models[1].results.get_best_result().config ``` ```text theme={null} {'input_size': 512, 'max_steps': 2000, 'val_check_steps': 100, 'early_stop_patience_steps': 5, 'learning_rate': 0.00019039338576148522, 'n_block': 6, 'dropout': 0.5902743834953548, 'ff_dim': 128, 'scaler_type': 'identity', 'futr_exog_list': ('ex_1', 'ex_2', 'ex_3', 'ex_4'), 'n_series': 7, 'h': 96, 'loss': MAE(), 'valid_loss': MAE()} ``` We compute the test errors of the best config for the two metrics of interest: $\qquad MAE = \frac{1}{Windows * Horizon} \sum_{\tau} |y_{\tau} - \hat{y}_{\tau}| \qquad$ and $\qquad MSE = \frac{1}{Windows * Horizon} \sum_{\tau} (y_{\tau} - \hat{y}_{\tau})^{2} \qquad$ ```python theme={null} evaluate(Y_hat_df.drop(columns='cutoff'), metrics=[mae, mse], agg_fn='mean') ``` | | metric | AutoTSMixer | AutoTSMixerx | | - | ------ | ----------- | ------------ | | 0 | mae | 0.243749 | 0.251972 | | 1 | mse | 0.162212 | 0.164347 | We can compare the error metrics for our optimized setting to the earlier setting in which we used the default hyperparameters. In this case, for a horizon of 96, we got slightly improved results for `TSMixer` on `MAE`. Interestingly, we did not improve for `TSMixerx` as compared to the default settings. For this dataset, it seems there is limited value in using exogenous features with the `TSMixerx` architecture for a horizon of 96. | Metric | TSMixer
(optimized) | TSMixer
(default) | TSMixer
(paper) | TSMixerx
(optimized) | TSMixerx
(default) | | ------ | ------------------------- | ----------------------- | --------------------- | -------------------------- | ------------------------ | | MAE | **0.244** | 0.245 | 0.252 | 0.252 | 0.250 | | MSE | **0.162** | 0.163 | 0.163 | 0.164 | 0.163 | Note that we only evaluated 10 hyperparameter configurations (`num_samples=10`), which may suggest that it is possible to further improve forecasting performance by exploring more hyperparameter configurations. ## References [Chen, Si-An, Chun-Liang Li, Nate Yoder, Sercan O. Arik, and Tomas Pfister (2023). “TSMixer: An All-MLP Architecture for Time Series Forecasting.”](http://arxiv.org/abs/2303.06053)
[Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Robust Forecasting Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/robust_forecasting.html When outliers are present in a dataset, they can disrupt the calculated summary statistics, such as the mean and standard deviation, leading the model to favor the outlier values and deviate from most observations. Consequently, models need help in achieving a balance between accurately accommodating outliers and performing well on normal data, resulting in improved overall performance on both types of data. [Robust regression algorithms](https://en.wikipedia.org/wiki/Robust_regression) tackle this issue, explicitly accounting for outliers in the dataset. In this notebook we will show how to fit robust NeuralForecast methods. We will:
- Installing NeuralForecast.
- Loading Noisy AirPassengers.
- Fit and predict robustified NeuralForecast.
- Plot and evaluate predictions.
You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing NeuralForecast ```python theme={null} %%capture !pip install neuralforecast ``` ```python theme={null} import logging import numpy as np import pandas as pd import matplotlib.pyplot as plt from random import random from random import randint from random import seed from neuralforecast import NeuralForecast from neuralforecast.utils import AirPassengersDF from neuralforecast.models import NHITS from neuralforecast.losses.pytorch import MQLoss, DistributionLoss, HuberMQLoss from utilsforecast.losses import mape, mqloss from utilsforecast.evaluation import evaluate ``` ```python theme={null} logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) ``` ## 2. Loading Noisy AirPassengers For this example we will use the classic Box-Cox AirPassengers dataset that we will augment it by introducing outliers. In particular, we will focus on introducing outliers to the target variable altering it to deviate from its original observation by a specified factor, such as 2-to-4 times the standard deviation. ```python theme={null} # Original Box-Cox AirPassengers # as defined in neuralforecast.utils Y_df = AirPassengersDF.copy() plt.plot(Y_df.y) plt.ylabel('Monthly Passengers') plt.xlabel('Timestamp [t]') plt.grid() ``` ```python theme={null} # Here we add some artificial outliers to AirPassengers seed(1) for i in range(len(Y_df)): factor = randint(2, 4) if random() > 0.97: Y_df.loc[i, "y"] += factor * Y_df["y"].std() plt.plot(Y_df.y) plt.ylabel('Monthly Passengers + Noise') plt.xlabel('Timestamp [t]') plt.grid() ``` ```python theme={null} # Split datasets into train/test # Last 12 months for test Y_train_df = Y_df.groupby('unique_id').head(-12) Y_test_df = Y_df.groupby('unique_id').tail(12) Y_test_df ``` | | unique\_id | ds | y | | --- | ---------- | ---------- | ----- | | 132 | 1.0 | 1960-01-31 | 417.0 | | 133 | 1.0 | 1960-02-29 | 391.0 | | 134 | 1.0 | 1960-03-31 | 419.0 | | 135 | 1.0 | 1960-04-30 | 461.0 | | 136 | 1.0 | 1960-05-31 | 472.0 | | 137 | 1.0 | 1960-06-30 | 535.0 | | 138 | 1.0 | 1960-07-31 | 622.0 | | 139 | 1.0 | 1960-08-31 | 606.0 | | 140 | 1.0 | 1960-09-30 | 508.0 | | 141 | 1.0 | 1960-10-31 | 461.0 | | 142 | 1.0 | 1960-11-30 | 390.0 | | 143 | 1.0 | 1960-12-31 | 432.0 | ## 3. Fit and predict robustified NeuralForecast ### Huber MQ Loss The Huber loss, employed in robust regression, is a loss function that exhibits reduced sensitivity to outliers in data when compared to the squared error loss. The Huber loss function is quadratic for small errors and linear for large errors. Here we will use a slight modification for probabilistic predictions. Feel free to play with the $\delta$ parameter. ![](https://github.com/Nixtla/neuralforecast/blob/main/nbs/imgs_losses/huber_loss.png?raw=1) ### Dropout Regularization The dropout technique is a regularization method used in neural networks to prevent overfitting. During training, dropout randomly sets a fraction of the input units or neurons in a layer to zero at each update, effectively “dropping out” those units. This means that the network cannot rely on any individual unit because it may be dropped out at any time. By doing so, dropout forces the network to learn more robust and generalizable representations by preventing units from co-adapting too much. The dropout method, can help us to robustify the network to outliers in the auto-regressive features. You can explore it through the `dropout_prob_theta` parameter. ### Fit NeuralForecast models Using the `NeuralForecast.fit` method you can train a set of models to your dataset. You can define the forecasting `horizon` (12 in this example), and modify the hyperparameters of the model. For example, for the `NHITS` we changed the default hidden size for both encoder and decoders. See the `NHITS` and `MLP` [model documentation](https://nixtlaverse.nixtla.io/neuralforecast/models.mlp.html). ```python theme={null} %%capture horizon = 12 level = [50, 80] # Try different hyperparmeters to improve accuracy. models = [NHITS(h=horizon, # Forecast horizon input_size=2 * horizon, # Length of input sequence loss=HuberMQLoss(level=level), # Robust Huber Loss valid_loss=MQLoss(level=level), # Validation signal max_steps=500, # Number of steps to train dropout_prob_theta=0.6, # Dropout to robustify vs outlier lag inputs #early_stop_patience_steps=2, # Early stopping regularization patience val_check_steps=10, # Frequency of validation signal (affects early stopping) alias='Huber', ), NHITS(h=horizon, input_size=2 * horizon, loss=DistributionLoss(distribution='Normal', level=level), # Classic Normal distribution valid_loss=MQLoss(level=level), max_steps=500, #early_stop_patience_steps=2, dropout_prob_theta=0.6, val_check_steps=10, alias='Normal', ) ] nf = NeuralForecast(models=models, freq='M') nf.fit(df=Y_train_df) Y_hat_df = nf.predict() ``` ```python theme={null} # By default NeuralForecast produces forecast intervals # In this case the lo-x and high-x levels represent the # low and high bounds of the prediction accumulating x% probability Y_hat_df ``` | | unique\_id | ds | Huber-median | Huber-lo-80 | Huber-lo-50 | Huber-hi-50 | Huber-hi-80 | Normal | Normal-median | Normal-lo-80 | Normal-lo-50 | Normal-hi-50 | Normal-hi-80 | | -- | ---------- | ---------- | ------------ | ----------- | ----------- | ----------- | ----------- | ---------- | ------------- | ------------ | ------------ | ------------ | ------------ | | 0 | 1.0 | 1960-01-31 | 412.738525 | 401.058044 | 406.131958 | 420.779266 | 432.124268 | 406.459717 | 416.787842 | -124.278656 | 135.413223 | 680.997070 | 904.871765 | | 1 | 1.0 | 1960-02-29 | 403.913544 | 384.403534 | 391.904419 | 420.288208 | 469.040375 | 399.827148 | 418.305725 | -137.291870 | 103.988327 | 661.940430 | 946.699219 | | 2 | 1.0 | 1960-03-31 | 472.311523 | 446.644531 | 460.767334 | 486.710999 | 512.552979 | 380.263947 | 378.253998 | -105.411003 | 117.415565 | 647.887695 | 883.611633 | | 3 | 1.0 | 1960-04-30 | 460.996674 | 444.471039 | 452.971802 | 467.544189 | 480.843903 | 432.131378 | 442.395844 | -104.205200 | 135.457123 | 729.306885 | 974.661743 | | 4 | 1.0 | 1960-05-31 | 465.534790 | 452.048889 | 457.472626 | 476.141022 | 490.311005 | 417.186279 | 417.956543 | -117.399597 | 150.915833 | 692.936523 | 930.934814 | | 5 | 1.0 | 1960-06-30 | 538.116028 | 518.049866 | 527.238159 | 551.501709 | 563.818848 | 444.510834 | 440.168396 | -54.501572 | 189.301392 | 703.502014 | 946.068909 | | 6 | 1.0 | 1960-07-31 | 613.937866 | 581.048035 | 597.368408 | 629.111450 | 645.550659 | 423.707275 | 431.251526 | -97.069489 | 164.821259 | 687.764526 | 942.432251 | | 7 | 1.0 | 1960-08-31 | 616.188660 | 581.982300 | 599.544128 | 632.137512 | 643.219543 | 386.655823 | 383.755157 | -134.702011 | 139.954285 | 658.973022 | 897.393494 | | 8 | 1.0 | 1960-09-30 | 537.559143 | 513.477478 | 526.664856 | 551.563293 | 573.146667 | 388.874817 | 379.827057 | -139.859344 | 110.772484 | 673.086182 | 926.355774 | | 9 | 1.0 | 1960-10-31 | 471.107605 | 449.207916 | 459.288025 | 486.402985 | 515.082458 | 401.483643 | 412.114990 | -185.928085 | 95.805717 | 703.490784 | 970.837830 | | 10 | 1.0 | 1960-11-30 | 412.758423 | 389.203308 | 398.727295 | 431.723602 | 451.208588 | 425.829895 | 425.018799 | -172.022018 | 108.840889 | 723.424011 | 1035.656128 | | 11 | 1.0 | 1960-12-31 | 457.254761 | 438.565582 | 446.097168 | 468.809296 | 483.967865 | 406.916595 | 399.852051 | -199.963684 | 110.715050 | 729.735107 | 951.728577 | ## 4. Plot and Evaluate Predictions Finally, we plot the forecasts of both models against the real values. And evaluate the accuracy of the `NHITS-Huber` and `NHITS-Normal` forecasters. ```python theme={null} fig, ax = plt.subplots(1, 1, figsize = (20, 7)) plot_df = pd.concat([Y_train_df, Y_hat_df]).set_index('ds') # Concatenate the train and forecast dataframes plot_df[['y', 'Huber-median', 'Normal-median']].plot(ax=ax, linewidth=2) ax.set_title('Noisy AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Timestamp [t]', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` To evaluate the median predictions we use the mean average percentage error (MAPE), defined as follows: $\mathrm{MAPE}(\mathbf{y}_{\tau}, \hat{\mathbf{y}}_{\tau}) = \mathrm{mean}\left(\frac{|\mathbf{y}_{\tau}-\hat{\mathbf{y}}_{\tau}|}{|\mathbf{y}_{\tau}|}\right)$ To evaluate the coherent probabilistic predictions we use the Continuous Ranked Probability Score (CRPS), defined as follows: $\mathrm{CRPS}(\hat{F}_{\tau},\mathbf{y}_{\tau}) = \int^{1}_{0} \mathrm{QL}(\hat{F}_{\tau}, y_{\tau})_{q} dq$ As you can see, robust regression improvements reflect in both the normal and probabilistic forecast setting. ```python theme={null} df_metrics = Y_hat_df.merge(Y_test_df, on=['ds', 'unique_id']) df_metrics.rename(columns={'Huber-median': 'Huber'}, inplace=True) metrics = evaluate(df_metrics, metrics=[mape, mqloss], models=['Huber', 'Normal'], level = [50, 80], agg_fn="mean") metrics ``` | | metric | Huber | Normal | | - | ------ | -------- | --------- | | 0 | mape | 0.034726 | 0.140207 | | 1 | mqloss | 5.511535 | 61.891651 | ## References * [Huber Peter, J (1964). “Robust Estimation of a Location Parameter”. Annals of Statistics.](https://projecteuclid.org/journals/annals-of-mathematical-statistics/volume-35/issue-1/Robust-Estimation-of-a-Location-Parameter/10.1214/aoms/1177703732.full)
* [Nitish Srivastava, Geoffrey Hinton, Alex Krizhevsky, Ilya Sutskever, Ruslan Salakhutdinov (2014).”Dropout: A Simple Way to Prevent Neural Networks from Overfitting”. Journal of Machine Learning Research.](https://jmlr.org/papers/v15/srivastava14a.html)
* [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2023). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Simulation Paths | NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/simulation.html > Generate correlated sample paths for scenario analysis ## Simulation vs. Prediction Intervals: When to Use Which NeuralForecast provides two complementary approaches for reasoning about future uncertainty: ### Prediction Intervals (`predict(level=...)`) Prediction intervals answer: **“What range of values is the target likely to fall within at each future time step?”** * Output: Independent confidence bands per horizon step (e.g., 90% interval at step $h$). * Each step is treated marginally — no information about how steps relate to each other. * Best for: monitoring, alerting, dashboarding, reporting confidence bands. ### Simulation Paths (`simulate(...)`) Simulation paths answer: **“What are realistic *joint* future trajectories the time series could follow?”** * Output: $N$ complete trajectories of length $H$, each representing a plausible future. * Temporal correlations between steps are preserved — if step $h$ is high, step $h+1$ is likely high too. * Best for: scenario analysis, portfolio optimization, supply chain planning, energy dispatch, risk assessment (VaR/CVaR), stochastic programming. ### Key Difference: Marginal vs. Joint Consider electricity price forecasting over 24 hours. A 90% prediction interval tells you the price at hour 12 will likely be between \$40 and \$80. But it says nothing about whether hours 11, 12, and 13 will *all* be high simultaneously (a sustained price spike) or whether high and low values alternate randomly. Simulation paths capture this joint structure. From 500 simulated paths you can compute: - **P(total cost > budget)** — requires summing across correlated hours - **P(at least 3 consecutive hours above \$70)** — requires temporal ordering - **Expected shortfall (CVaR)** — requires the joint tail distribution These questions *cannot* be answered from marginal prediction intervals alone. ## Available Simulation Methods | Method | Temporal Correlation | Description | | --------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `gaussian_copula` (default) | AR(1) Gaussian copula | Draws correlated uniform samples via Cholesky decomposition, maps through marginal CDFs. Parametric correlation structure. | | `schaake_shuffle` | Empirical (historical templates) | Draws independent marginal samples, reorders them to match rank structure of historical trajectory templates. Nonparametric — captures arbitrary dependence. | **Compatible losses**: - **`DistributionLoss`** (Normal, StudentT, Poisson, etc.) and **mixture losses** (`GMM`, `PMM`, `NBMM`) — produce arbitrary quantiles natively. - **`MQLoss`** / **`HuberMQLoss`** — uses the model’s trained quantile grid. - **`IQLoss`** / **`HuberIQLoss`** — evaluates multiple quantiles via repeated forward passes. - **Point losses** (`MAE`, `MSE`, etc.) — requires `prediction_intervals` (Conformal Prediction) set during `fit()` to build a quantile grid from calibration scores. ## 1. Setup ```python theme={null} import logging import warnings import matplotlib.pyplot as plt import numpy as np import pandas as pd import torch warnings.filterwarnings("ignore") logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) torch.set_float32_matmul_precision("high") ``` ## 2. Load Data We use the AirPassengers dataset — a classic monthly time series of airline passenger counts. ```python theme={null} from neuralforecast.utils import AirPassengersDF Y_df = AirPassengersDF.copy() Y_train_df = Y_df[Y_df.ds <= "1959-12-31"] # 132 months train Y_test_df = Y_df[Y_df.ds > "1959-12-31"] # 12 months test print(f"Train: {len(Y_train_df)} rows, Test: {len(Y_test_df)} rows") Y_train_df.tail() ``` ```text theme={null} 2026-07-28 21:22:50,689 INFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output. 2026-07-28 21:22:50,793 INFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output. ``` ```text theme={null} Train: 132 rows, Test: 12 rows ``` | | unique\_id | ds | y | | --- | ---------- | ---------- | ----- | | 127 | 1.0 | 1959-08-31 | 559.0 | | 128 | 1.0 | 1959-09-30 | 463.0 | | 129 | 1.0 | 1959-10-31 | 407.0 | | 130 | 1.0 | 1959-11-30 | 362.0 | | 131 | 1.0 | 1959-12-31 | 405.0 | ## 3. Train Models We train five models showcasing different loss types: - **NHITS + GMM** — Gaussian Mixture Model, a mixture `DistributionLoss`. - **NHITS + DistributionLoss(Normal)** — parametric distribution output. - **NHITS + MQLoss** — Multi-Quantile Loss, directly optimizes quantile levels. - **NHITS + IQLoss** — Implicit Quantile Loss, can evaluate any quantile at inference. - **NHITS + MAE** with Conformal Prediction — point-loss model with calibrated prediction intervals. Since the MAE model needs conformal prediction intervals, we pass `prediction_intervals` to `fit()`. ```python theme={null} from neuralforecast import NeuralForecast from neuralforecast.losses.pytorch import GMM, MAE, MQLoss, DistributionLoss, IQLoss from neuralforecast.models import NHITS from neuralforecast.utils import PredictionIntervals H = 12 MAX_STEPS = 100 models = [ NHITS(h=H, input_size=36, max_steps=MAX_STEPS, loss=GMM(), alias="NHITS_GMM", scaler_type="robust"), NHITS(h=H, input_size=36, max_steps=MAX_STEPS, loss=DistributionLoss(distribution="Normal"), alias="NHITS_Normal", scaler_type="robust"), NHITS(h=H, input_size=36, max_steps=MAX_STEPS, loss=MQLoss(level=[80, 90]), alias="NHITS_MQ", scaler_type="robust"), NHITS(h=H, input_size=36, max_steps=MAX_STEPS, loss=IQLoss(), alias="NHITS_IQL", scaler_type="robust"), NHITS(h=H, input_size=36, max_steps=MAX_STEPS, loss=MAE(), alias="NHITS_MAE", scaler_type="robust"), ] nf = NeuralForecast(models=models, freq="MS") nf.fit( df=Y_train_df, prediction_intervals=PredictionIntervals(n_windows=2, method="conformal_error"), ) ``` ## 4. Prediction Intervals (Baseline) First, let’s see the standard prediction intervals — these are marginal (per-step) uncertainty bands. ```python theme={null} from utilsforecast.plotting import plot_series fcst_df = nf.predict(level=[80, 90]) ``` ```python theme={null} plot_series(Y_train_df, fcst_df, level=[80, 90], models=["NHITS_Normal"]) ``` The shaded bands show the 80% and 90% prediction intervals. These are useful for understanding per-step uncertainty, but they do not capture the **temporal correlation** between forecast steps. Each band is computed independently. ## 5. Simulation Paths Now let’s generate correlated simulation paths using the `simulate()` method. This returns a long-format DataFrame with columns `[unique_id, ds, sample_id, model_1, model_2, ...]`. ### Plotting helper ```python theme={null} def extract_sims(sim_df, model_col, uid=None): """Extract simulation paths as a (n_paths, H) numpy array from simulate() output.""" if uid is None: uid = sim_df["unique_id"].iloc[0] series = sim_df[sim_df["unique_id"] == uid] return series.pivot(index="sample_id", columns="ds", values=model_col).values def plot_simulations(train_df, sims, model_name, title, color="steelblue", n_show=100): """Plot historical data with simulated future paths and derived prediction intervals.""" fig, ax = plt.subplots(1, 1, figsize=(10, 4)) # History (last 48 months) hist = train_df.sort_values("ds").tail(48) ax.plot(hist["ds"], hist["y"], color="black", linewidth=1.5, label="History") # Future dates last_date = hist["ds"].iloc[-1] future_dates = pd.date_range(start=last_date, periods=sims.shape[1] + 1, freq="MS")[1:] # Individual paths for i in range(min(n_show, sims.shape[0])): ax.plot(future_dates, sims[i], color=color, alpha=0.05, linewidth=0.5) # Derived prediction intervals from simulations for q_lo, q_hi, alpha in [(0.05, 0.95, 0.15), (0.10, 0.90, 0.25), (0.25, 0.75, 0.35)]: lo = np.quantile(sims, q_lo, axis=0) hi = np.quantile(sims, q_hi, axis=0) ax.fill_between(future_dates, lo, hi, color=color, alpha=alpha, label=f"{int((q_hi - q_lo) * 100)}% PI") # Median median = np.median(sims, axis=0) ax.plot(future_dates, median, color=color, linewidth=2, label="Median") # Actual (if available) ax.plot(Y_test_df["ds"], Y_test_df["y"], color="black", linestyle="--", label="Actual") ax.set_title(f"{title} — {model_name}") ax.legend(loc="upper left", fontsize=8) ax.set_xlabel("Date") ax.set_ylabel("Passengers") plt.tight_layout() plt.show() ``` ### Generate simulation paths `simulate()` generates `n_paths` correlated sample paths for every model × series combination. ```python theme={null} N_PATHS = 500 SEED = 42 sim_df = nf.simulate(n_paths=N_PATHS, seed=SEED) ``` ```python theme={null} sim_df.head(10) ``` | | unique\_id | ds | sample\_id | NHITS\_GMM | NHITS\_Normal | NHITS\_MQ | NHITS\_IQL | NHITS\_MAE | | - | ---------- | ---------- | ---------- | ---------- | ------------- | ---------- | ---------- | ---------- | | 0 | 1.0 | 1960-01-01 | 0 | 424.577554 | 424.765725 | 415.206802 | 409.314326 | 438.886536 | | 1 | 1.0 | 1960-02-01 | 0 | 404.827416 | 404.260575 | 414.203816 | 388.755800 | 416.497571 | | 2 | 1.0 | 1960-03-01 | 0 | 469.169496 | 469.978479 | 464.367880 | 466.584492 | 482.319912 | | 3 | 1.0 | 1960-04-01 | 0 | 450.245014 | 451.145057 | 454.416200 | 462.261105 | 459.493987 | | 4 | 1.0 | 1960-05-01 | 0 | 472.641135 | 471.130120 | 475.350769 | 486.486899 | 493.030374 | | 5 | 1.0 | 1960-06-01 | 0 | 528.117486 | 527.280406 | 539.266724 | 497.874372 | 530.030655 | | 6 | 1.0 | 1960-07-01 | 0 | 594.137207 | 592.315796 | 623.242188 | 507.372412 | 586.462607 | | 7 | 1.0 | 1960-08-01 | 0 | 604.532895 | 604.907234 | 617.977478 | 531.723887 | 583.457802 | | 8 | 1.0 | 1960-09-01 | 0 | 530.447128 | 529.882652 | 545.858480 | 566.198389 | 524.067964 | | 9 | 1.0 | 1960-10-01 | 0 | 458.686895 | 458.105852 | 466.378017 | 466.831645 | 468.689723 | ### Visualize paths for each model ```python theme={null} # Extract (n_paths, H) arrays for plotting model_cols = [c for c in sim_df.columns if c not in ("unique_id", "ds", "sample_id")] for model_col in model_cols: sims = extract_sims(sim_df, model_col) print(f"{model_col}: simulation paths shape = {sims.shape}") plot_simulations(Y_train_df, sims, model_col, "Gaussian Copula", color="tab:red") ``` ```text theme={null} NHITS_GMM: simulation paths shape = (500, 12) NHITS_Normal: simulation paths shape = (500, 12) NHITS_MQ: simulation paths shape = (500, 12) NHITS_IQL: simulation paths shape = (500, 12) NHITS_MAE: simulation paths shape = (500, 12) ``` ## 6. Using Simulation Paths for Decision-Making The key advantage of simulation paths over prediction intervals is answering **joint** probabilistic questions. Here are concrete examples. ### Example: Probability of exceeding a cumulative threshold Suppose we have a capacity of 5,800 total passengers over the next 12 months. What is the probability of exceeding this capacity? This question requires the **joint** distribution — it cannot be answered from marginal intervals. ```python theme={null} THRESHOLD = 5800 # Use NHITS_GMM simulations sims_gmm = extract_sims(sim_df, "NHITS_GMM") cumulative_passengers = sims_gmm.sum(axis=1) # Sum across H=12 months per path prob_exceed = (cumulative_passengers > THRESHOLD).mean() print(f"P(total passengers > {THRESHOLD:,}) = {prob_exceed:.1%}") print(f"Expected total passengers = {cumulative_passengers.mean():,.0f}") print(f"5th percentile = {np.quantile(cumulative_passengers, 0.05):,.0f}") print(f"95th percentile = {np.quantile(cumulative_passengers, 0.95):,.0f}") fig, ax = plt.subplots(figsize=(8, 3)) ax.hist(cumulative_passengers, bins=50, color="tab:red", alpha=0.7, edgecolor="white") ax.axvline(THRESHOLD, color="black", linestyle="--", linewidth=2, label=f"Threshold = {THRESHOLD:,}") ax.axvline(Y_test_df["y"].sum(), color="green", linestyle="-", linewidth=2, label=f"Actual = {Y_test_df['y'].sum():,.0f}") ax.set_xlabel("Total passengers (12 months)") ax.set_ylabel("Count") ax.set_title("Distribution of cumulative passengers from simulation paths") ax.legend() plt.tight_layout() plt.show() ``` ```text theme={null} P(total passengers > 5,800) = 35.4% Expected total passengers = 5,783 5th percentile = 5,720 95th percentile = 5,851 ``` ## 7. Comparing Simulation Methods: Gaussian Copula vs. Schaake Shuffle Both methods use the same marginal quantile forecasts but differ in how they introduce temporal correlation across horizon steps. Let’s compare them on the **NHITS\_Normal** model. ```python theme={null} # Generate paths with both methods sim_copula = nf.simulate(n_paths=N_PATHS, seed=SEED, method="gaussian_copula") sim_schaake = nf.simulate(n_paths=N_PATHS, seed=SEED, method="schaake_shuffle") ``` ```python theme={null} sims_copula = extract_sims(sim_copula, "NHITS_Normal") sims_schaake = extract_sims(sim_schaake, "NHITS_Normal") # Side-by-side path comparison fig, axes = plt.subplots(1, 2, figsize=(16, 5), sharey=True) hist = Y_train_df.sort_values("ds").tail(48) last_date = hist["ds"].iloc[-1] future_dates = pd.date_range(start=last_date, periods=H + 1, freq="MS")[1:] for ax, sims, title, color in zip( axes, [sims_copula, sims_schaake], ["Gaussian Copula", "Schaake Shuffle"], ["tab:red", "tab:purple"], ): ax.plot(hist["ds"], hist["y"], color="black", linewidth=1.5, label="History") for i in range(min(100, sims.shape[0])): ax.plot(future_dates, sims[i], color=color, alpha=0.05, linewidth=0.5) for q_lo, q_hi, alpha in [(0.05, 0.95, 0.15), (0.25, 0.75, 0.30)]: lo = np.quantile(sims, q_lo, axis=0) hi = np.quantile(sims, q_hi, axis=0) ax.fill_between(future_dates, lo, hi, color=color, alpha=alpha) median = np.median(sims, axis=0) ax.plot(future_dates, median, color=color, linewidth=2, label="Median") ax.plot(Y_test_df["ds"], Y_test_df["y"], color="black", linestyle="--", label="Actual") ax.set_title(f"{title} — NHITS_Normal") ax.set_xlabel("Date") ax.legend(loc="upper left", fontsize=8) axes[0].set_ylabel("Passengers") plt.suptitle("Simulation Method Comparison (NHITS_Normal)", fontsize=14) plt.tight_layout() plt.show() ``` ### Diagnosing the dependence structure The path bundles above look nearly identical, and they should: both methods share the same marginal quantiles, so any per-step summary — the median line, the shaded intervals — is by construction the same. The methods differ only in the **joint** structure, which no marginal view can reveal. Two diagnostics that do reveal it: the step-to-step rank correlation of the paths, and the mean longest run of consecutive months above their own median. Both are compared against the same statistics on the historical rolling windows (the Schaake shuffle’s own templates) and against a null built by permuting each step independently — identical marginals, dependence destroyed. ```python theme={null} def lag1_rank_corr(paths): """Mean lag-1 Spearman correlation across consecutive horizon steps.""" ranks = np.apply_along_axis(lambda c: np.argsort(np.argsort(c)), 0, paths) return np.mean([ np.corrcoef(ranks[:, h], ranks[:, h + 1])[0, 1] for h in range(paths.shape[1] - 1) ]) def mean_longest_run(paths): """Mean longest run of consecutive months above that month's median.""" above = paths > np.median(paths, axis=0) runs = [] for row in above: best = current = 0 for flag in row: current = current + 1 if flag else 0 best = max(best, current) runs.append(best) return np.mean(runs) def break_dependence(paths, seed=0): """Permute each step independently: same marginals, no temporal structure.""" rng = np.random.default_rng(seed) return np.column_stack([rng.permutation(paths[:, h]) for h in range(paths.shape[1])]) # Reference: the same statistics on the history's length-H rolling windows, which is # exactly what the Schaake shuffle uses as its templates. hist_windows = np.lib.stride_tricks.sliding_window_view( Y_train_df["y"].to_numpy(), H ) rows = [ ("History (rolling windows)", hist_windows), ("Gaussian copula", sims_copula), ("Schaake shuffle", sims_schaake), ("Schaake, dependence broken", break_dependence(sims_schaake)), ] print(f"{'':28s}{'lag-1 rank corr':>17s}{'mean longest run':>19s}") for name, paths in rows: print(f"{name:28s}{lag1_rank_corr(paths):>+17.3f}{mean_longest_run(paths):>19.2f}") ``` ```text theme={null} lag-1 rank corr mean longest run History (rolling windows) +0.962 5.51 Gaussian copula +0.267 3.49 Schaake shuffle +0.963 5.60 Schaake, dependence broken -0.006 3.04 ``` ## 8. Evaluating Marginal vs. Joint Distributions We can evaluate both the **marginal** distribution (from `predict(quantiles=...)`) and the **joint** distribution (from `simulate()`) using the same metrics. For each model we compare: * **Point metrics**: MAE, MSE on the median forecast. * **Probabilistic metric**: scaled CRPS on the quantile forecasts — either from `predict` (marginal) or derived from simulation paths (joint). If the simulation paths are well-calibrated, their empirical quantiles should score comparably to the model’s native marginal quantiles. ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae, mse, scaled_crps # ── Quantile grid for evaluation ── eval_quantiles = np.round(np.arange(0.1, 1.0, 0.1), 2).tolist() # [0.1, 0.2, ..., 0.9] # We'll evaluate NHITS_Normal only for clarity MODEL = "NHITS_Normal" # ── 1. Marginal quantiles from predict() ── marginal_df = nf.predict(quantiles=eval_quantiles) # Add actuals — align by position (predict uses freq="MS" dates, test data has month-end dates) marginal_df = marginal_df.sort_values(["unique_id", "ds"]).reset_index(drop=True) marginal_df["y"] = Y_test_df.sort_values(["unique_id", "ds"])["y"].values # ── 2. Joint quantiles derived from simulation paths (gaussian_copula) ── sims_gc = extract_sims(sim_copula, MODEL) # (n_paths, H) joint_gc_df = marginal_df[["unique_id", "ds", "y"]].copy() for q in eval_quantiles: joint_gc_df[f"{MODEL}_ql{q}"] = np.quantile(sims_gc, q, axis=0) joint_gc_df[MODEL] = np.median(sims_gc, axis=0) # ── 3. Joint quantiles derived from simulation paths (schaake_shuffle) ── sims_ss = extract_sims(sim_schaake, MODEL) # (n_paths, H) joint_ss_df = marginal_df[["unique_id", "ds", "y"]].copy() for q in eval_quantiles: joint_ss_df[f"{MODEL}_ql{q}"] = np.quantile(sims_ss, q, axis=0) joint_ss_df[MODEL] = np.median(sims_ss, axis=0) ``` ```python theme={null} # ── Evaluate point metrics (via evaluate) ── point_marginal = evaluate(marginal_df, metrics=[mae, mse], models=[MODEL], agg_fn="mean") point_gc = evaluate(joint_gc_df, metrics=[mae, mse], models=[MODEL], agg_fn="mean") point_ss = evaluate(joint_ss_df, metrics=[mae, mse], models=[MODEL], agg_fn="mean") # ── Evaluate scaled CRPS (called directly — evaluate expects level-style columns) ── quantile_cols = {MODEL: [f"{MODEL}_ql{q}" for q in eval_quantiles]} quantiles_arr = np.array(eval_quantiles) crps_marginal = scaled_crps(marginal_df, models=quantile_cols, quantiles=quantiles_arr) crps_gc = scaled_crps(joint_gc_df, models=quantile_cols, quantiles=quantiles_arr) crps_ss = scaled_crps(joint_ss_df, models=quantile_cols, quantiles=quantiles_arr) # ── Combine results ── results = pd.DataFrame({ "Metric": ["MAE", "MSE", "Scaled CRPS"], "Marginal (predict)": [ point_marginal[MODEL].values[0], point_marginal[MODEL].values[1], crps_marginal[MODEL].mean(), ], "Joint — Gaussian Copula": [ point_gc[MODEL].values[0], point_gc[MODEL].values[1], crps_gc[MODEL].mean(), ], "Joint — Schaake Shuffle": [ point_ss[MODEL].values[0], point_ss[MODEL].values[1], crps_ss[MODEL].mean(), ], }).set_index("Metric") print(f"Model: {MODEL}") results ``` ```text theme={null} Model: NHITS_Normal ``` | | Marginal (predict) | Joint — Gaussian Copula | Joint — Schaake Shuffle | | ----------- | ------------------ | ----------------------- | ----------------------- | | Metric | | | | | MAE | 12.669576 | 12.942177 | 12.516772 | | MSE | 276.955045 | 288.357283 | 275.653049 | | Scaled CRPS | 0.021440 | 0.022133 | 0.021545 | **Interpreting the results:** * **MAE / MSE** (point metrics): Both simulation methods use the median of the sample paths as their point forecast. Since the marginal quantiles and simulation-derived quantiles share the same underlying model, the median forecasts are similar but not identical — simulation introduces sampling variability. * **Scaled CRPS** (probabilistic metric): Measures the quality of the full quantile distribution. The marginal quantiles come directly from the model’s predictive distribution, while the simulation-derived quantiles are empirical (computed from `n_paths` samples). With enough paths, the simulation CRPS should converge to the marginal CRPS. ## 9. Summary | Question | Use | | ----------------------------------------------- | ------------------------------------------- | | “What range is step $h$ likely in?” | `predict(level=...)` — prediction intervals | | “What are realistic joint future trajectories?” | `simulate(n_paths=...)` — sample paths | | “What is P(total > threshold)?” | `simulate()` then sum paths | | “What is P(3 consecutive spikes)?” | `simulate()` then check path patterns | | “What is the CVaR of my portfolio?” | `simulate()` then compute tail statistics | ### Simulation Methods | Method | Use when… | | --------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `gaussian_copula` (default) | You want smooth, parametric temporal correlation (AR(1) structure). Fast and robust. | | `schaake_shuffle` | You want nonparametric dependence from historical data. Captures asymmetric / non-Gaussian correlation patterns. | ### Compatible Losses | Loss type | How quantiles are obtained | | ---------------------------------------- | ------------------------------------------------------------------------------- | | `DistributionLoss`, `GMM`, `PMM`, `NBMM` | Arbitrary quantiles from parametric distribution | | `MQLoss` / `HuberMQLoss` | Uses the model’s trained quantile grid | | `IQLoss` / `HuberIQLoss` | Multiple forward passes, one per quantile | | Point losses (`MAE`, `MSE`, etc.) | Conformal Prediction intervals (requires `prediction_intervals` during `fit()`) | ### References * Baron, E. et al. (2025). [Efficiently generating correlated sample paths from multi-step time series foundation models](https://arxiv.org/abs/2510.02224). NeurIPS 2025 Workshop. * Clark, M. et al. (2004). The Schaake shuffle: A method for reconstructing space-time variability. *Journal of Hydrometeorology*, 5(1). # Temporal Classification Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/temporal_classification.html A logistic regression analyzes the relationship between a binary target variable and its predictor variables to estimate the probability of the dependent variable taking the value 1. In the presence of temporal data where observations along time aren’t independent, the errors of the model will be correlated through time and incorporating autoregressive features or lags can capture temporal dependencies and enhance the predictive power of logistic regression.
NHITS’s inputs are static exogenous $\mathbf{x}^{(s)}$, historic exogenous $\mathbf{x}^{(h)}_{[:t]}$, exogenous available at the time of the prediction $\mathbf{x}^{(f)}_{[:t+H]}$ and autoregressive features $\mathbf{y}_{[:t]}$, each of these inputs is further decomposed into categorical and continuous. The network uses a multi-quantile regression to model the following conditional probability:$\mathbb{P}(\mathbf{y}_{[t+1:t+H]}|\;\mathbf{y}_{[:t]},\; \mathbf{x}^{(h)}_{[:t]},\; \mathbf{x}^{(f)}_{[:t+H]},\; \mathbf{x}^{(s)})$ In this notebook we show how to fit NeuralForecast methods for binary sequences regression. We will: - Installing NeuralForecast. - Loading binary sequence data. - Fit and predict temporal classifiers. - Plot and evaluate predictions. You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing NeuralForecast ```python theme={null} %%capture !pip install neuralforecast ``` ```python theme={null} import numpy as np import pandas as pd from sklearn import datasets import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import MLP, NHITS, LSTM from neuralforecast.losses.pytorch import DistributionLoss, Accuracy ``` ## 2. Loading Binary Sequence Data The `core.NeuralForecast` class contains shared, `fit`, `predict` and other methods that take as inputs pandas DataFrames with columns `['unique_id', 'ds', 'y']`, where `unique_id` identifies individual time series from the dataset, `ds` is the date, and `y` is the target binary variable. In this motivation example we convert 8x8 digits images into 64-length sequences and define a classification problem, to identify when the pixels surpass certain threshold. We declare a pandas dataframe in long format, to match NeuralForecast’s inputs. ```python theme={null} digits = datasets.load_digits() images = digits.images[:100] plt.imshow(images[0,:,:], cmap=plt.cm.gray, vmax=16, interpolation="nearest") pixels = np.reshape(images, (len(images), 64)) ytarget = (pixels > 10) * 1 fig, ax1 = plt.subplots() ax2 = ax1.twinx() ax1.plot(pixels[10]) ax2.plot(ytarget[10], color='purple') ax1.set_xlabel('Pixel index') ax1.set_ylabel('Pixel value') ax2.set_ylabel('Pixel threshold', color='purple') plt.grid() plt.show() ``` ```python theme={null} # We flat the images and create an input dataframe # with 'unique_id' series identifier and 'ds' time stamp identifier. Y_df = pd.DataFrame.from_dict({ 'unique_id': np.repeat(np.arange(100), 64), 'ds': np.tile(np.arange(64)+1910, 100), 'y': ytarget.flatten(), 'pixels': pixels.flatten()}) Y_df ``` | | unique\_id | ds | y | pixels | | ---- | ---------- | ---- | --- | ------ | | 0 | 0 | 1910 | 0 | 0.0 | | 1 | 0 | 1911 | 0 | 0.0 | | 2 | 0 | 1912 | 0 | 5.0 | | 3 | 0 | 1913 | 1 | 13.0 | | 4 | 0 | 1914 | 0 | 9.0 | | ... | ... | ... | ... | ... | | 6395 | 99 | 1969 | 1 | 14.0 | | 6396 | 99 | 1970 | 1 | 16.0 | | 6397 | 99 | 1971 | 0 | 3.0 | | 6398 | 99 | 1972 | 0 | 0.0 | | 6399 | 99 | 1973 | 0 | 0.0 | ## 3. Fit and predict temporal classifiers ### Fit the models Using the `NeuralForecast.fit` method you can train a set of models to your dataset. You can define the forecasting `horizon` (12 in this example), and modify the hyperparameters of the model. For example, for the `NHITS` we changed the default hidden size for both encoder and decoders. See the `NHITS` and `MLP` [model documentation](https://nixtlaverse.nixtla.io/neuralforecast/models.mlp.html). > **Warning** > > For the moment Recurrent-based model family is not available to > operate with Bernoulli distribution output. This affects the following > methods `LSTM`, `GRU`, `DilatedRNN`, and `TCN`. This feature is work > in progress. ```python theme={null} # %%capture horizon = 12 # Try different hyperparmeters to improve accuracy. models = [MLP(h=horizon, # Forecast horizon input_size=2 * horizon, # Length of input sequence loss=DistributionLoss('Bernoulli'), # Binary classification loss valid_loss=Accuracy(), # Accuracy validation signal max_steps=500, # Number of steps to train scaler_type='standard', # Type of scaler to normalize data hidden_size=64, # Defines the size of the hidden state of the LSTM #early_stop_patience_steps=2, # Early stopping regularization patience val_check_steps=10, # Frequency of validation signal (affects early stopping) ), NHITS(h=horizon, # Forecast horizon input_size=2 * horizon, # Length of input sequence loss=DistributionLoss('Bernoulli'), # Binary classification loss valid_loss=Accuracy(), # Accuracy validation signal max_steps=500, # Number of steps to train n_freq_downsample=[2, 1, 1], # Downsampling factors for each stack output #early_stop_patience_steps=2, # Early stopping regularization patience val_check_steps=10, # Frequency of validation signal (affects early stopping) interpolation_mode="nearest", ) ] nf = NeuralForecast(models=models, freq=1) Y_hat_df = nf.cross_validation(df=Y_df, n_windows=1) ``` ```python theme={null} # By default NeuralForecast produces forecast intervals # In this case the lo-x and high-x levels represent the # low and high bounds of the prediction accumulating x% probability Y_hat_df ``` | | unique\_id | ds | cutoff | MLP | MLP-median | MLP-lo-90 | MLP-lo-80 | MLP-hi-80 | MLP-hi-90 | NHITS | NHITS-median | NHITS-lo-90 | NHITS-lo-80 | NHITS-hi-80 | NHITS-hi-90 | y | | ---- | ---------- | ---- | ------ | ----- | ---------- | --------- | --------- | --------- | --------- | ----- | ------------ | ----------- | ----------- | ----------- | ----------- | --- | | 0 | 0 | 1962 | 1961 | 0.173 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0.761 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0 | | 1 | 0 | 1963 | 1961 | 0.784 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0.571 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 1 | | 2 | 0 | 1964 | 1961 | 0.042 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.009 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0 | | 3 | 0 | 1965 | 1961 | 0.072 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.054 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0 | | 4 | 0 | 1966 | 1961 | 0.059 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.000 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 1195 | 99 | 1969 | 1961 | 0.551 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0.697 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 1 | | 1196 | 99 | 1970 | 1961 | 0.662 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0.465 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 1 | | 1197 | 99 | 1971 | 1961 | 0.369 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0.382 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0 | | 1198 | 99 | 1972 | 1961 | 0.056 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.000 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0 | | 1199 | 99 | 1973 | 1961 | 0.000 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.000 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0 | ```python theme={null} # Define classification threshold for final predictions # If (prob > threshold) -> 1 Y_hat_df['NHITS'] = (Y_hat_df['NHITS'] > 0.5) * 1 Y_hat_df['MLP'] = (Y_hat_df['MLP'] > 0.5) * 1 Y_hat_df ``` | | unique\_id | ds | cutoff | MLP | MLP-median | MLP-lo-90 | MLP-lo-80 | MLP-hi-80 | MLP-hi-90 | NHITS | NHITS-median | NHITS-lo-90 | NHITS-lo-80 | NHITS-hi-80 | NHITS-hi-90 | y | | ---- | ---------- | ---- | ------ | --- | ---------- | --------- | --------- | --------- | --------- | ----- | ------------ | ----------- | ----------- | ----------- | ----------- | --- | | 0 | 0 | 1962 | 1961 | 0 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 1 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0 | | 1 | 0 | 1963 | 1961 | 1 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 1 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 1 | | 2 | 0 | 1964 | 1961 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0 | | 3 | 0 | 1965 | 1961 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0 | | 4 | 0 | 1966 | 1961 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 1195 | 99 | 1969 | 1961 | 1 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 1 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 1 | | 1196 | 99 | 1970 | 1961 | 1 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 1 | | 1197 | 99 | 1971 | 1961 | 0 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0 | | 1198 | 99 | 1972 | 1961 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0 | | 1199 | 99 | 1973 | 1961 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0 | ## 4. Plot and Evaluate Predictions Finally, we plot the forecasts of both models against the real values. And evaluate the accuracy of the `MLP` and `NHITS` temporal classifiers. ```python theme={null} plot_df = Y_hat_df[Y_hat_df.unique_id==10] fig, ax = plt.subplots(1, 1, figsize = (20, 7)) plt.plot(plot_df.ds, plot_df.y, label='target signal') plt.plot(plot_df.ds, plot_df['MLP'] * 1.1, label='MLP prediction') plt.plot(plot_df.ds, plot_df['NHITS'] * .9, label='NHITS prediction') ax.set_title('Binary Sequence Forecast', fontsize=22) ax.set_ylabel('Pixel Threshold and Prediction', fontsize=20) ax.set_xlabel('Timestamp [t]', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ```python theme={null} def accuracy(y, y_hat): return np.mean(y==y_hat) mlp_acc = accuracy(y=Y_hat_df['y'], y_hat=Y_hat_df['MLP']) nhits_acc = accuracy(y=Y_hat_df['y'], y_hat=Y_hat_df['NHITS']) print(f'MLP Accuracy: {mlp_acc:.1%}') print(f'NHITS Accuracy: {nhits_acc:.1%}') ``` ```text theme={null} MLP Accuracy: 77.8% NHITS Accuracy: 74.5% ``` ## References * [Cox D. R. (1958). “The Regression Analysis of Binary Sequences.” Journal of the Royal Statistical Society B, 20(2), 215–242.](https://arxiv.org/abs/2201.12886) * [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2023). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Transfer Learning | NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/transfer_learning.html > Explore transfer learning for time series forecasting Transfer learning refers to the process of pre-training a flexible model on a large dataset and using it later on other data with little to no training. It is one of the most outstanding 🚀 achievements in Machine Learning 🧠 and has many practical applications. For time series forecasting, the technique allows you to get lightning-fast predictions ⚡ bypassing the tradeoff between accuracy and speed (more than 30 times faster than our already fast [autoARIMA](https://github.com/Nixtla/statsforecast) for a similar accuracy). This notebook shows how to generate a pre-trained model and store it in a checkpoint to make it available to forecast new time series never seen by the model. Table of Contents
1. Installing NeuralForecast/DatasetsForecast
2\. Load M4 Data
3. Instantiate NeuralForecast core, Fit, and save
4. Load pre-trained model and predict on AirPassengers
5. Evaluate Results
You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing Libraries ```python theme={null} %%capture !pip install datasetsforecast neuralforecast ``` ```python theme={null} import logging import numpy as np import pandas as pd import torch from datasetsforecast.m4 import M4 from neuralforecast.core import NeuralForecast from neuralforecast.models import NHITS from neuralforecast.utils import AirPassengersDF from utilsforecast.losses import mae from utilsforecast.plotting import plot_series ``` ```python theme={null} logging.getLogger("pytorch_lightning").setLevel(logging.WARNING) ``` This example will automatically run on GPUs if available. **Make sure** cuda is available. (If you need help to put this into production send us an email or join our community, we also offer a fully hosted solution) ```python theme={null} torch.cuda.is_available() ``` ```text theme={null} True ``` ## 2. Load M4 Data The `M4` class will automatically download the complete M4 dataset and process it. It return three Dataframes: `Y_df` contains the values for the target variables, `X_df` contains exogenous calendar features and `S_df` contains static features for each time-series (none for M4). For this example we will only use `Y_df`. If you want to use your own data just replace `Y_df`. Be sure to use a long format and have a similar structure to our data set. ```python theme={null} Y_df, _, _ = M4.load(directory='./', group='Monthly', cache=True) Y_df['ds'] = pd.to_datetime(Y_df['ds']) Y_df ``` | | unique\_id | ds | y | | -------- | ---------- | ----------------------------- | ------ | | 0 | M1 | 1970-01-01 00:00:00.000000001 | 8000.0 | | 1 | M1 | 1970-01-01 00:00:00.000000002 | 8350.0 | | 2 | M1 | 1970-01-01 00:00:00.000000003 | 8570.0 | | 3 | M1 | 1970-01-01 00:00:00.000000004 | 7700.0 | | 4 | M1 | 1970-01-01 00:00:00.000000005 | 7080.0 | | ... | ... | ... | ... | | 11246406 | M9999 | 1970-01-01 00:00:00.000000083 | 4200.0 | | 11246407 | M9999 | 1970-01-01 00:00:00.000000084 | 4300.0 | | 11246408 | M9999 | 1970-01-01 00:00:00.000000085 | 3800.0 | | 11246409 | M9999 | 1970-01-01 00:00:00.000000086 | 4400.0 | | 11246410 | M9999 | 1970-01-01 00:00:00.000000087 | 4300.0 | ## 3. Model Train and Save Using the `NeuralForecast.fit` method you can train a set of models to your dataset. You just have to define the `input_size` and `horizon` of your model. The `input_size` is the number of historic observations (lags) that the model will use to learn to predict `h` steps in the future. Also, you can modify the hyperparameters of the model to get a better accuracy. ```python theme={null} horizon = 12 stacks = 3 models = [NHITS(input_size=5 * horizon, h=horizon, max_steps=100, stack_types = stacks*['identity'], n_blocks = stacks*[1], mlp_units = [[256,256] for _ in range(stacks)], n_pool_kernel_size = stacks*[1], batch_size = 32, scaler_type='standard', n_freq_downsample=[12,4,1], enable_progress_bar=False, interpolation_mode="nearest", )] nf = NeuralForecast(models=models, freq='ME') nf.fit(df=Y_df) ``` ```text theme={null} INFO:lightning_fabric.utilities.seed:Seed set to 1 ``` Save model with `core.NeuralForecast.save` method. This method uses PytorchLightning `save_checkpoint` function. We set `save_dataset=False` to only save the model. ```python theme={null} nf.save(path='./results/transfer/', model_index=None, overwrite=True, save_dataset=False) ``` ## 4. Transfer M4 to AirPassengers We load the stored model with the `core.NeuralForecast.load` method, and forecast `AirPassenger` with the `core.NeuralForecast.predict` function. ```python theme={null} fcst2 = NeuralForecast.load(path='./results/transfer/') ``` ```text theme={null} c:\Nixtla\Repositories\neuralforecast\neuralforecast\common\_base_model.py:133: UserWarning: NHITS is a univariate model. Parameter n_series is ignored. warnings.warn( INFO:lightning_fabric.utilities.seed:Seed set to 1 ``` ```python theme={null} # We define the train df. Y_df = AirPassengersDF.copy() mean = Y_df[Y_df.ds<='1959-12-31']['y'].mean() std = Y_df[Y_df.ds<='1959-12-31']['y'].std() Y_train_df = Y_df[Y_df.ds<='1959-12-31'] # 132 train Y_test_df = Y_df[Y_df.ds>'1959-12-31'] # 12 test ``` ```python theme={null} Y_hat_df = fcst2.predict(df=Y_train_df) Y_hat_df.head() ``` | | unique\_id | ds | NHITS | | - | ---------- | ---------- | ---------- | | 0 | 1.0 | 1960-01-31 | 422.038757 | | 1 | 1.0 | 1960-02-29 | 424.678040 | | 2 | 1.0 | 1960-03-31 | 439.538879 | | 3 | 1.0 | 1960-04-30 | 447.967072 | | 4 | 1.0 | 1960-05-31 | 470.603333 | ```python theme={null} plot_series(Y_train_df, Y_hat_df) ``` ## 5. Evaluate Results We evaluate the forecasts of the pre-trained model with the Mean Absolute Error (`mae`). $$ \qquad MAE = \frac{1}{Horizon} \sum_{\tau} |y_{\tau} - \hat{y}_{\tau}|\qquad $$ ```python theme={null} fcst_mae = mae(Y_test_df.merge(Y_hat_df), models=['NHITS'])['NHITS'].item() print(f'NHITS MAE: {fcst_mae:.3f}') print('ETS MAE: 16.222') print('AutoARIMA MAE: 18.551') ``` ```text theme={null} NHITS MAE: 17.245 ETS MAE: 16.222 AutoARIMA MAE: 18.551 ``` # Probabilistic Forecasting | NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/uncertainty_quantification.html > Quantify uncertainty Probabilistic forecasting is a natural answer to quantify the uncertainty of target variable’s future. The task requires to model the following conditional predictive distribution: $\mathbb{P}(\mathbf{y}_{t+1:t+H} \;|\; \mathbf{y}_{:t})$ We will show you how to tackle the task with `NeuralForecast` by combining a classic Long Short Term Memory Network [(LSTM)](https://arxiv.org/abs/2201.12886) and the Neural Hierarchical Interpolation [(NHITS)](https://arxiv.org/abs/2201.12886) with the multi quantile loss function (MQLoss). $\mathrm{MQLoss}(y_{\tau}, [\hat{y}^{(q1)}_{\tau},\hat{y}^{(q2)}_{\tau},\dots,\hat{y}^{(Q)}_{\tau}]) = \frac{1}{H} \sum_{q} \mathrm{QL}(y_{\tau}, \hat{y}^{(q)}_{\tau})$ In this notebook we will:
1. Install NeuralForecast Library
2. Explore the M4-Hourly data.
3. Train the LSTM and NHITS
4. Visualize the LSTM/NHITS prediction intervals. You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing NeuralForecast ```python theme={null} %%capture !pip install neuralforecast ``` #### Useful functions The `plot_grid` auxiliary function defined below will be useful to plot different time series, and different models’ forecasts. ```python theme={null} import logging import warnings import torch from utilsforecast.plotting import plot_series ``` ```python theme={null} warnings.filterwarnings("ignore") ``` ## 2. Loading M4 Data For testing purposes, we will use the Hourly dataset from the [M4 competition](https://www.researchgate.net/publication/325901666_The_M4_Competition_Results_findings_conclusion_and_way_forward). ```python theme={null} import pandas as pd ``` ```python theme={null} Y_train_df = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly.csv') Y_test_df = pd.read_csv( 'https://auto-arima-results.s3.amazonaws.com/M4-Hourly-test.csv' ).rename(columns={'y': 'y_test'}) ``` In this example we will use a subset of the data to avoid waiting too long. You can modify the number of series if you want. ```python theme={null} n_series = 8 uids = Y_train_df['unique_id'].unique()[:n_series] Y_train_df = Y_train_df.query('unique_id in @uids') Y_test_df = Y_test_df.query('unique_id in @uids') ``` ```python theme={null} plot_series(Y_train_df, Y_test_df) ``` ## 3. Model Training The `core.NeuralForecast` provides a high-level interface with our collection of PyTorch models. `NeuralForecast` is instantiated with a list of `models=[LSTM(...), NHITS(...)]`, configured for the forecasting task. * The `horizon` parameter controls the number of steps ahead of the predictions, in this example 48 hours ahead (2 days). * The `MQLoss` with `levels=[80,90]` specializes the network’s output into the 80% and 90% prediction intervals. * The `max_steps=2000`, controls the duration of the network’s training. For more network’s instantiation details check their [documentation](https://nixtlaverse.nixtla.io/neuralforecast/models.dilated_rnn.html). ```python theme={null} from neuralforecast import NeuralForecast from neuralforecast.losses.pytorch import MQLoss from neuralforecast.models import LSTM, NHITS ``` ```python theme={null} logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) torch.set_float32_matmul_precision('high') ``` ```python theme={null} horizon = 48 levels = [80, 90] models = [LSTM(input_size=3*horizon, h=horizon, loss=MQLoss(level=levels), max_steps=1000), NHITS(input_size=7*horizon, h=horizon, n_freq_downsample=[24, 12, 1], loss=MQLoss(level=levels), max_steps=2000),] nf = NeuralForecast(models=models, freq=1) ``` ```text theme={null} Seed set to 1 Seed set to 1 ``` All the models of the library are global, meaning that all time series in `Y_train_df` is used during a shared optimization to train a single model with shared parameters. This is the most common practice in the forecasting literature for deep learning models, and it is known as “cross-learning”. ```python theme={null} %%capture nf.fit(df=Y_train_df) ``` ```python theme={null} Y_hat_df = nf.predict() Y_hat_df.head() ``` ```text theme={null} Predicting: | | 0/? [00:00 ### NHITS ```python theme={null} plot_series(Y_train_df, Y_test_df, level=levels, models=['NHITS']) ``` ## References * [Roger Koenker and Gilbert Basset (1978). Regression Quantiles, Econometrica.](https://www.jstor.org/stable/1913643)
* [Jeffrey L. Elman (1990). “Finding Structure in Time”.](https://onlinelibrary.wiley.com/doi/abs/10.1207/s15516709cog1402_1)
* [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). NHITS: Neural Hierarchical Interpolation for Time Series Forecasting. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886)
# Using MLflow Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/using_mlflow.html > Log your neuralforecast experiments to MLflow ## Installing dependencies To install Neuralforecast refer to [Installation](../getting-started/installation.html). To install mlflow: `pip install mlflow` ## Imports ```python theme={null} import logging import warnings import matplotlib.pyplot as plt import mlflow import mlflow.data import numpy as np import pandas as pd from mlflow.client import MlflowClient from mlflow.data.pandas_dataset import PandasDataset from utilsforecast.plotting import plot_series from neuralforecast.core import NeuralForecast from neuralforecast.models import NBEATSx from neuralforecast.utils import AirPassengersDF from neuralforecast.losses.pytorch import MAE ``` ```python theme={null} logging.getLogger("mlflow").setLevel(logging.ERROR) logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) warnings.filterwarnings("ignore") ``` ## Splitting the data ```python theme={null} # Split data and declare panel dataset Y_df = AirPassengersDF Y_train_df = Y_df[Y_df.ds<='1959-12-31'] # 132 train Y_test_df = Y_df[Y_df.ds>'1959-12-31'] # 12 test Y_df.tail() ``` | | unique\_id | ds | y | | --- | ---------- | ---------- | ----- | | 139 | 1.0 | 1960-08-31 | 606.0 | | 140 | 1.0 | 1960-09-30 | 508.0 | | 141 | 1.0 | 1960-10-31 | 461.0 | | 142 | 1.0 | 1960-11-30 | 390.0 | | 143 | 1.0 | 1960-12-31 | 432.0 | ## MLflow UI Run the following command from the terminal to start the UI: `mlflow ui`. You can then go to the printed URL to visualize the experiments. ## Model training ```python theme={null} mlflow.pytorch.autolog(checkpoint=False) with mlflow.start_run() as run: # Log the dataset to the MLflow Run. Specify the "training" context to indicate that the # dataset is used for model training dataset: PandasDataset = mlflow.data.from_pandas(Y_df, source="AirPassengersDF") mlflow.log_input(dataset, context="training") # Define and log parameters horizon = len(Y_test_df) model_params = dict( input_size=1 * horizon, h=horizon, max_steps=300, loss=MAE(), valid_loss=MAE(), activation='ReLU', scaler_type='robust', random_seed=42, enable_progress_bar=False, ) mlflow.log_params(model_params) # Fit NBEATSx model models = [NBEATSx(**model_params)] nf = NeuralForecast(models=models, freq='M') train = nf.fit(df=Y_train_df, val_size=horizon) # Save conda environment used to run the model (if you used a conda environment) mlflow.pytorch.get_default_conda_env() # Save pip requirements mlflow.pytorch.get_default_pip_requirements() mlflow.pytorch.autolog(disable=True) # Save the neural forecast model nf.save(path='./checkpoints/test_run_1/', model_index=None, overwrite=True, save_dataset=True) ``` ```text theme={null} Seed set to 42 ``` ## Forecasting the future ```python theme={null} Y_hat_df = nf.predict(futr_df=Y_test_df) plot_series(Y_train_df, Y_hat_df, palette='tab20b') ``` # Weighting Timesteps | NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/tutorials/weighting_timesteps.html > Assign relative importance weights to individual timesteps when > training a model. ## Motivation When working with time series data, it is possible that we want to assign a higher or lower importance to certain values or periods in the series. For example, historical sales data cover the abnormal COVID period, so the model should not learn too much from that historical sequence. Alternativaly, you might be interested in the model being very good at modeling periods when a promotion is running. Thus, we need to a way to tell the model when to assign more or less importance to specific timesteps. ## Understanding `sample_weight` The `sample_weight` is a reserved column name, similar to how we expect the data to have columns `["unique_id", "ds", "y"]`. In that column, we can assign a positive integer to indicate how important a timestep is. * Assigning a value of 0 means the particular timestep does not contribute to the loss. * Higher values increase the contribution to the loss, so the model learns “more” about these timesteps. ### Key considerations Deep learning models are trained with windows of data. Internally, we take the mean of the `sample_weight` for a window to get its relative importance. Therefore, training windows are never completely ignored, unless the entire window has timesteps with `sample_weight` of 0. In most cases, windows with timesteps assigned to a `sample_weight` of 0 will have a lower “mean importance”, and so will contribute less to the loss of the model. Take the following example: | ds | y | sample\_weight | | --- | - | -------------- | | t1 | … | 1 | | t2 | … | 1 | | t3 | … | 1 | | t4 | … | 1 | | t5 | … | 0 | | t6 | … | 0 | | t7 | … | 0 | | t8 | … | 0 | | t9 | … | 1 | | t10 | … | 1 | | t11 | … | 1 | | t12 | … | 1 | With `input_size=4` and `h=4`, NeuralForecast creates sliding windows of 8 timesteps. The `sample_weight` for each window is the\ mean over its **forecast horizon** (the future portion): | Window | Input (t) | Future (t) | Mean `sample_weight` | | ------ | --------- | ---------- | -------------------------- | | 1 | t1 – t4 | t5 – t8 | **0.00** — ignored | | 2 | t2 – t5 | t6 – t9 | 0.25 — low importance | | 3 | t3 – t6 | t7 – t10 | 0.50 — moderate importance | | 4 | t4 – t7 | t8 – t11 | 0.75 — high importance | | 5 | t5 – t8 | t9 – t12 | **1.00** — full importance | Window 1 is completely excluded from training: its entire forecast horizon falls within the zeroed period. Windows 2–4 contribute progressively more as the horizon moves out of it. Window 5 trains normally. The model still “sees” timesteps in the input context of windows 2–5. It learns what happened during that period, without being penalized for predicting its future. ### Important notes * `sample_weight` must be greater than or equal to 0 * there is no upper bound for `sample_weight`. It works as a relative importance. So a value of 2 vs 1 means “twice as important”. 100 vs 50 would be interpreted the same way. ## Usage Let’s see an example of how `sample_weight` can be used in practice. We use the Air Passengers dataset and cover different scenarios. ### Setup ```python theme={null} import logging import warnings import numpy as np from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae from utilsforecast.plotting import plot_series from neuralforecast import NeuralForecast from neuralforecast.models import NHITS warnings.filterwarnings("ignore") logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) ``` ### Load data ```python theme={null} from neuralforecast.utils import AirPassengersDF Y_df = AirPassengersDF.copy() Y_train_df = Y_df[Y_df.ds <= "1959-12-31"] # 132 months train Y_test_df = Y_df[Y_df.ds > "1959-12-31"] # 12 months test Y_train_df.tail() ``` | | unique\_id | ds | y | | --- | ---------- | ---------- | ----- | | 127 | 1.0 | 1959-08-31 | 559.0 | | 128 | 1.0 | 1959-09-30 | 463.0 | | 129 | 1.0 | 1959-10-31 | 407.0 | | 130 | 1.0 | 1959-11-30 | 362.0 | | 131 | 1.0 | 1959-12-31 | 405.0 | ```python theme={null} plot_series(Y_train_df) ``` ### Prolonged anomaly Here, we inject a prolonged anomaly where values are 50% lower than they actually are. For that anomalous period, we set `sample_weight` to 0, and 1 otherwise. We then compare how the model performs when setting `sample_weight` against using the default behavior. ```python theme={null} s1 = Y_train_df.copy() anomaly_mask = s1["ds"].between("1953-01-31", "1953-12-31") s1.loc[anomaly_mask, "y"] *= 0.5 s1["sample_weight"] = 1.0 s1.loc[anomaly_mask, "sample_weight"] = 0.0 s1.head() ``` | | unique\_id | ds | y | sample\_weight | | - | ---------- | ---------- | ----- | -------------- | | 0 | 1.0 | 1949-01-31 | 112.0 | 1.0 | | 1 | 1.0 | 1949-02-28 | 118.0 | 1.0 | | 2 | 1.0 | 1949-03-31 | 132.0 | 1.0 | | 3 | 1.0 | 1949-04-30 | 129.0 | 1.0 | | 4 | 1.0 | 1949-05-31 | 121.0 | 1.0 | ```python theme={null} plot_series(s1) ``` #### Training and evaluating ```python theme={null} H = 12 MAX_STEPS = 100 models = [ NHITS( h=H, input_size=3*H, max_steps=MAX_STEPS, scaler_type="robust", enable_progress_bar=False, enable_model_summary=False ) ] nf = NeuralForecast(models=models, freq="ME") # With `sample_weight` nf.fit(df=s1) preds_sw = nf.predict() preds_sw = preds_sw.rename(columns={"NHITS": "NHITS_SW"}) # Without `sample_weight` nf.fit(df=s1.drop(columns=["sample_weight"])) preds = nf.predict() eval_df = Y_test_df.merge(preds_sw, "left", ["unique_id", "ds"]) eval_df = eval_df.merge(preds, "left", ["unique_id", "ds"]) evaluation = evaluate(eval_df, metrics=[mae]) evaluation ``` ```text theme={null} Seed set to 1 ``` | | unique\_id | metric | NHITS\_SW | NHITS | | - | ---------- | ------ | --------- | --------- | | 0 | 1.0 | mae | 18.040064 | 45.309769 | ```python theme={null} plot_series(s1, eval_df, max_insample_length=5*12) ``` From the figure above and from the calculated MAE, we can see that using `sample_weight` improved the performance of the model as we assigned less importance to the anomalous period. ### Isolated anomalies Now, let’s consider a scenario where isolated anomalies occur in the data. As before, we assign a `sample_weight` of 0 to those anomalies and 1 otherwise, and compare the performance. ```python theme={null} rng = np.random.default_rng(42) s2 = Y_train_df.copy() outlier_idx = rng.choice(s2.index, size=4, replace=False) s2.loc[outlier_idx, "y"] *= rng.uniform(2.0, 3.0, size=4) # random spikes s2["sample_weight"] = 1.0 s2.loc[outlier_idx, "sample_weight"] = 0.0 ``` ```python theme={null} s2.head() ``` | | unique\_id | ds | y | sample\_weight | | - | ---------- | ---------- | ----- | -------------- | | 0 | 1.0 | 1949-01-31 | 112.0 | 1.0 | | 1 | 1.0 | 1949-02-28 | 118.0 | 1.0 | | 2 | 1.0 | 1949-03-31 | 132.0 | 1.0 | | 3 | 1.0 | 1949-04-30 | 129.0 | 1.0 | | 4 | 1.0 | 1949-05-31 | 121.0 | 1.0 | ```python theme={null} plot_series(s2) ``` #### Training and evaluating ```python theme={null} # With `sample_weight` nf.fit(df=s2) preds_sw = nf.predict() preds_sw = preds_sw.rename(columns={"NHITS": "NHITS_SW"}) # Without `sample_weight` nf.fit(df=s2.drop(columns=["sample_weight"])) preds = nf.predict() eval_df = Y_test_df.merge(preds_sw, "left", ["unique_id", "ds"]) eval_df = eval_df.merge(preds, "left", ["unique_id", "ds"]) evaluation = evaluate(eval_df, metrics=[mae]) evaluation ``` | | unique\_id | metric | NHITS\_SW | NHITS | | - | ---------- | ------ | --------- | --------- | | 0 | 1.0 | mae | 62.247646 | 61.333698 | ```python theme={null} plot_series(s2, eval_df, max_insample_length=5*12) ``` In this case, using the `sample_weight` is not sufficient. In fact, the model performs slightly worse than not using `sample_weight`. Here, it might be beneficial to use other methods robust to outliers, like selecting the `HuberLoss` as the optimization objective. ### Emphasize certain periods Now, let’s consider the scenario where we want to give more importance to the summer months. Those are the months with the highest traffic, so we might want our model to be espcially good in those periods. ```python theme={null} s3 = Y_train_df.copy() s3["sample_weight"] = 1.0 summer_mask = s3["ds"].dt.month.isin([6, 7, 8]) s3.loc[summer_mask, "sample_weight"] = 3.0 ``` ```python theme={null} s3.tail() ``` | | unique\_id | ds | y | sample\_weight | | --- | ---------- | ---------- | ----- | -------------- | | 127 | 1.0 | 1959-08-31 | 559.0 | 3.0 | | 128 | 1.0 | 1959-09-30 | 463.0 | 1.0 | | 129 | 1.0 | 1959-10-31 | 407.0 | 1.0 | | 130 | 1.0 | 1959-11-30 | 362.0 | 1.0 | | 131 | 1.0 | 1959-12-31 | 405.0 | 1.0 | ```python theme={null} plot_series(s3) ``` #### Training and evaluating ```python theme={null} # With `sample_weight` nf.fit(df=s3) preds_sw = nf.predict() preds_sw = preds_sw.rename(columns={"NHITS": "NHITS_SW"}) # Without `sample_weight` nf.fit(df=s3.drop(columns=["sample_weight"])) preds = nf.predict() eval_df = Y_test_df.merge(preds_sw, "left", ["unique_id", "ds"]) eval_df = eval_df.merge(preds, "left", ["unique_id", "ds"]) evaluation = evaluate(eval_df, metrics=[mae]) evaluation ``` | | unique\_id | metric | NHITS\_SW | NHITS | | - | ---------- | ------ | --------- | --------- | | 0 | 1.0 | mae | 11.672673 | 13.421109 | ```python theme={null} plot_series(s3, eval_df, max_insample_length=5*12) ``` Here, we see that using `sample_weight` improved the performance again. Although it’s hard to see in the plot, the model trained with `sample_weight` better forecasts the peaks of summer, resulting in a performance gain. ## Summary NeuralForecast now supports the `sample_weight` column which is a reserved column name to indicate the relative importance of each timestep. During training, the `sample_weight` of each window is the mean over the forecast horizon. This helps the model either ignore anomalous sequences or data points, or focus more on important periods. # Detect Demand Peaks | NeuralForecast Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/use-cases/electricity_peak_forecasting.html > In this example we will show how to perform electricity load > forecasting on the ERCOT (Texas) market for detecting daily peaks. ## Introduction Predicting peaks in different markets is useful. In the electricity market, consuming electricity at peak demand is penalized with higher tarifs. When an individual or company consumes electricity when its most demanded, regulators calls that a coincident peak (CP). In the Texas electricity market (ERCOT), the peak is the monthly 15-minute interval when the ERCOT Grid is at a point of highest capacity. The peak is caused by all consumers’ combined demand on the electrical grid. The coincident peak demand is an important factor used by ERCOT to determine final electricity consumption bills. ERCOT registers the CP demand of each client for 4 months, between June and September, and uses this to adjust electricity prices. Clients can therefore save on electricity bills by reducing the coincident peak demand. In this example we will train an `NHITS` model on historic load data to forecast day-ahead peaks on September 2022. Multiple seasonality is traditionally present in low sampled electricity data. Demand exhibits daily and weekly seasonality, with clear patterns for specific hours of the day such as 6:00pm vs 3:00am or for specific days such as Sunday vs Friday. First, we will load ERCOT historic demand, then we will use the `Neuralforecast.cross_validation` method to fit the model and forecast daily load during September. Finally, we show how to use the forecasts to detect the coincident peak. **Outline** 1. Install libraries 2. Load and explore the data 3. Fit NHITS model and forecast 4. Peak detection > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Libraries We assume you have NeuralForecast already installed. Check this guide for instructions on [how to install NeuralForecast](../getting-started/installation.html). Install the necessary packages using `pip install neuralforecast` ## Load Data The input to NeuralForecast models is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestamp ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. We will rename the First, download and read the 2022 historic total demand of the ERCOT market, available [here](https://www.ercot.com/gridinfo/load/load_hist). The data processing includes adding the missing hour due to daylight saving time, parsing the date to datetime format, and filtering columns of interest. ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} # Load data Y_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/ERCOT-clean.csv', parse_dates=['ds']) Y_df = Y_df.query("ds >= '2022-01-01' & ds <= '2022-10-01'") ``` ```python theme={null} Y_df.plot(x='ds', y='y', figsize=(20, 7)) ``` ## Fit and Forecast with NHITS Import the `NeuralForecast` class and the models you need. ```python theme={null} from neuralforecast.core import NeuralForecast from neuralforecast.auto import AutoNHITS ``` First, instantiate the model and define the parameters. To instantiate `AutoNHITS` you need to define: * `h`: forecasting horizon * `loss`: training loss. Use the `DistributionLoss` to produce probabilistic forecasts. Default: `MAE`. * `config`: hyperparameter search space. If `None`, the `AutoNHITS` class will use a pre-defined suggested hyperparameter space. * `num_samples`: number of configurations explored. ```python theme={null} models = [AutoNHITS(h=24, config=None, # Uses default config num_samples=10 ) ] ``` We fit the model by instantiating a `NeuralForecast` object with the following required parameters: * `models`: a list of models. Select the models you want from [models](../capabilities/overview.html) and import them. * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) ```python theme={null} # Instantiate StatsForecast class as sf nf = NeuralForecast( models=models, freq='h', ) ``` The `cross_validation` method allows the user to simulate multiple historic forecasts, greatly simplifying pipelines by replacing for loops with `fit` and `predict` methods. This method re-trains the model and forecast each window. See [this tutorial](https://nixtlaverse.nixtla.io/statsforecast/docs/getting-started/getting_started_complete.html) for an animation of how the windows are defined. Use the `cross_validation` method to produce all the daily forecasts for September. To produce daily forecasts set the forecasting horizon `h` as 24\. In this example we are simulating deploying the pipeline during September, so set the number of windows as 30 (one for each day). Finally, set the step size between windows as 24, to only produce one forecast per day. ```python theme={null} %%capture crossvalidation_df = nf.cross_validation( df=Y_df, step_size=24, n_windows=30 ) ``` ```python theme={null} crossvalidation_df.head() ``` | | unique\_id | ds | cutoff | AutoNHITS | y | | - | ---------- | ------------------- | ------------------- | ------------ | ------------ | | 0 | ERCOT | 2022-09-01 00:00:00 | 2022-08-31 23:00:00 | 45841.601562 | 45482.471757 | | 1 | ERCOT | 2022-09-01 01:00:00 | 2022-08-31 23:00:00 | 43613.394531 | 43602.658043 | | 2 | ERCOT | 2022-09-01 02:00:00 | 2022-08-31 23:00:00 | 41968.945312 | 42284.817342 | | 3 | ERCOT | 2022-09-01 03:00:00 | 2022-08-31 23:00:00 | 41038.539062 | 41663.156771 | | 4 | ERCOT | 2022-09-01 04:00:00 | 2022-08-31 23:00:00 | 41237.203125 | 41710.621904 | > **Important** > > When using `cross_validation` make sure the forecasts are produced at > the desired timestamps. Check the `cutoff` column which specifies the > last timestamp before the forecasting window. ## Peak Detection Finally, we use the forecasts in `crossvaldation_df` to detect the daily hourly demand peaks. For each day, we set the detected peaks as the highest forecasts. In this case, we want to predict one peak (`npeaks`); depending on your setting and goals, this parameter might change. For example, the number of peaks can correspond to how many hours a battery can be discharged to reduce demand. ```python theme={null} npeaks = 1 # Number of peaks ``` For the ERCOT 4CP detection task we are interested in correctly predicting the highest monthly load. Next, we filter the day in September with the highest hourly demand and predict the peak. ```python theme={null} crossvalidation_df = crossvalidation_df[['ds','y','AutoNHITS']] max_day = crossvalidation_df.iloc[crossvalidation_df['y'].argmax()].ds.day # Day with maximum load cv_df_day = crossvalidation_df.query('ds.dt.day == @max_day') max_hour = cv_df_day['y'].argmax() peaks = cv_df_day['AutoNHITS'].argsort().iloc[-npeaks:].values # Predicted peaks ``` In the following plot we see how the model is able to correctly detect the coincident peak for September 2022. ```python theme={null} import matplotlib.pyplot as plt ``` ```python theme={null} plt.figure(figsize=(10, 5)) plt.axvline(cv_df_day.iloc[max_hour]['ds'], color='black', label='True Peak') plt.scatter(cv_df_day.iloc[peaks]['ds'], cv_df_day.iloc[peaks]['AutoNHITS'], color='green', label=f'Predicted Top-{npeaks}') plt.plot(cv_df_day['ds'], cv_df_day['y'], label='y', color='blue') plt.plot(cv_df_day['ds'], cv_df_day['AutoNHITS'], label='Forecast', color='red') plt.xlabel('Time') plt.ylabel('Load (MW)') plt.grid() plt.legend() ``` > **Important** > > In this example we only include September. However, `NHITS` can > correctly predict the peaks for the 4 months of 2022. You can try this > by increasing the `nwindows` parameter of `cross_validation` or > filtering the `Y_df` dataset. The complete run for all months take > only 10 minutes. ## References * [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). “NHITS: Neural Hierarchical Interpolation for Time Series Forecasting”. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Predictive Maintenance Source: https://nixtlaverse.nixtla.io/neuralforecast/docs/use-cases/predictive_maintenance.html Predictive maintenance (PdM) is a data-driven preventive maintanance program. It is a proactive maintenance strategy that uses sensors to monitor the performance and equipment conditions during operation. The PdM methods constantly analyze the data to predict when optimal maintenance schedules. It can reduce maintenance costs and prevent catastrophic equipment failure when used correctly. In this notebook, we will apply NeuralForecast to perform a supervised Remaining Useful Life (RUL) estimation on the classic PHM2008 aircraft degradation dataset. Outline
1. Installing Packages
2. Load PHM2008 aircraft degradation dataset
3. Fit and Predict NeuralForecast
4. Evaluate Predictions You can run these experiments using GPU with Google Colab. Open In Colab ## 1. Installing Packages ```python theme={null} %%capture !pip install neuralforecast datasetsforecast ``` ```python theme={null} import logging import numpy as np import pandas as pd import matplotlib.pyplot as plt from neuralforecast.models import NBEATSx from neuralforecast import NeuralForecast from neuralforecast.losses.pytorch import HuberLoss from datasetsforecast.phm2008 import PHM2008 ``` ```text theme={null} /Users/marcopeix/dev/neuralforecast/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm 2026-04-02 15:43:47,243 INFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output. 2026-04-02 15:43:47,362 INFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output. ``` ```python theme={null} logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) ``` ## 2. Load PHM2008 aircraft degradation dataset Here we will load the Prognosis and Health Management 2008 challenge dataset. This dataset used the Commercial Modular Aero-Propulsion System Simulation to recreate the degradation process of turbofan engines for different aircraft with varying wear and manufacturing starting under normal conditions. The training dataset consists of complete run-to-failure simulations, while the test dataset comprises sequences before failure. ![](https://github.com/Nixtla/neuralforecast/blob/main/nbs/imgs_losses/turbofan_engine.png?raw=1) ```python theme={null} Y_train_df, Y_test_df = PHM2008.load(directory='./data', group='FD001', clip_rul=False) Y_train_df.head() ``` | | unique\_id | ds | s\_2 | s\_3 | s\_4 | s\_7 | s\_8 | s\_9 | s\_11 | s\_12 | s\_13 | s\_14 | s\_15 | s\_17 | s\_20 | s\_21 | y | | - | ---------- | -- | ------ | ------- | ------- | ------ | ------- | ------- | ----- | ------ | ------- | ------- | ------ | ----- | ----- | ------- | --- | | 0 | 1 | 1 | 641.82 | 1589.70 | 1400.60 | 554.36 | 2388.06 | 9046.19 | 47.47 | 521.66 | 2388.02 | 8138.62 | 8.4195 | 392 | 39.06 | 23.4190 | 191 | | 1 | 1 | 2 | 642.15 | 1591.82 | 1403.14 | 553.75 | 2388.04 | 9044.07 | 47.49 | 522.28 | 2388.07 | 8131.49 | 8.4318 | 392 | 39.00 | 23.4236 | 190 | | 2 | 1 | 3 | 642.35 | 1587.99 | 1404.20 | 554.26 | 2388.08 | 9052.94 | 47.27 | 522.42 | 2388.03 | 8133.23 | 8.4178 | 390 | 38.95 | 23.3442 | 189 | | 3 | 1 | 4 | 642.35 | 1582.79 | 1401.87 | 554.45 | 2388.11 | 9049.48 | 47.13 | 522.86 | 2388.08 | 8133.83 | 8.3682 | 392 | 38.88 | 23.3739 | 188 | | 4 | 1 | 5 | 642.37 | 1582.85 | 1406.22 | 554.00 | 2388.06 | 9055.15 | 47.28 | 522.19 | 2388.04 | 8133.80 | 8.4294 | 393 | 38.90 | 23.4044 | 187 | ```python theme={null} plot_df1 = Y_train_df[Y_train_df['unique_id']==1] plot_df2 = Y_train_df[Y_train_df['unique_id']==2] plot_df3 = Y_train_df[Y_train_df['unique_id']==3] plt.plot(plot_df1.ds, np.minimum(plot_df1.y, 125), color='#2D6B8F', linestyle='--') plt.plot(plot_df1.ds, plot_df1.y, color='#2D6B8F', label='Engine 1') plt.plot(plot_df2.ds, np.minimum(plot_df2.y, 125)+1.5, color='#CA6F6A', linestyle='--') plt.plot(plot_df2.ds, plot_df2.y+1.5, color='#CA6F6A', label='Engine 2') plt.plot(plot_df3.ds, np.minimum(plot_df3.y, 125)-1.5, color='#D5BC67', linestyle='--') plt.plot(plot_df3.ds, plot_df3.y-1.5, color='#D5BC67', label='Engine 3') plt.ylabel('Remaining Useful Life (RUL)', fontsize=15) plt.xlabel('Time Cycle', fontsize=15) plt.legend() plt.grid() ``` ```python theme={null} def smooth(s, b = 0.98): v = np.zeros(len(s)+1) #v_0 is already 0. bc = np.zeros(len(s)+1) for i in range(1, len(v)): #v_t = 0.95 v[i] = (b * v[i-1] + (1-b) * s[i-1]) bc[i] = 1 - b**i sm = v[1:] / bc[1:] return sm unique_id = 1 plot_df = Y_train_df[Y_train_df.unique_id == unique_id].copy() fig, axes = plt.subplots(2,3, figsize = (8,5)) fig.tight_layout() j = -1 #, 's_11', 's_12', 's_13', 's_14', 's_15', 's_17', 's_20', 's_21' for feature in ['s_2', 's_3', 's_4', 's_7', 's_8', 's_9']: if ('s' in feature) and ('smoothed' not in feature): j += 1 axes[j // 3, j % 3].plot(plot_df.ds, plot_df[feature], c = '#2D6B8F', label = 'original') axes[j // 3, j % 3].plot(plot_df.ds, smooth(plot_df[feature].values), c = '#CA6F6A', label = 'smoothed') #axes[j // 3, j % 3].plot([10,10],[0,1], c = 'black') axes[j // 3, j % 3].set_title(feature) axes[j // 3, j % 3].grid() axes[j // 3, j % 3].legend() plt.suptitle(f'Engine {unique_id} sensor records') plt.tight_layout() ``` ## 3. Fit and Predict NeuralForecast NeuralForecast methods are capable of addressing regression problems involving various variables. The regression problem involves predicting the target variable $y_{t+h}$ based on its lags $y_{:t}$, temporal exogenous features $x^{(h)}_{:t}$, exogenous features available at the time of prediction $x^{(f)}_{:t+h}$, and static features $x^{(s)}$. The task of estimating the remaining useful life (RUL) simplifies the problem to a single horizon prediction $h=1$, where the objective is to predict $y_{t+1}$ based on the exogenous features $x^{(f)}_{:t+1}$ and static features $x^{(s)}$. In the RUL estimation task, the exogenous features typically correspond to sensor monitoring information, while the target variable represents the RUL itself. $P(y_{t+1}\;|\;x^{(f)}_{:t+1},x^{(s)})$ ```python theme={null} max_ds = Y_train_df.groupby('unique_id')["ds"].max() Y_test_df = Y_test_df.merge(max_ds, on='unique_id', how='left', suffixes=('', '_train_max_date')) Y_test_df["ds"] = Y_test_df["ds"] + Y_test_df["ds_train_max_date"] Y_test_df = Y_test_df.drop(columns=["ds_train_max_date"]) ``` ```python theme={null} Y_df = pd.concat([Y_train_df, Y_test_df], ignore_index=True) ``` ```python theme={null} %%capture futr_exog_list =['s_2', 's_3', 's_4', 's_7', 's_8', 's_9', 's_11', 's_12', 's_13', 's_14', 's_15', 's_17', 's_20', 's_21'] model = NBEATSx(h=1, input_size=24, loss=HuberLoss(), scaler_type='robust', stack_types=['identity', 'identity', 'identity'], dropout_prob_theta=0.5, futr_exog_list=futr_exog_list, exclude_insample_y=True, max_steps=1000) nf = NeuralForecast(models=[model], freq=1) Y_hat_cv_df = nf.cross_validation(df=Y_df, n_windows=31) ``` ```text theme={null} Seed set to 1 ``` ## 4. Evaluate Predictions In the original PHM2008 dataset the true RUL values for the test set are only provided for the last time cycle of each enginge. We will filter the predictions to only evaluate the last time cycle. $RMSE(\mathbf{y}_{T},\hat{\mathbf{y}}_{T}) = \sqrt{\frac{1}{|\mathcal{D}_{test}|} \sum_{i} (y_{i,T}-\hat{y}_{i,T})^{2}}$ ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import rmse ``` ```python theme={null} Y_hat_last = Y_hat_cv_df.loc[Y_hat_cv_df.groupby('unique_id')['ds'].idxmax()] metrics = evaluate( Y_hat_last.drop(columns=["cutoff"]), metrics=[rmse], agg_fn='mean' ) metrics ``` | | metric | NBEATSx | | - | ------ | -------- | | 0 | rmse | 0.363993 | Alternatively, we can also evaluate over multiple windows to have a more representative metric of the performance. ```python theme={null} metrics = evaluate( Y_hat_cv_df.drop(columns=["cutoff"]), metrics=[rmse], agg_fn='mean' ) metrics ``` | | metric | NBEATSx | | - | ------ | -------- | | 0 | rmse | 1.323458 | Finally, we can plot the true value and predicted value of the model across many cross-validation windows. ```python theme={null} model_name = "NBEATSx" plot_df1 = Y_hat_cv_df[Y_hat_cv_df['unique_id']==2] plot_df2 = Y_hat_cv_df[Y_hat_cv_df['unique_id']==3] plot_df3 = Y_hat_cv_df[Y_hat_cv_df['unique_id']==4] plt.plot(plot_df1.ds, plot_df1['y'], c='#2D6B8F', label='E2 true RUL') plt.plot(plot_df1.ds, plot_df1[model_name]+1, c='#2D6B8F', linestyle='--', label='E2 predicted RUL') plt.plot(plot_df1.ds, plot_df2['y'], c='#CA6F6A', label='E3 true RUL') plt.plot(plot_df1.ds, plot_df2[model_name]+1, c='#CA6F6A', linestyle='--', label='E3 predicted RUL') plt.plot(plot_df1.ds, plot_df3['y'], c='#D5BC67', label='E4 true RUL') plt.plot(plot_df1.ds, plot_df3[model_name]+1, c='#D5BC67', linestyle='--', label='E4 predicted RUL') plt.legend() plt.grid() ``` ## References * [R. Keith Mobley (2002). “An Introduction to Predictive Maintenance”](https://www.irantpm.ir/wp-content/uploads/2008/02/an-introduction-to-predictive-maintenance.pdf)
* [Saxena, A., Goebel, K., Simon, D.,\&Eklund, N. (2008). “Damage propagation modeling for aircraft engine run-to-failure simulation”. International conference on prognostics and health management.](https://ntrs.nasa.gov/api/citations/20090029214/downloads/20090029214.pdf) # NumPy Evaluation Source: https://nixtlaverse.nixtla.io/neuralforecast/losses.numpy.html Comprehensive NumPy evaluation metrics for NeuralForecast including MAE, MSE, MAPE, MASE, and probabilistic losses for time series forecast accuracy. The most important train signal is the forecast error, which is the difference between the observed value $y_{\tau}$ and the prediction $\hat{y}_{\tau}$, at time $y_{\tau}$: $e_{\tau} = y_{\tau}-\hat{y}_{\tau} \qquad \qquad \tau \in \{t+1,\dots,t+H \}$ The train loss summarizes the forecast errors in different evaluation metrics. # 1. Scale-dependent Errors These metrics are on the same scale as the data. ## Mean Absolute Error ### `mae` ```python theme={null} mae(y, y_hat, weights=None, axis=None) ``` Mean Absolute Error Calculates Mean Absolute Error between `y` and `y_hat`. MAE measures the relative prediction accuracy of a forecasting method by calculating the deviation of the prediction and the true value at a given time and averages these devations over the length of the series. ```math theme={null} \mathrm{MAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} |y_{\tau} - \hat{y}_{\tau}| ``` **Parameters:** | Name | Type | Description | Default | | ------- | -------------------------------------- | ---------------------------------------------------------------------- | ---------- | | `y` | [ndarray](#numpy.ndarray) | Actual values. | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Predicted values. | *required* | | `mask` | [ndarray](#numpy.ndarray) | Specifies date stamps per serie to consider in loss. Defaults to None. | *required* | **Returns:** | Name | Type | Description | | ------- | -------------------------------------------------------------------------------- | ----------- | | `float` | [Union](#typing.Union)\[[float](#float), [ndarray](#numpy.ndarray)] | MAE. | ## Mean Squared Error ### `mse` ```python theme={null} mse(y, y_hat, weights=None, axis=None) ``` Mean Squared Error Calculates Mean Squared Error between `y` and `y_hat`. MSE measures the relative prediction accuracy of a forecasting method by calculating the squared deviation of the prediction and the true value at a given time, and averages these devations over the length of the series. ```math theme={null} \mathrm{MSE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} (y_{\tau} - \hat{y}_{\tau})^{2} ``` **Parameters:** | Name | Type | Description | Default | | ------- | -------------------------------------- | ---------------------------------------------------------------------- | ---------- | | `y` | [ndarray](#numpy.ndarray) | Actual values. | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Predicted values. | *required* | | `mask` | [ndarray](#numpy.ndarray) | Specifies date stamps per serie to consider in loss. Defaults to None. | *required* | **Returns:** | Name | Type | Description | | ------- | -------------------------------------------------------------------------------- | ----------- | | `float` | [Union](#typing.Union)\[[float](#float), [ndarray](#numpy.ndarray)] | MSE. | ## Root Mean Squared Error ### `rmse` ```python theme={null} rmse(y, y_hat, weights=None, axis=None) ``` Root Mean Squared Error Calculates Root Mean Squared Error between `y` and `y_hat`. RMSE measures the relative prediction accuracy of a forecasting method by calculating the squared deviation of the prediction and the observed value at a given time and averages these devations over the length of the series. Finally the RMSE will be in the same scale as the original time series so its comparison with other series is possible only if they share a common scale. RMSE has a direct connection to the L2 norm. ```math theme={null} \mathrm{RMSE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \sqrt{\frac{1}{H} \sum^{t+H}_{\tau=t+1} (y_{\tau} - \hat{y}_{\tau})^{2}} ``` **Parameters:** | Name | Type | Description | Default | | ------- | -------------------------------------- | ---------------------------------------------------------------------- | ---------- | | `y` | [ndarray](#numpy.ndarray) | Actual values. | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Predicted values. | *required* | | `mask` | [ndarray](#numpy.ndarray) | Specifies date stamps per serie to consider in loss. Defaults to None. | *required* | **Returns:** | Name | Type | Description | | ------- | -------------------------------------------------------------------------------- | ----------- | | `float` | [Union](#typing.Union)\[[float](#float), [ndarray](#numpy.ndarray)] | RMSE. | # 2. Percentage errors These metrics are unit-free, suitable for comparisons across series. ## Mean Absolute Percentage Error ### `mape` ```python theme={null} mape(y, y_hat, weights=None, axis=None) ``` Mean Absolute Percentage Error Calculates Mean Absolute Percentage Error between `y` and `y_hat`. MAPE measures the relative prediction accuracy of a forecasting method by calculating the percentual deviation of the prediction and the observed value at a given time and averages these devations over the length of the series. The closer to zero an observed value is, the higher penalty MAPE loss assigns to the corresponding error. ```math theme={null} \mathrm{MAPE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{|y_{\tau}|} ``` **Parameters:** | Name | Type | Description | Default | | ------- | -------------------------------------- | ---------------------------------------------------------------------- | ---------- | | `y` | [ndarray](#numpy.ndarray) | Actual values. | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Predicted values. | *required* | | `mask` | [ndarray](#numpy.ndarray) | Specifies date stamps per serie to consider in loss. Defaults to None. | *required* | **Returns:** | Name | Type | Description | | ------- | -------------------------------------------------------------------------------- | ----------- | | `float` | [Union](#typing.Union)\[[float](#float), [ndarray](#numpy.ndarray)] | MAPE. | ## SMAPE ### `smape` ```python theme={null} smape(y, y_hat, weights=None, axis=None) ``` Symmetric Mean Absolute Percentage Error Calculates Symmetric Mean Absolute Percentage Error between `y` and `y_hat`. SMAPE measures the relative prediction accuracy of a forecasting method by calculating the relative deviation of the prediction and the observed value scaled by the sum of the absolute values for the prediction and observed value at a given time, then averages these devations over the length of the series. This allows the SMAPE to have bounds between 0% and 200% which is desirable compared to normal MAPE that may be undetermined when the target is zero. ```math theme={null} \mathrm{sMAPE}_{2}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{|y_{\tau}|+|\hat{y}_{\tau}|} ``` **Parameters:** | Name | Type | Description | Default | | ------- | -------------------------------------- | ---------------------------------------------------------------------- | ---------- | | `y` | [ndarray](#numpy.ndarray) | Actual values. | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Predicted values. | *required* | | `mask` | [ndarray](#numpy.ndarray) | Specifies date stamps per serie to consider in loss. Defaults to None. | *required* | **Returns:** | Name | Type | Description | | ------- | -------------------------------------------------------------------------------- | ----------- | | `float` | [Union](#typing.Union)\[[float](#float), [ndarray](#numpy.ndarray)] | SMAPE. |
References * [Makridakis S., "Accuracy measures: theoretical and practical concerns".](https://www.sciencedirect.com/science/article/pii/0169207093900793)
# 3. Scale-independent Errors These metrics measure the relative improvements versus baselines. ## Mean Absolute Scaled Error ### `mase` ```python theme={null} mase(y, y_hat, y_train, seasonality, weights=None, axis=None) ``` Mean Absolute Scaled Error Calculates the Mean Absolute Scaled Error between `y` and `y_hat`. MASE measures the relative prediction accuracy of a forecasting method by comparinng the mean absolute errors of the prediction and the observed value against the mean absolute errors of the seasonal naive model. The MASE partially composed the Overall Weighted Average (OWA), used in the M4 Competition. ```math theme={null} \mathrm{MASE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{\mathrm{MAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau})} ``` **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------- | | `y` | [ndarray](#numpy.ndarray) | Actual values. | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Predicted values. | *required* | | `y_insample` | [ndarray](#numpy.ndarray) | Actual insample Seasonal Naive predictions. | *required* | | `seasonality` | [int](#int) | Main frequency of the time series; Hourly 24, Daily 7, Weekly 52, Monthly 12, Quarterly 4, Yearly 1. | *required* | | `mask` | [ndarray](#numpy.ndarray) | Specifies date stamps per serie to consider in loss. Defaults to None. | *required* | **Returns:** | Name | Type | Description | | ------- | -------------------------------------------------------------------------------- | ----------- | | `float` | [Union](#typing.Union)\[[float](#float), [ndarray](#numpy.ndarray)] | MASE. |
References * [Rob J. Hyndman, & Koehler, A. B. "Another look at measures of forecast accuracy".](https://www.sciencedirect.com/science/article/pii/S0169207006000239) * [Spyros Makridakis, Evangelos Spiliotis, Vassilios Assimakopoulos, "The M4 Competition: 100,000 time series and 61 forecasting methods".](https://www.sciencedirect.com/science/article/pii/S0169207019301128)
## Relative Mean Absolute Error ### `rmae` ```python theme={null} rmae(y, y_hat1, y_hat2, weights=None, axis=None) ``` RMAE Calculates Relative Mean Absolute Error (RMAE) between two sets of forecasts (from two different forecasting methods). A number smaller than one implies that the forecast in the numerator is better than the forecast in the denominator. ```math theme={null} \mathrm{rMAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}, \mathbf{\hat{y}}^{base}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{\mathrm{MAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{base}_{\tau})} ``` **Parameters:** | Name | Type | Description | Default | | --------- | ------------------------------------------------------- | -------------------------------------------------------- | ----------------- | | `y` | [ndarray](#numpy.ndarray) | observed values. | *required* | | `y_hat1` | [ndarray](#numpy.ndarray) | Predicted values of first model. | *required* | | `y_hat2` | [ndarray](#numpy.ndarray) | Predicted values of baseline model. | *required* | | `weights` | [ndarray](#numpy.ndarray) | Weights for weighted average. Defaults to None. | None | | `axis` | [Optional](#typing.Optional)\[[int](#int)] | Axis or axes along which to average a. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | -------------------------------------------------------------------------------- | ----------- | | `float` | [Union](#typing.Union)\[[float](#float), [ndarray](#numpy.ndarray)] | RMAE. |
References * [Rob J. Hyndman, & Koehler, A. B. "Another look at measures of forecast accuracy".](https://www.sciencedirect.com/science/article/pii/S0169207006000239)
# 4. Probabilistic Errors These measure absolute deviation non-symmetrically, that produce under/over estimation. ## Quantile Loss ### `quantile_loss` ```python theme={null} quantile_loss(y, y_hat, q=0.5, weights=None, axis=None) ``` Quantile Loss Computes the quantile loss between `y` and `y_hat`. QL measures the deviation of a quantile forecast. By weighting the absolute deviation in a non symmetric way, the loss pays more attention to under or over estimation. A common value for q is 0.5 for the deviation from the median (Pinball loss). ```math theme={null} \mathrm{QL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q)}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \Big( (1-q)\,( \hat{y}^{(q)}_{\tau} - y_{\tau} )_{+} + q\,( y_{\tau} - \hat{y}^{(q)}_{\tau} )_{+} \Big) ``` **Parameters:** | Name | Type | Description | Default | | ------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | | `y` | [ndarray](#numpy.ndarray) | Actual values. | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Predicted values. | *required* | | `q` | [float](#float) | The slope of the quantile loss, in the context of quantile regression, the q determines the conditional quantile level. Defaults to 0.5. | 0.5 | | `mask` | [ndarray](#numpy.ndarray) | Specifies date stamps per serie to consider in loss. Defaults to None. | *required* | **Returns:** | Name | Type | Description | | ------- | -------------------------------------------------------------------------------- | -------------- | | `float` | [Union](#typing.Union)\[[float](#float), [ndarray](#numpy.ndarray)] | Quantile loss. |
References * [Roger Koenker and Gilbert Bassett, Jr., "Regression Quantiles".](https://www.jstor.org/stable/1913643)
## Multi-Quantile Loss ### `mqloss` ```python theme={null} mqloss(y, y_hat, quantiles, weights=None, axis=None) ``` Multi-Quantile loss Calculates the Multi-Quantile loss (MQL) between `y` and `y_hat`. MQL calculates the average multi-quantile Loss for a given set of quantiles, based on the absolute difference between predicted quantiles and observed values. ```math theme={null} \mathrm{MQL}(\mathbf{y}_{\tau},[\mathbf{\hat{y}}^{(q_{1})}_{\tau}, ... ,\hat{y}^{(q_{n})}_{\tau}]) = \frac{1}{n} \sum_{q_{i}} \mathrm{QL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q_{i})}_{\tau}) ``` The limit behavior of MQL allows to measure the accuracy of a full predictive distribution $\\mathbf{\\hat{F}}\_{\\tau}$ with the continuous ranked probability score (CRPS). This can be achieved through a numerical integration technique, that discretizes the quantiles and treats the CRPS integral with a left Riemann approximation, averaging over uniformly distanced quantiles. ```math theme={null} \mathrm{CRPS}(y_{\tau}, \mathbf{\hat{F}}_{\tau}) = \int^{1}_{0} \mathrm{QL}(y_{\tau}, \hat{y}^{(q)}_{\tau}) dq ``` **Parameters:** | Name | Type | Description | Default | | ----------- | -------------------------------------- | ---------------------------------------------------------------------- | ---------- | | `y` | [ndarray](#numpy.ndarray) | Actual values. | *required* | | `y_hat` | [ndarray](#numpy.ndarray) | Predicted values. | *required* | | `quantiles` | [ndarray](#numpy.ndarray) | Quantiles to estimate from the distribution of y. | *required* | | `mask` | [ndarray](#numpy.ndarray) | Specifies date stamps per serie to consider in loss. Defaults to None. | *required* | **Returns:** | Name | Type | Description | | ------- | -------------------------------------------------------------------------------- | ----------- | | `float` | [Union](#typing.Union)\[[float](#float), [ndarray](#numpy.ndarray)] | MQLoss. |
References * [Roger Koenker and Gilbert Bassett, Jr., "Regression Quantiles".](https://www.jstor.org/stable/1913643)
[James E. Matheson and Robert L. Winkler, "Scoring Rules for Continuous Probability Distributions".](https://www.jstor.org/stable/2629907) # Examples and Validation ```python theme={null} import unittest import torch as t import numpy as np from neuralforecast.losses.pytorch import ( MAE, MSE, RMSE, # unscaled errors MAPE, SMAPE, # percentage errors MASE, # scaled error QuantileLoss, MQLoss # probabilistic errors ) from neuralforecast.losses.numpy import ( mae, mse, rmse, # unscaled errors mape, smape, # percentage errors mase, # scaled error quantile_loss, mqloss # probabilistic errors ) ``` # PyTorch Losses Source: https://nixtlaverse.nixtla.io/neuralforecast/losses.pytorch.html PyTorch loss functions for neural forecast training: MAE, MSE, MAPE, quantile losses, distribution losses, and robust losses for model optimization. The most important train signal is the forecast error, which is the difference between the observed value $y_{\tau}$ and the prediction $\hat{y}_{\tau}$, at time $y_{\tau}$: $e_{\tau} = y_{\tau}-\hat{y}_{\tau} \qquad \qquad \tau \in \{t+1,\dots,t+H \}$ The train loss summarizes the forecast errors in different train optimization objectives. All the losses are `torch.nn.modules` which helps to automatically moved them across CPU/GPU/TPU devices with Pytorch Lightning. ### `BasePointLoss` ```python theme={null} BasePointLoss( horizon_weight=None, outputsize_multiplier=None, output_names=None ) ``` Bases: [Module](#torch.nn.Module) Base class for point loss functions. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ----------------- | | `horizon_weight` | [Optional](#typing.Optional)\[[Tensor](#torch.Tensor)] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None | | `outputsize_multiplier` | [Optional](#typing.Optional)\[[int](#int)] | Multiplier for the output size. Defaults to None. | None | | `output_names` | [Optional](#typing.Optional)\[[List](#typing.List)\[[str](#str)]] | Names of the outputs. Defaults to None. | None | # 1. Scale-dependent Errors These metrics are on the same scale as the data. ## Mean Absolute Error (MAE) ### `MAE` ```python theme={null} MAE(horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Mean Absolute Error. Calculates Mean Absolute Error between `y` and `y_hat`. MAE measures the relative prediction accuracy of a forecasting method by calculating the deviation of the prediction and the true value at a given time and averages these devations over the length of the series. ```math theme={null} \mathrm{MAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} |y_{\tau} - \hat{y}_{\tau}| ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------- | | `horizon_weight` | [Optional](#typing.Optional)\[[Tensor](#torch.Tensor)] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None | #### `MAE.__call__` ```python theme={null} __call__(y, y_hat, mask=None, y_insample=None) ``` Calculate Mean Absolute Error between actual and predicted values. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies datapoints to consider in loss. Defaults to None. | None | | `y_insample` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Actual insample values. Defaults to None. | None | **Returns:** | Type | Description | | ------------------------------------ | --------------------------------- | | [Tensor](#torch.Tensor) | torch.Tensor: MAE (single value). | ## Mean Squared Error (MSE) ### `MSE` ```python theme={null} MSE(horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Mean Squared Error. Calculates Mean Squared Error between `y` and `y_hat`. MSE measures the relative prediction accuracy of a forecasting method by calculating the squared deviation of the prediction and the true value at a given time, and averages these devations over the length of the series. ```math theme={null} \mathrm{MSE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} (y_{\tau} - \hat{y}_{\tau})^{2} ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------- | | `horizon_weight` | [Optional](#typing.Optional)\[[Tensor](#torch.Tensor)] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None | #### `MSE.__call__` ```python theme={null} __call__(y, y_hat, y_insample=None, mask=None) ``` Calculate Mean Squared Error between actual and predicted values. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `y_insample` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Actual insample values. Defaults to None. | None | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies datapoints to consider in loss. Defaults to None. | None | **Returns:** | Type | Description | | ------------------------------------ | --------------------------------- | | [Tensor](#torch.Tensor) | torch.Tensor: MSE (single value). | ## Root Mean Squared Error (RMSE) ### `RMSE` ```python theme={null} RMSE(horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Root Mean Squared Error. Calculates Root Mean Squared Error between `y` and `y_hat`. RMSE measures the relative prediction accuracy of a forecasting method by calculating the squared deviation of the prediction and the observed value at a given time and averages these devations over the length of the series. Finally the RMSE will be in the same scale as the original time series so its comparison with other series is possible only if they share a common scale. RMSE has a direct connection to the L2 norm. ```math theme={null} \mathrm{RMSE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \sqrt{\frac{1}{H} \sum^{t+H}_{\tau=t+1} (y_{\tau} - \hat{y}_{\tau})^{2}} ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------- | | `horizon_weight` | [Optional](#typing.Optional)\[[Tensor](#torch.Tensor)] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None | #### `RMSE.__call__` ```python theme={null} __call__(y, y_hat, mask=None, y_insample=None) ``` **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------- | ------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Tensor, Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Tensor, Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Tensor, Specifies datapoints to consider in loss. | None | **Returns:** | Name | Type | Description | | ------ | ------------------------------------ | ---------------------- | | `rmse` | [Tensor](#torch.Tensor) | Tensor (single value). | # 2. Percentage errors These metrics are unit-free, suitable for comparisons across series. ## Mean Absolute Percentage Error (MAPE) ### `MAPE` ```python theme={null} MAPE(horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Mean Absolute Percentage Error Calculates Mean Absolute Percentage Error between `y` and `y_hat`. MAPE measures the relative prediction accuracy of a forecasting method by calculating the percentual deviation of the prediction and the observed value at a given time and averages these devations over the length of the series. The closer to zero an observed value is, the higher penalty MAPE loss assigns to the corresponding error. ```math theme={null} \mathrm{MAPE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{|y_{\tau}|} ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ---- | ---------------------------------------------------------------------- | ----------------- | | `horizon_weight` | | Tensor of size h, weight for each timestamp of the forecasting window. | None |
References * [Makridakis S., "Accuracy measures: theoretical and practical concerns".](https://www.sciencedirect.com/science/article/pii/0169207093900793)
#### `MAPE.__call__` ```python theme={null} __call__(y, y_hat, y_insample=None, mask=None) ``` **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------- | | `y` | [Tensor](#torch.Tensor) | Tensor, Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Tensor, Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Tensor, Specifies date stamps per serie to consider in loss. | None | **Returns:** | Name | Type | Description | | ------ | ------------------------------------ | ---------------------- | | `mape` | [Tensor](#torch.Tensor) | Tensor (single value). | ## Symmetric MAPE (sMAPE) ### `SMAPE` ```python theme={null} SMAPE(horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Symmetric Mean Absolute Percentage Error Calculates Symmetric Mean Absolute Percentage Error between `y` and `y_hat`. SMAPE measures the relative prediction accuracy of a forecasting method by calculating the relative deviation of the prediction and the observed value scaled by the sum of the absolute values for the prediction and observed value at a given time, then averages these devations over the length of the series. This allows the SMAPE to have bounds between 0% and 200% which is desirable compared to normal MAPE that may be undetermined when the target is zero. ```math theme={null} \mathrm{sMAPE}_{2}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{|y_{\tau}|+|\hat{y}_{\tau}|} ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ---- | ---------------------------------------------------------------------- | ----------------- | | `horizon_weight` | | Tensor of size h, weight for each timestamp of the forecasting window. | None |
References * [Makridakis S., "Accuracy measures: theoretical and practical concerns".](https://www.sciencedirect.com/science/article/pii/0169207093900793)
#### `SMAPE.__call__` ```python theme={null} __call__(y, y_hat, mask=None, y_insample=None) ``` **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------- | | `y` | [Tensor](#torch.Tensor) | Tensor, Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Tensor, Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Tensor, Specifies date stamps per serie to consider in loss. | None | **Returns:** | Name | Type | Description | | ------- | ------------------------------------ | ---------------------- | | `smape` | [Tensor](#torch.Tensor) | Tensor (single value). | # 3. Scale-independent Errors These metrics measure the relative improvements versus baselines. ## Mean Absolute Scaled Error (MASE) ### `MASE` ```python theme={null} MASE(seasonality, horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Mean Absolute Scaled Error Calculates the Mean Absolute Scaled Error between `y` and `y_hat`. MASE measures the relative prediction accuracy of a forecasting method by comparinng the mean absolute errors of the prediction and the observed value against the mean absolute errors of the seasonal naive model. The MASE partially composed the Overall Weighted Average (OWA), used in the M4 Competition. ```math theme={null} \mathrm{MASE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{\mathrm{MAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau})} ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ------------------------ | --------------------------------------------------------------------------------------------------------- | ----------------- | | `seasonality` | [int](#int) | Int. Main frequency of the time series; Hourly 24, Daily 7, Weekly 52, Monthly 12, Quarterly 4, Yearly 1. | *required* | | `horizon_weight` | | Tensor of size h, weight for each timestamp of the forecasting window. | None |
References [Rob J. Hyndman, & Koehler, A. B. "Another look at measures of forecast accuracy".](https://www.sciencedirect.com/science/article/pii/S0169207006000239) [Spyros Makridakis, Evangelos Spiliotis, Vassilios Assimakopoulos, "The M4 Competition: 100,000 time series and 61 forecasting methods".](https://www.sciencedirect.com/science/article/pii/S0169207019301128)
#### `MASE.__call__` ```python theme={null} __call__(y, y_hat, y_insample, mask=None) ``` **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------- | | `y` | [Tensor](#torch.Tensor) | Tensor (batch\_size, output\_size), Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Tensor (batch\_size, output\_size)), Predicted values. | *required* | | `y_insample` | [Tensor](#torch.Tensor) | Tensor (batch\_size, input\_size), Actual insample values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Tensor, Specifies date stamps per serie to consider in loss. | None | **Returns:** | Name | Type | Description | | ------ | ------------------------------------ | ---------------------- | | `mase` | [Tensor](#torch.Tensor) | Tensor (single value). | ## Relative Mean Squared Error (relMSE) ### `relMSE` ```python theme={null} relMSE(y_train=None, horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Relative Mean Squared Error Computes Relative Mean Squared Error (relMSE), as proposed by Hyndman & Koehler (2006) as an alternative to percentage errors, to avoid measure unstability. ```math theme={null} \mathrm{relMSE}(\mathbf{y}, \mathbf{\hat{y}}, \mathbf{\hat{y}}^{benchmark}) = \frac{\mathrm{MSE}(\mathbf{y}, \mathbf{\hat{y}})}{\mathrm{MSE}(\mathbf{y}, \mathbf{\hat{y}}^{benchmark})} ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ---- | ---------------------------------------------------------------------- | ----------------- | | `y_train` | | Numpy array, deprecated. | None | | `horizon_weight` | | Tensor of size h, weight for each timestamp of the forecasting window. | None |
References * [Hyndman, R. J and Koehler, A. B. (2006). "Another look at measures of forecast accuracy", International Journal of Forecasting, Volume 22, Issue 4.](https://www.sciencedirect.com/science/article/pii/S0169207006000239) * [Kin G. Olivares, O. Nganba Meetei, Ruijun Ma, Rohan Reddy, Mengfei Cao, Lee Dicker. "Probabilistic Hierarchical Forecasting with Deep Poisson Mixtures. Submitted to the International Journal Forecasting, Working paper available at arxiv.](https://arxiv.org/pdf/2110.13179.pdf)
#### `relMSE.__call__` ```python theme={null} __call__(y, y_hat, y_benchmark, mask=None) ``` **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Tensor (batch\_size, output\_size), Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Tensor (batch\_size, output\_size)), Predicted values. | *required* | | `y_benchmark` | [Tensor](#torch.Tensor) | Tensor (batch\_size, output\_size), Benchmark predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Tensor, Specifies date stamps per serie to consider in loss. | None | **Returns:** | Name | Type | Description | | -------- | ------------------------------------ | ---------------------- | | `relMSE` | [Tensor](#torch.Tensor) | Tensor (single value). | # 4. Probabilistic Errors These methods use statistical approaches for estimating unknown probability distributions using observed data. Maximum likelihood estimation involves finding the parameter values that maximize the likelihood function, which measures the probability of obtaining the observed data given the parameter values. MLE has good theoretical properties and efficiency under certain satisfied assumptions. On the non-parametric approach, quantile regression measures non-symmetrically deviation, producing under/over estimation. ## Quantile Loss ### `QuantileLoss` ```python theme={null} QuantileLoss(q, horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Quantile Loss. Computes the quantile loss between `y` and `y_hat`. QL measures the deviation of a quantile forecast. By weighting the absolute deviation in a non symmetric way, the loss pays more attention to under or over estimation. A common value for q is 0.5 for the deviation from the median (Pinball loss). ```math theme={null} \mathrm{QL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q)}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \Big( (1-q)\,( \hat{y}^{(q)}_{\tau} - y_{\tau} )_{+} + q\,( y_{\tau} - \hat{y}^{(q)}_{\tau} )_{+} \Big) ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `q` | [float](#float) | Between 0 and 1. The slope of the quantile loss, in the context of quantile regression, the q determines the conditional quantile level. | *required* | | `horizon_weight` | [Optional](#typing.Optional)\[[Tensor](#torch.Tensor)] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None |
References [Roger Koenker and Gilbert Bassett, Jr., "Regression Quantiles".](https://www.jstor.org/stable/1913643)
#### `QuantileLoss.__call__` ```python theme={null} __call__(y, y_hat, y_insample=None, mask=None) ``` Calculate quantile loss between actual and predicted values. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `y_insample` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Actual insample values. Defaults to None. | None | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies datapoints to consider in loss. Defaults to None. | None | **Returns:** | Type | Description | | ------------------------------------ | ------------------------------------------- | | [Tensor](#torch.Tensor) | torch.Tensor: Quantile loss (single value). | ## Multi Quantile Loss (MQLoss) ### `MQLoss` ```python theme={null} MQLoss(level=[80, 90], quantiles=None, horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Multi-Quantile loss Calculates the Multi-Quantile loss (MQL) between `y` and `y_hat`. MQL calculates the average multi-quantile Loss for a given set of quantiles, based on the absolute difference between predicted quantiles and observed values. ```math theme={null} \mathrm{MQL}(\mathbf{y}_{\tau},[\mathbf{\hat{y}}^{(q_{1})}_{\tau}, ... ,\hat{y}^{(q_{n})}_{\tau}]) = \frac{1}{n} \sum_{q_{i}} \mathrm{QL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q_{i})}_{\tau}) ``` The limit behavior of MQL allows to measure the accuracy of a full predictive distribution $\\mathbf{\\hat{F}}\_{\\tau}$ with the continuous ranked probability score (CRPS). This can be achieved through a numerical integration technique, that discretizes the quantiles and treats the CRPS integral with a left Riemann approximation, averaging over uniformly distanced quantiles. ```math theme={null} \mathrm{CRPS}(y_{\tau}, \mathbf{\hat{F}}_{\tau}) = \int^{1}_{0} \mathrm{QL}(y_{\tau}, \hat{y}^{(q)}_{\tau}) dq ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------- | | `level` | [List](#typing.List)\[[int](#int)] | Probability levels for prediction intervals. Defaults to \[80, 90]. | \[80, 90] | | `quantiles` | [Optional](#typing.Optional)\[[List](#typing.List)\[[float](#float)]] | Alternative to level, quantiles to estimate from y distribution. Defaults to None. | None | | `horizon_weight` | [Optional](#typing.Optional)\[[Tensor](#torch.Tensor)] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None |
References [Roger Koenker and Gilbert Bassett, Jr., "Regression Quantiles".](https://www.jstor.org/stable/1913643) [James E. Matheson and Robert L. Winkler, "Scoring Rules for Continuous Probability Distributions".](https://www.jstor.org/stable/2629907)
#### `MQLoss.__call__` ```python theme={null} __call__(y, y_hat, y_insample=None, mask=None) ``` Computes the multi-quantile loss. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `y_insample` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | In-sample values. Defaults to None. | None | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Type | Description | | ------------------------------------ | ------------------------------------------------- | | [Tensor](#torch.Tensor) | torch.Tensor: Multi-quantile loss (single value). | ## Implicit Quantile Loss (IQLoss) ### `QuantileLayer` ```python theme={null} QuantileLayer(num_output, cos_embedding_dim=128) ``` Bases: [Module](#torch.nn.Module) Implicit Quantile Layer from the paper IQN for Distributional Reinforcement Learning. Code from GluonTS: [https://github.com/awslabs/gluonts/blob/61133ef6e2d88177b32ace4afc6843ab9a7bc8cd/src/gluonts/torch/distributions/implicit\_quantile\_network.py](https://github.com/awslabs/gluonts/blob/61133ef6e2d88177b32ace4afc6843ab9a7bc8cd/src/gluonts/torch/distributions/implicit_quantile_network.py)
References Dabney et al. 2018. [https://arxiv.org/abs/1806.06923](https://arxiv.org/abs/1806.06923)
### `IQLoss` ```python theme={null} IQLoss( cos_embedding_dim=64, concentration0=1.0, concentration1=1.0, horizon_weight=None, ) ``` Bases: [QuantileLoss](#neuralforecast.losses.pytorch.QuantileLoss) Implicit Quantile Loss. Computes the quantile loss between `y` and `y_hat`, with the quantile `q` provided as an input to the network. IQL measures the deviation of a quantile forecast. By weighting the absolute deviation in a non symmetric way, the loss pays more attention to under or over estimation. ```math theme={null} \mathrm{QL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q)}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \Big( (1-q)\,( \hat{y}^{(q)}_{\tau} - y_{\tau} )_{+} + q\,( y_{\tau} - \hat{y}^{(q)}_{\tau} )_{+} \Big) ``` **Parameters:** | Name | Type | Description | Default | | ------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------- | | `cos_embedding_dim` | [int](#int) | Cosine embedding dimension. Defaults to 64. | 64 | | `concentration0` | [float](#float) | Beta distribution concentration parameter. Defaults to 1.0. | 1.0 | | `concentration1` | [float](#float) | Beta distribution concentration parameter. Defaults to 1.0. | 1.0 | | `horizon_weight` | [Optional](#typing.Optional)\[[Tensor](#torch.Tensor)] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None |
References Gouttes, Adèle, Kashif Rasul, Mateusz Koren, Johannes Stephan, and Tofigh Naghibi, "Probabilistic Time Series Forecasting with Implicit Quantile Networks". [http://arxiv.org/abs/2107.03743](http://arxiv.org/abs/2107.03743)
#### `IQLoss.__call__` ```python theme={null} __call__(y, y_hat, y_insample=None, mask=None) ``` Calculate quantile loss between actual and predicted values. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `y_insample` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Actual insample values. Defaults to None. | None | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies datapoints to consider in loss. Defaults to None. | None | **Returns:** | Type | Description | | ------------------------------------ | ------------------------------------------- | | [Tensor](#torch.Tensor) | torch.Tensor: Quantile loss (single value). | ## DistributionLoss ### `DistributionLoss` ```python theme={null} DistributionLoss( distribution, level=[80, 90], quantiles=None, num_samples=1000, return_params=False, horizon_weight=None, **distribution_kwargs ) ``` Bases: [Module](#torch.nn.Module) DistributionLoss This PyTorch module wraps the `torch.distribution` classes allowing it to interact with NeuralForecast models modularly. It shares the negative log-likelihood as the optimization objective and a sample method to generate empirically the quantiles defined by the `level` list. Additionally, it implements a distribution transformation that factorizes the scale-dependent likelihood parameters into a base scale and a multiplier efficiently learnable within the network's non-linearities operating ranges. Available distributions: * Poisson * Normal * StudentT * NegativeBinomial * Tweedie * Bernoulli (Temporal Classifiers) * ISQF (Incremental Spline Quantile Function) **Parameters:** | Name | Type | Description | Default | | ---------------- | ------------------------------ | ---------------------------------------------------------------------- | ---------------------- | | `distribution` | [str](#str) | Identifier of a torch.distributions.Distribution class. | *required* | | `level` | float list | Confidence levels for prediction intervals. | \[80, 90] | | `quantiles` | float list | Alternative to level list, target quantiles. | None | | `num_samples` | [int](#int) | Number of samples for the empirical quantiles. | 1000 | | `return_params` | [bool](#bool) | Whether or not return the Distribution parameters. | False | | `horizon_weight` | [Tensor](#Tensor) | Tensor of size h, weight for each timestamp of the forecasting window. | None | **Returns:** | Name | Type | Description | | ------- | ---- | -------------------------------------------------- | | `tuple` | | Tuple with tensors of ISQF distribution arguments. |
References * [PyTorch Probability Distributions Package: StudentT.](https://pytorch.org/docs/stable/distributions.html#studentt) * [David Salinas, Valentin Flunkert, Jan Gasthaus, Tim Januschowski (2020). "DeepAR: Probabilistic forecasting with autoregressive recurrent networks". International Journal of Forecasting.](https://www.sciencedirect.com/science/article/pii/S0169207019301888) * [Park, Youngsuk, Danielle Maddix, François-Xavier Aubet, Kelvin Kan, Jan Gasthaus, and Yuyang Wang (2022). "Learning Quantile Functions without Quantile Crossing for Distribution-free Time Series Forecasting".](https://proceedings.mlr.press/v151/park22a.html)
#### `DistributionLoss.__call__` ```python theme={null} __call__(y, distr_args, mask=None) ``` Computes the negative log-likelihood objective function. To estimate the following predictive distribution: ```math theme={null} \mathrm{P}(\mathbf{y}_{\tau}\,|\,\theta) \quad \mathrm{and} \quad -\log(\mathrm{P}(\mathbf{y}_{\tau}\,|\,\theta)) ``` where $\\theta$ represents the distributions parameters. It aditionally summarizes the objective signal using a weighted average using the `mask` tensor. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `distr_args` | [Tensor](#torch.Tensor) | Constructor arguments for the underlying Distribution type. | *required* | | `loc` | [Optional](#typing.Optional)\[[Tensor](#torch.Tensor)] | Optional tensor, of the same shape as the batch\_shape + event\_shape. Defaults to None. of the resulting distribution. | *required* | | `scale` | [Optional](#typing.Optional)\[[Tensor](#torch.Tensor)] | Optional tensor, of the same shape as the batch\_shape+event\_shape of the resulting distribution. Defaults to None. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ---- | ----------------------------------------------------------------------- | | `float` | | Weighted loss function against which backpropagation will be performed. | ## Poisson Mixture Mesh (PMM) ### `PMM` ```python theme={null} PMM( n_components=10, level=[80, 90], quantiles=None, num_samples=1000, return_params=False, batch_correlation=False, horizon_correlation=False, weighted=False, ) ``` Bases: [Module](#torch.nn.Module) Poisson Mixture Mesh This Poisson Mixture statistical model assumes independence across groups of data $\\mathcal{G}={[g\_{i}]}$, and estimates relationships within the group. ```math theme={null} \mathrm{P}\left(\mathbf{y}_{[b][t+1:t+H]}\right) = \prod_{ [g_{i}] \in \mathcal{G}} \mathrm{P} \left(\mathbf{y}_{[g_{i}][\tau]} \right) = \prod_{\beta\in[g_{i}]} \left(\sum_{k=1}^{K} w_k \prod_{(\beta,\tau) \in [g_i][t+1:t+H]} \mathrm{Poisson}(y_{\beta,\tau}, \hat{\lambda}_{\beta,\tau,k}) \right) ``` **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------- | --------------------------------------------------------------------- | ---------------------- | | `n_components` | [int](#int) | The number of mixture components. Defaults to 10. | 10 | | `level` | float list | Confidence levels for prediction intervals. Defaults to \[80, 90]. | \[80, 90] | | `quantiles` | float list | Alternative to level list, target quantiles. Defaults to None. | None | | `return_params` | [bool](#bool) | Whether or not return the Distribution parameters. Defaults to False. | False | | `batch_correlation` | [bool](#bool) | Whether or not model batch correlations. Defaults to False. | False | | `horizon_correlation` | [bool](#bool) | Whether or not model horizon correlations. Defaults to False. | False |
References * [Kin G. Olivares, O. Nganba Meetei, Ruijun Ma, Rohan Reddy, Mengfei Cao, Lee Dicker. Probabilistic Hierarchical Forecasting with Deep Poisson Mixtures. Submitted to the International Journal Forecasting, Working paper available at arxiv.](https://arxiv.org/pdf/2110.13179.pdf)
#### `PMM.__call__` ```python theme={null} __call__(y, distr_args, mask=None) ``` Computes the negative log-likelihood objective function. To estimate the following predictive distribution: ```math theme={null} \mathrm{P}(\mathbf{y}_{\tau}\,|\,\theta) \quad \mathrm{and} \quad -\log(\mathrm{P}(\mathbf{y}_{\tau}\,|\,\theta)) ``` where $\\theta$ represents the distributions parameters. It aditionally summarizes the objective signal using a weighted average using the `mask` tensor. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `distr_args` | [Tensor](#torch.Tensor) | Constructor arguments for the underlying Distribution type. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ---- | ----------------------------------------------------------------------- | | `float` | | Weighted loss function against which backpropagation will be performed. | ## Gaussian Mixture Mesh (GMM) ### `GMM` ```python theme={null} GMM( n_components=1, level=[80, 90], quantiles=None, num_samples=1000, return_params=False, batch_correlation=False, horizon_correlation=False, weighted=False, ) ``` Bases: [Module](#torch.nn.Module) Gaussian Mixture Mesh This Gaussian Mixture statistical model assumes independence across groups of data $\\mathcal{G}={[g\_{i}]}$, and estimates relationships within the group. ```math theme={null} \mathrm{P}\left(\mathbf{y}_{[b][t+1:t+H]}\right) = \prod_{ [g_{i}] \in \mathcal{G}} \mathrm{P}\left(\mathbf{y}_{[g_{i}][\tau]}\right)= \prod_{\beta\in[g_{i}]} \left(\sum_{k=1}^{K} w_k \prod_{(\beta,\tau) \in [g_i][t+1:t+H]} \mathrm{Gaussian}(y_{\beta,\tau}, \hat{\mu}_{\beta,\tau,k}, \sigma_{\beta,\tau,k})\right) ``` **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------- | --------------------------------------------------------------------- | ---------------------- | | `n_components` | [int](#int) | The number of mixture components. Defaults to 10. | 1 | | `level` | float list | Confidence levels for prediction intervals. Defaults to \[80, 90]. | \[80, 90] | | `quantiles` | float list | Alternative to level list, target quantiles. Defaults to None. | None | | `return_params` | [bool](#bool) | Whether or not return the Distribution parameters. Defaults to False. | False | | `batch_correlation` | [bool](#bool) | Whether or not model batch correlations. Defaults to False. | False | | `horizon_correlation` | [bool](#bool) | Whether or not model horizon correlations. Defaults to False. | False | | `weighted` | [bool](#bool) | Whether or not model weighted components. Defaults to False. | False | | `num_samples` | [int](#int) | Number of samples for the empirical quantiles. Defaults to 1000. | 1000 |
References * [Kin G. Olivares, O. Nganba Meetei, Ruijun Ma, Rohan Reddy, Mengfei Cao, Lee Dicker. Probabilistic Hierarchical Forecasting with Deep Poisson Mixtures. Submitted to the International Journal Forecasting, Working paper available at arxiv.](https://arxiv.org/pdf/2110.13179.pdf)
#### `GMM.__call__` ```python theme={null} __call__(y, distr_args, mask=None) ``` Computes the negative log-likelihood objective function. To estimate the following predictive distribution: ```math theme={null} \mathrm{P}(\mathbf{y}_{\tau}\,|\,\theta) \quad \mathrm{and} \quad -\log(\mathrm{P}(\mathbf{y}_{\tau}\,|\,\theta)) ``` where $\\theta$ represents the distributions parameters. It aditionally summarizes the objective signal using a weighted average using the `mask` tensor. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `distr_args` | [Tensor](#torch.Tensor) | Constructor arguments for the underlying Distribution type. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ---- | ----------------------------------------------------------------------- | | `float` | | Weighted loss function against which backpropagation will be performed. | ## Negative Binomial Mixture Mesh (NBMM) ### `NBMM` ```python theme={null} NBMM( n_components=1, level=[80, 90], quantiles=None, num_samples=1000, return_params=False, weighted=False, ) ``` Bases: [Module](#torch.nn.Module) Negative Binomial Mixture Mesh This N. Binomial Mixture statistical model assumes independence across groups of data $\\mathcal{G}={[g\_{i}]}$, and estimates relationships within the group. ```math theme={null} \mathrm{P}\left(\mathbf{y}_{[b][t+1:t+H]}\right) = \prod_{ [g_{i}] \in \mathcal{G}} \mathrm{P}\left(\mathbf{y}_{[g_{i}][\tau]}\right)= \prod_{\beta\in[g_{i}]} \left(\sum_{k=1}^{K} w_k \prod_{(\beta,\tau) \in [g_i][t+1:t+H]} \mathrm{NBinomial}(y_{\beta,\tau}, \hat{r}_{\beta,\tau,k}, \hat{p}_{\beta,\tau,k})\right) ``` **Parameters:** | Name | Type | Description | Default | | --------------- | -------------------------- | --------------------------------------------------------------------- | ---------------------- | | `n_components` | [int](#int) | The number of mixture components. Defaults to 10. | 1 | | `level` | float list | Confidence levels for prediction intervals. Defaults to \[80, 90]. | \[80, 90] | | `quantiles` | float list | Alternative to level list, target quantiles. Defaults to None. | None | | `return_params` | [bool](#bool) | Whether or not return the Distribution parameters. Defaults to False. | False | | `weighted` | [bool](#bool) | Whether or not model weighted components. Defaults to False. | False | | `num_samples` | [int](#int) | Number of samples for the empirical quantiles. Defaults to 1000. | 1000 |
References * [Kin G. Olivares, O. Nganba Meetei, Ruijun Ma, Rohan Reddy, Mengfei Cao, Lee Dicker. Probabilistic Hierarchical Forecasting with Deep Poisson Mixtures. Submitted to the International Journal Forecasting, Working paper available at arxiv.](https://arxiv.org/pdf/2110.13179.pdf)
#### `NBMM.__call__` ```python theme={null} __call__(y, distr_args, mask=None) ``` Computes the negative log-likelihood objective function. To estimate the following predictive distribution: ```math theme={null} \mathrm{P}(\mathbf{y}_{\tau}\,|\,\theta) \quad \mathrm{and} \quad -\log(\mathrm{P}(\mathbf{y}_{\tau}\,|\,\theta)) ``` where $\\theta$ represents the distributions parameters. It aditionally summarizes the objective signal using a weighted average using the `mask` tensor. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `distr_args` | [Tensor](#torch.Tensor) | Constructor arguments for the underlying Distribution type. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ---- | ----------------------------------------------------------------------- | | `float` | | Weighted loss function against which backpropagation will be performed. | # 5. Robustified Errors ## Huber Loss ### `HuberLoss` ```python theme={null} HuberLoss(delta=1.0, horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Huber Loss The Huber loss, employed in robust regression, is a loss function that exhibits reduced sensitivity to outliers in data when compared to the squared error loss. This function is also referred to as SmoothL1. The Huber loss function is quadratic for small errors and linear for large errors, with equal values and slopes of the different sections at the two points where $(y\_{\\tau}-\\hat{y}_{\\tau})^{2}$=$|y_{\\tau}-\\hat{y}\_{\\tau}|$. ```math theme={null} L_{\delta}(y_{\tau},\; \hat{y}_{\tau}) =\begin{cases}{\frac{1}{2}}(y_{\tau}-\hat{y}_{\tau})^{2}\;{\text{for }}|y_{\tau}-\hat{y}_{\tau}|\leq \delta \\ \delta \ \cdot \left(|y_{\tau}-\hat{y}_{\tau}|-{\frac {1}{2}}\delta \right),\;{\text{otherwise.}}\end{cases} ``` where $\\delta$ is a threshold parameter that determines the point at which the loss transitions from quadratic to linear, and can be tuned to control the trade-off between robustness and accuracy in the predictions. **Parameters:** | Name | Type | Description | Default | | ---------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------- | | `delta` | [float](#float) | Specifies the threshold at which to change between delta-scaled L1 and L2 loss. Defaults to 1.0. | 1.0 | | `horizon_weight` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None |
References * [Huber Peter, J (1964). "Robust Estimation of a Location Parameter". Annals of Statistics](https://projecteuclid.org/journals/annals-of-mathematical-statistics/volume-35/issue-1/Robust-Estimation-of-a-Location-Parameter/10.1214/aoms/1177703732.full)
#### `HuberLoss.__call__` ```python theme={null} __call__(y, y_hat, y_insample=None, mask=None) ``` **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ------------------------------------ | ----------- | | `float` | [Tensor](#torch.Tensor) | Huber loss. | ## Tukey Loss ### `TukeyLoss` ```python theme={null} TukeyLoss(c=4.685, normalize=True) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Tukey Loss The Tukey loss function, also known as Tukey's biweight function, is a robust statistical loss function used in robust statistics. Tukey's loss exhibits quadratic behavior near the origin, like the Huber loss; however, it is even more robust to outliers as the loss for large residuals remains constant instead of scaling linearly. The parameter $c$ in Tukey's loss determines the ''saturation'' point of the function: Higher values of $c$ enhance sensitivity, while lower values increase resistance to outliers. ```math theme={null} L_{c}(y_{\tau},\; \hat{y}_{\tau}) =\begin{cases}{ \frac{c^{2}}{6}} \left[1-(\frac{y_{\tau}-\hat{y}_{\tau}}{c})^{2} \right]^{3} \;\text{for } |y_{\tau}-\hat{y}_{\tau}|\leq c \\ \frac{c^{2}}{6} \qquad \text{otherwise.} \end{cases} ``` Please note that the Tukey loss function assumes the data to be stationary or normalized beforehand. If the error values are excessively large, the algorithm may need help to converge during optimization. It is advisable to employ small learning rates. **Parameters:** | Name | Type | Description | Default | | ----------- | ---------------------------- | --------------------------------------------------------------------------------------------------- | ------------------ | | `c` | [float](#float) | Specifies the Tukey loss' threshold on which residuals are no longer considered. Defaults to 4.685. | 4.685 | | `normalize` | [bool](#bool) | Wether normalization is performed within Tukey loss' computation. Defaults to True. | True |
References * [Beaton, A. E., and Tukey, J. W. (1974). "The Fitting of Power Series, Meaning Polynomials, Illustrated on Band-Spectroscopic Data."](https://www.jstor.org/stable/1267936)
#### `TukeyLoss.__call__` ```python theme={null} __call__(y, y_hat, y_insample=None, mask=None) ``` **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ------------------------------------ | ----------- | | `float` | [Tensor](#torch.Tensor) | Tukey loss. | ## Huberized Quantile Loss ### `HuberQLoss` ```python theme={null} HuberQLoss(q, delta=1.0, horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Huberized Quantile Loss The Huberized quantile loss is a modified version of the quantile loss function that combines the advantages of the quantile loss and the Huber loss. It is commonly used in regression tasks, especially when dealing with data that contains outliers or heavy tails. The Huberized quantile loss between `y` and `y_hat` measure the Huber Loss in a non-symmetric way. The loss pays more attention to under/over-estimation depending on the quantile parameter $q$; and controls the trade-off between robustness and accuracy in the predictions with the parameter $delta$. ```math theme={null} \mathrm{HuberQL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q)}_{\tau}) = (1-q)\, L_{\delta}(y_{\tau},\; \hat{y}^{(q)}_{\tau}) \mathbb{1}\{ \hat{y}^{(q)}_{\tau} \geq y_{\tau} \} + q\, L_{\delta}(y_{\tau},\; \hat{y}^{(q)}_{\tau}) \mathbb{1}\{ \hat{y}^{(q)}_{\tau} < y_{\tau} \} ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `delta` | [float](#float) | Specifies the threshold at which to change between delta-scaled L1 and L2 loss. Defaults to 1.0. | 1.0 | | `q` | [float](#float) | The slope of the quantile loss, in the context of quantile regression, the q determines the conditional quantile level. Defaults to 0.5. | *required* | | `horizon_weight` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None |
References * [Huber Peter, J (1964). "Robust Estimation of a Location Parameter". Annals of Statistics](https://projecteuclid.org/journals/annals-of-mathematical-statistics/volume-35/issue-1/Robust-Estimation-of-a-Location-Parameter/10.1214/aoms/1177703732.full) * [Roger Koenker and Gilbert Bassett, Jr., "Regression Quantiles".](https://www.jstor.org/stable/1913643)
#### `HuberQLoss.__call__` ```python theme={null} __call__(y, y_hat, y_insample=None, mask=None) ``` **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ------------------------------------ | ----------- | | `float` | [Tensor](#torch.Tensor) | HuberQLoss. | ## Huberized MQLoss ### `HuberMQLoss` ```python theme={null} HuberMQLoss(level=[80, 90], quantiles=None, delta=1.0, horizon_weight=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Huberized Multi-Quantile loss The Huberized Multi-Quantile loss (HuberMQL) is a modified version of the multi-quantile loss function that combines the advantages of the quantile loss and the Huber loss. HuberMQL is commonly used in regression tasks, especially when dealing with data that contains outliers or heavy tails. The loss function pays more attention to under/over-estimation depending on the quantile list $[q\_{1},q\_{2},\\dots]$ parameter. It controls the trade-off between robustness and prediction accuracy with the parameter $\\delta$. ```math theme={null} \mathrm{HuberMQL}_{\delta}(\mathbf{y}_{\tau},[\mathbf{\hat{y}}^{(q_{1})}_{\tau}, ... ,\hat{y}^{(q_{n})}_{\tau}]) = \frac{1}{n} \sum_{q_{i}} \mathrm{HuberQL}_{\delta}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q_{i})}_{\tau}) ``` **Parameters:** | Name | Type | Description | Default | | ---------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------- | | `level` | int list | Probability levels for prediction intervals (Defaults median). Defaults to \[80, 90]. | \[80, 90] | | `quantiles` | float list | Alternative to level, quantiles to estimate from y distribution. Defaults to None. | None | | `delta` | [float](#float) | Specifies the threshold at which to change between delta-scaled L1 and L2 loss. Defaults to 1.0. | 1.0 | | `horizon_weight` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None |
References * [Huber Peter, J (1964). "Robust Estimation of a Location Parameter". Annals of Statistics](https://projecteuclid.org/journals/annals-of-mathematical-statistics/volume-35/issue-1/Robust-Estimation-of-a-Location-Parameter/10.1214/aoms/1177703732.full) * [Roger Koenker and Gilbert Bassett, Jr., "Regression Quantiles".](https://www.jstor.org/stable/1913643)
#### `HuberMQLoss.__call__` ```python theme={null} __call__(y, y_hat, y_insample=None, mask=None) ``` **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ------------------------------------ | ------------ | | `float` | [Tensor](#torch.Tensor) | HuberMQLoss. | ## Huberized IQLoss ### `HuberIQLoss` ```python theme={null} HuberIQLoss( cos_embedding_dim=64, concentration0=1.0, concentration1=1.0, delta=1.0, horizon_weight=None, ) ``` Bases: [HuberQLoss](#neuralforecast.losses.pytorch.HuberQLoss) Implicit Huber Quantile Loss Computes the huberized quantile loss between `y` and `y_hat`, with the quantile `q` provided as an input to the network. HuberIQLoss measures the deviation of a huberized quantile forecast. By weighting the absolute deviation in a non symmetric way, the loss pays more attention to under or over estimation. ```math theme={null} \mathrm{HuberIQL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q)}_{\tau}) = (1-q)\, L_{\delta}(y_{\tau},\; \hat{y}^{(q)}_{\tau}) \mathbb{1}\{ \hat{y}^{(q)}_{\tau} \geq y_{\tau} \} + q\, L_{\delta}(y_{\tau},\; \hat{y}^{(q)}_{\tau}) \mathbb{1}\{ \hat{y}^{(q)}_{\tau} < y_{\tau} \} ``` **Parameters:** | Name | Type | Description | Default | | ------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `quantile_sampling` | [str](#str) | Sampling distribution used to sample the quantiles during training. Choose from \['uniform', 'beta']. Defaults to 'uniform'. | *required* | | `horizon_weight` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Tensor of size h, weight for each timestamp of the forecasting window. Defaults to None. | None | | `delta` | [float](#float) | Specifies the threshold at which to change between delta-scaled L1 and L2 loss. Defaults to 1.0. | 1.0 |
References * [Gouttes, Adèle, Kashif Rasul, Mateusz Koren, Johannes Stephan, and Tofigh Naghibi, "Probabilistic Time Series Forecasting with Implicit Quantile Networks".](http://arxiv.org/abs/2107.03743) * [Huber Peter, J (1964). "Robust Estimation of a Location Parameter". Annals of Statistics](https://projecteuclid.org/journals/annals-of-mathematical-statistics/volume-35/issue-1/Robust-Estimation-of-a-Location-Parameter/10.1214/aoms/1177703732.full) * [Roger Koenker and Gilbert Bassett, Jr., "Regression Quantiles".](https://www.jstor.org/stable/1913643)
#### `HuberIQLoss.__call__` ```python theme={null} __call__(y, y_hat, y_insample=None, mask=None) ``` **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ------------------------------------ | ----------- | | `float` | [Tensor](#torch.Tensor) | HuberQLoss. | # 6. Others ## Accuracy ### `Accuracy` ```python theme={null} Accuracy() ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Accuracy Computes the accuracy between categorical `y` and `y_hat`. This evaluation metric is only meant for evaluation, as it is not differentiable. ```math theme={null} \mathrm{Accuracy}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \mathrm{1}\{\mathbf{y}_{\tau}==\mathbf{\hat{y}}_{\tau}\} ``` #### `Accuracy.__call__` ```python theme={null} __call__(y, y_hat, y_insample, mask=None) ``` **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per serie to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ------------------------------------ | ----------- | | `float` | [Tensor](#torch.Tensor) | Accuracy. | ## Scaled Continuous Ranked Probability Score (sCRPS) ### `sCRPS` ```python theme={null} sCRPS(level=[80, 90], quantiles=None) ``` Bases: [BasePointLoss](#neuralforecast.losses.pytorch.BasePointLoss) Scaled Continues Ranked Probability Score Calculates a scaled variation of the CRPS, as proposed by Rangapuram (2021), to measure the accuracy of predicted quantiles `y_hat` compared to the observation `y`. This metric averages percentual weighted absolute deviations as defined by the quantile losses. ```math theme={null} \mathrm{sCRPS}(\mathbf{\hat{y}}^{(q)}_{\tau}, \mathbf{y}_{\tau}) = \frac{2}{N} \sum_{i} \int^{1}_{0} \frac{\mathrm{QL}(\mathbf{\hat{y}}^{(q}_{\tau} y_{i,\tau})_{q}}{\sum_{i} | y_{i,\tau} |} dq ``` where $\\mathbf{\\hat{y}}^{(q}_{\\tau}$ is the estimated quantile, and $y_{i,\\tau}$ are the target variable realizations. **Parameters:** | Name | Type | Description | Default | | ----------- | ----------------------- | ------------------------------------------------------------------------------------- | ---------------------- | | `level` | int list | Probability levels for prediction intervals (Defaults median). Defaults to \[80, 90]. | \[80, 90] | | `quantiles` | float list | Alternative to level, quantiles to estimate from y distribution. Defaults to None. | None |
References * [Gneiting, Tilmann. (2011). "Quantiles as optimal point forecasts". International Journal of Forecasting.](https://www.sciencedirect.com/science/article/pii/S0169207010000063) * [Spyros Makridakis, Evangelos Spiliotis, Vassilios Assimakopoulos, Zhi Chen, Anil Gaba, Ilia Tsetlin, Robert L. Winkler. (2022). "The M5 uncertainty competition: Results, findings and conclusions". International Journal of Forecasting.](https://www.sciencedirect.com/science/article/pii/S0169207021001722) * [Syama Sundar Rangapuram, Lucien D Werner, Konstantinos Benidis, Pedro Mercado, Jan Gasthaus, Tim Januschowski. (2021). "End-to-End Learning of Coherent Probabilistic Forecasts for Hierarchical Time Series". Proceedings of the 38th International Conference on Machine Learning (ICML).](https://proceedings.mlr.press/v139/rangapuram21a.html)
#### `sCRPS.__call__` ```python theme={null} __call__(y, y_hat, y_insample, mask=None) ``` **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------- | | `y` | [Tensor](#torch.Tensor) | Actual values. | *required* | | `y_hat` | [Tensor](#torch.Tensor) | Predicted values. | *required* | | `mask` | [Union](#typing.Union)\[[Tensor](#torch.Tensor), None] | Specifies date stamps per series to consider in loss. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ------------------------------------ | ----------- | | `float` | [Tensor](#torch.Tensor) | sCRPS. | # Autoformer Source: https://nixtlaverse.nixtla.io/neuralforecast/models.autoformer.html Autoformer: Transformer with auto-correlation mechanism and progressive decomposition for reliable long-horizon time series forecasting with trend-seasonality. The Autoformer model tackles the challenge of finding reliable dependencies on intricate temporal patterns of long-horizon forecasting. The architecture has the following distinctive features: - In-built progressive decomposition in trend and seasonal compontents based on a moving average filter. - Auto-Correlation mechanism that discovers the period-based dependencies by calculating the autocorrelation and aggregating similar sub-series based on the periodicity. - Classic encoder-decoder proposed by Vaswani et al. (2017) with a multi-head attention mechanism. The Autoformer model utilizes a three-component approach to define its embedding: - It employs encoded autoregressive features obtained from a convolution network. - Absolute positional embeddings obtained from calendar features are utilized. **References** * [Wu, Haixu, Jiehui Xu, Jianmin Wang, and Mingsheng Long. “Autoformer: Decomposition transformers with auto-correlation for long-term series forecasting”](https://proceedings.neurips.cc/paper/2021/hash/bcc0d400288793e8bdcd7c19a8ac0c2b-Abstract.html) Figure 1. Autoformer Architecture. *Figure 1. Autoformer Architecture.* ## 1. Autoformer ### `Autoformer` ```python theme={null} Autoformer( h, input_size, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, decoder_input_size_multiplier=0.5, hidden_size=128, dropout=0.05, factor=3, n_head=4, conv_hidden_size=32, activation="gelu", encoder_layers=2, decoder_layers=1, MovingAvg_window=25, loss=MAE(), valid_loss=None, max_steps=5000, learning_rate=0.0001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) Autoformer The Autoformer model tackles the challenge of finding reliable dependencies on intricate temporal patterns of long-horizon forecasting. The architecture has the following distinctive features: * In-built progressive decomposition in trend and seasonal components based on a moving average filter. * Auto-Correlation mechanism that discovers the period-based dependencies by calculating the autocorrelation and aggregating similar sub-series based on the periodicity. * Classic encoder-decoder proposed by Vaswani et al. (2017) with a multi-head attention mechanism. The Autoformer model utilizes a three-component approach to define its embedding: * It employs encoded autoregressive features obtained from a convolution network. * Absolute positional embeddings obtained from calendar features are utilized. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. Default -1 uses all history. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `decoder_input_size_multiplier` | [float](#float) | . | 0.5 | | `hidden_size` | [int](#int) | units of embeddings and encoders. | 128 | | `n_head` | [int](#int) | controls number of multi-head's attention. | 4 | | `dropout` | [float](#float) | dropout throughout Autoformer architecture. | 0.05 | | `factor` | [int](#int) | Probsparse attention factor. | 3 | | `conv_hidden_size` | [int](#int) | channels of the convolutional encoder. | 32 | | `activation` | [str](#str) | activation from \['ReLU', 'Softplus', 'Tanh', 'SELU', 'LeakyReLU', 'PReLU', 'Sigmoid', 'GELU']. | 'gelu' | | `encoder_layers` | [int](#int) | number of layers for the TCN encoder. | 2 | | `decoder_layers` | [int](#int) | number of layers for the MLP decoder. | 1 | | `MovingAvg_window` | [int](#int) | window size for the moving average filter. | 25 | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated validation loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 5000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.0001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Wu, Haixu, Jiehui Xu, Jianmin Wang, and Mingsheng Long. "Autoformer: Decomposition transformers with auto-correlation for long-term series forecasting"](https://proceedings.neurips.cc/paper/2021/hash/bcc0d400288793e8bdcd7c19a8ac0c2b-Abstract.html)
#### `Autoformer.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `Autoformer.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import Autoformer from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic, augment_calendar_df AirPassengersPanel, calendar_cols = augment_calendar_df(df=AirPassengersPanel, freq='M') Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = Autoformer(h=12, input_size=24, hidden_size = 16, conv_hidden_size = 32, n_head=2, loss=MAE(), futr_exog_list=calendar_cols, scaler_type='robust', learning_rate=1e-3, max_steps=300, val_check_steps=50, early_stop_patience_steps=2) nf = NeuralForecast( models=[model], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = nf.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) if model.loss.is_distribution_output: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['Autoformer-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['Autoformer-lo-90'][-12:].values, y2=plot_df['Autoformer-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.legend() plt.plot() else: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['Autoformer'], c='blue', label='Forecast') plt.legend() plt.grid() ``` ## 2. Auxiliary functions ### `Decoder` ```python theme={null} Decoder(layers, norm_layer=None, projection=None) ``` Bases: [Module](#torch.nn.Module) Autoformer decoder ### `DecoderLayer` ```python theme={null} DecoderLayer( self_attention, cross_attention, hidden_size, c_out, conv_hidden_size=None, MovingAvg=25, dropout=0.1, activation="relu", ) ``` Bases: [Module](#torch.nn.Module) Autoformer decoder layer with the progressive decomposition architecture ### `Encoder` ```python theme={null} Encoder(attn_layers, conv_layers=None, norm_layer=None) ``` Bases: [Module](#torch.nn.Module) Autoformer encoder ### `EncoderLayer` ```python theme={null} EncoderLayer( attention, hidden_size, conv_hidden_size=None, MovingAvg=25, dropout=0.1, activation="relu", ) ``` Bases: [Module](#torch.nn.Module) Autoformer encoder layer with the progressive decomposition architecture ### `LayerNorm` ```python theme={null} LayerNorm(channels) ``` Bases: [Module](#torch.nn.Module) Special designed layernorm for the seasonal part ### `AutoCorrelationLayer` ```python theme={null} AutoCorrelationLayer( correlation, hidden_size, n_head, d_keys=None, d_values=None ) ``` Bases: [Module](#torch.nn.Module) Auto Correlation Layer ### `AutoCorrelation` ```python theme={null} AutoCorrelation( mask_flag=True, factor=1, scale=None, attention_dropout=0.1, output_attention=False, ) ``` Bases: [Module](#torch.nn.Module) AutoCorrelation Mechanism with the following two phases: (1) period-based dependencies discovery (2) time delay aggregation This block can replace the self-attention family mechanism seamlessly. # BiTCN Source: https://nixtlaverse.nixtla.io/neuralforecast/models.bitcn.html BiTCN: Bidirectional Temporal Convolutional Network for forecasting. Parameter-efficient architecture with forward-backward encoding for probabilistic predictions. Bidirectional Temporal Convolutional Network (BiTCN) is a forecasting architecture based on two temporal convolutional networks (TCNs). The first network (‘forward’) encodes future covariates of the time series, whereas the second network (‘backward’) encodes past observations and covariates. This method allows to preserve the temporal information of sequence data, and is computationally more efficient than common RNN methods (LSTM, GRU, …). As compared to Transformer-based methods, BiTCN has a lower space complexity, i.e. it requires orders of magnitude less parameters. This model may be a good choice if you seek a small model (small amount of trainable parameters) with few hyperparameters to tune (only 2). **References** * [Olivier Sprangers, Sebastian Schelter, Maarten de Rijke (2023). Parameter-Efficient Deep Probabilistic Forecasting. International Journal of Forecasting 39, no. 1 (1 January 2023): 332–45. URL: https://doi.org/10.1016/j.ijforecast.2021.11.011.](https://doi.org/10.1016/j.ijforecast.2021.11.011) * [Shaojie Bai, Zico Kolter, Vladlen Koltun. (2018). An Empirical Evaluation of Generic Convolutional and Recurrent Networks for Sequence Modeling. Computing Research Repository, abs/1803.01271. URL: https://arxiv.org/abs/1803.01271.](https://arxiv.org/abs/1803.01271) * [van den Oord, A., Dieleman, S., Zen, H., Simonyan, K., Vinyals, O., Graves, A., Kalchbrenner, N., Senior, A. W., & Kavukcuoglu, K. (2016). Wavenet: A generative model for raw audio. Computing Research Repository, abs/1609.03499. URL: http://arxiv.org/abs/1609.03499. arXiv:1609.03499.](https://arxiv.org/abs/1609.03499) Figure 1. Visualization of a stack of dilated causal convolutional layers. *Figure 1. Visualization of a stack of dilated causal convolutional layers.* ## 1. BiTCN ### `BiTCN` ```python theme={null} BiTCN( h, input_size, hidden_size=16, dropout=0.5, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) BiTCN Bidirectional Temporal Convolutional Network (BiTCN) is a forecasting architecture based on two temporal convolutional networks (TCNs). The first network ('forward') encodes future covariates of the time series, whereas the second network ('backward') encodes past observations and covariates. This is a univariate model. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | considered autorregresive inputs (lags), y=\[1,2,3,4] input\_size=2 -> lags=\[1,2]. | *required* | | `hidden_size` | [int](#int) | units for the TCN's hidden state size. Default: 16. | 16 | | `dropout` | [float](#float) | dropout rate used for the dropout layers throughout the architecture. Default: 0.1. | 0.5 | | `futr_exog_list` | [list](#list) | future exogenous columns. | None | | `hist_exog_list` | [list](#list) | historic exogenous columns. | None | | `stat_exog_list` | [list](#list) | static exogenous columns. | None | | `cat_exog_list` | [list](#list) | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. Default: False. | False | | `loss` | [Module](#torch.nn.Module) | PyTorch module, instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | [Module](#torch.nn.Module) | PyTorch module, instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. Default: 1000. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). Default: 1e-3. | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. Default: -1. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. Default: -1. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. Default: 100. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. Default: 32. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. Default: None. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. Default: 1024. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. Default: 1024. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. Default: False. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). Default: 0.0. | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. Default: 1. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). Default: 'identity'. | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. Default: 1. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. Default: False. | False | | `alias` | [str](#str) | optional, Custom name of the model. Default: None. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Olivier Sprangers, Sebastian Schelter, Maarten de Rijke (2023). Parameter-Efficient Deep Probabilistic Forecasting. International Journal of Forecasting 39, no. 1 (1 January 2023): 333-345.](https://doi.org/10.1016/j.ijforecast.2021.11.011)
#### `BiTCN.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `BiTCN.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.losses.pytorch import GMM from neuralforecast.models import BiTCN from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test fcst = NeuralForecast( models=[ BiTCN(h=12, input_size=24, loss=GMM(n_components=7, level=[80,90]), max_steps=100, scaler_type='standard', futr_exog_list=['y_[lag12]'], hist_exog_list=None, stat_exog_list=['airline1'], windows_batch_size=2048, val_check_steps=10, early_stop_patience_steps=-1, ), ], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic) forecasts = fcst.predict(futr_df=Y_test_df) # Plot quantile predictions Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['BiTCN-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['BiTCN-lo-90'][-12:].values, y2=plot_df['BiTCN-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() ``` ## 2. Auxilary functions ### `TCNCell` ```python theme={null} TCNCell( in_channels, out_channels, kernel_size, padding, dilation, mode, groups, dropout, ) ``` Bases: [Module](#torch.nn.Module) Temporal Convolutional Network Cell, consisting of CustomConv1D modules. ### `CustomConv1d` ```python theme={null} CustomConv1d( in_channels, out_channels, kernel_size, padding=0, dilation=1, mode="backward", groups=1, ) ``` Bases: [Module](#torch.nn.Module) Forward- and backward looking Conv1D # DeepAR Source: https://nixtlaverse.nixtla.io/neuralforecast/models.deepar.html DeepAR: Probabilistic autoregressive RNN for forecasting. Uses Monte Carlo sampling with distribution outputs for uncertainty quantification in time series. The DeepAR model produces probabilistic forecasts based on an autoregressive recurrent neural network optimized on panel data using cross-learning. DeepAR obtains its forecast distribution uses a Markov Chain Monte Carlo sampler with the following conditional probability: $\mathbb{P}(\mathbf{y}_{[t+1:t+H]}|\;\mathbf{y}_{[:t]},\; \mathbf{x}^{(f)}_{[:t+H]},\; \mathbf{x}^{(s)})$ where $\mathbf{x}^{(s)}$ are static exogenous inputs, $\mathbf{x}^{(f)}_{[:t+H]}$ are future exogenous available at the time of the prediction. The predictions are obtained by transforming the hidden states $\mathbf{h}_{t}$ into predictive distribution parameters $\theta_{t}$, and then generating samples $\mathbf{\hat{y}}_{[t+1:t+H]}$ through Monte Carlo sampling trajectories. $$ \begin{align} \mathbf{h}_{t} &= \textrm{RNN}([\mathbf{y}_{t},\mathbf{x}^{(f)}_{t+1},\mathbf{x}^{(s)}], \mathbf{h}_{t-1})\\ \mathbf{\theta}_{t}&=\textrm{Linear}(\mathbf{h}_{t}) \\ \hat{y}_{t+1}&=\textrm{sample}(\;\mathrm{P}(y_{t+1}\;|\;\mathbf{\theta}_{t})\;) \end{align} $$ **References** * [David Salinas, Valentin Flunkert, Jan Gasthaus, Tim Januschowski (2020). “DeepAR: Probabilistic forecasting with autoregressive recurrent networks”. International Journal of Forecasting.](https://www.sciencedirect.com/science/article/pii/S0169207019301888) * [Alexander Alexandrov et. al (2020). “GluonTS: Probabilistic and Neural Time Series Modeling in Python”. Journal of Machine Learning Research.](https://www.jmlr.org/papers/v21/19-820.html) > **Exogenous Variables, Losses, and Parameters Availability** > > Given the sampling procedure during inference, DeepAR only supports > [`DistributionLoss`](./losses.pytorch.html#distributionloss) > as training loss. > > Note that DeepAR generates a non-parametric forecast distribution > using Monte Carlo. We use this sampling procedure also during > validation to make it closer to the inference procedure. Therefore, > only the > [`MQLoss`](./losses.pytorch.html#mqloss) > is available for validation. > > Aditionally, Monte Carlo implies that historic exogenous variables are > not available for the model. Figure 1. DeepAR model, during training the optimization signal comes from likelihood of observations, during inference a recurrent multi-step strategy is used to generate predictive distributions. *Figure 1. DeepAR model, during training the optimization signal comes from likelihood of observations, during inference a recurrent multi-step strategy is used to generate predictive distributions.* ## 1. DeepAR ### `DeepAR` ```python theme={null} DeepAR( h, input_size=-1, h_train=1, lstm_n_layers=2, lstm_hidden_size=128, lstm_dropout=0.1, decoder_hidden_layers=0, decoder_hidden_size=0, trajectory_samples=100, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", hist_exog_list=None, futr_exog_list=None, exclude_insample_y=False, loss=DistributionLoss( distribution="StudentT", level=[80, 90], return_params=False ), valid_loss=MAE(), max_steps=1000, learning_rate=0.001, num_lr_decays=3, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=-1, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) DeepAR **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. Default -1 uses 3 \* horizon | -1 | | `h_train` | [int](#int) | maximum sequence length for truncated train backpropagation. Default 1. | 1 | | `lstm_n_layers` | [int](#int) | number of LSTM layers. | 2 | | `lstm_hidden_size` | [int](#int) | LSTM hidden size. | 128 | | `lstm_dropout` | [float](#float) | LSTM dropout. | 0.1 | | `decoder_hidden_layers` | [int](#int) | number of decoder MLP hidden layers. Default: 0 for linear layer. | 0 | | `decoder_hidden_size` | [int](#int) | decoder MLP hidden size. Default: 0 for linear layer. | 0 | | `trajectory_samples` | [int](#int) | number of Monte Carlo trajectories during inference. | 100 | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `hist_exog_list` | str list | historic exogenous columns. | None | | `futr_exog_list` | str list | future exogenous columns. | None | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [DistributionLoss](#neuralforecast.losses.pytorch.DistributionLoss)(distribution='StudentT', level=\[80, 90], return\_params=False) | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | 3 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | -1 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [David Salinas, Valentin Flunkert, Jan Gasthaus, Tim Januschowski (2020). "DeepAR: Probabilistic forecasting with autoregressive recurrent networks". International Journal of Forecasting.](https://www.sciencedirect.com/science/article/pii/S0169207019301888) * [Alexander Alexandrov et. al (2020). "GluonTS: Probabilistic and Neural Time Series Modeling in Python". Journal of Machine Learning Research.](https://www.jmlr.org/papers/v21/19-820.html)
#### `DeepAR.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `DeepAR.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import DeepAR from neuralforecast.losses.pytorch import DistributionLoss, MQLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test nf = NeuralForecast( models=[DeepAR(h=12, input_size=24, lstm_n_layers=1, trajectory_samples=100, loss=DistributionLoss(distribution='StudentT', level=[80, 90], return_params=True), valid_loss=MQLoss(level=[80, 90]), learning_rate=0.005, stat_exog_list=['airline1'], futr_exog_list=['trend'], max_steps=100, val_check_steps=10, early_stop_patience_steps=-1, scaler_type='standard', enable_progress_bar=True, ), ], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) Y_hat_df = nf.predict(futr_df=Y_test_df) # Plot quantile predictions Y_hat_df = Y_hat_df.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['DeepAR-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['DeepAR-lo-90'][-12:].values, y2=plot_df['DeepAR-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() plt.plot() ``` ## 2. Auxiliary functions # DeepNPTS Source: https://nixtlaverse.nixtla.io/neuralforecast/models.deepnpts.html DeepNPTS: Deep Non-Parametric Time Series forecaster that samples from empirical distributions. Strong baseline for probabilistic forecasting tasks. Deep Non-Parametric Time Series Forecaster ([`DeepNPTS`](./models.deepnpts.html#deepnpts)) is a non-parametric baseline model for time-series forecasting. This model generates predictions by sampling from the empirical distribution according to a tunable strategy. This strategy is learned by exploiting the information across multiple related time series. This model provides a strong, simple baseline for time series forecasting. **References** * [Rangapuram, Syama Sundar, Jan Gasthaus, Lorenzo Stella, Valentin Flunkert, David Salinas, Yuyang Wang, and Tim Januschowski (2023). “Deep Non-Parametric Time Series Forecaster”. arXiv.](https://arxiv.org/abs/2312.14657) > **Losses** > > This implementation differs from the original work in that a weighted > sum of the empirical distribution is returned as forecast. Therefore, > it only supports point losses. ## DeepNPTS ### `DeepNPTS` ```python theme={null} DeepNPTS( h, input_size, hidden_size=32, batch_norm=True, dropout=0.1, n_layers=2, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, loss=MAE(), valid_loss=MAE(), max_steps=1000, learning_rate=0.001, num_lr_decays=3, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="standard", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) DeepNPTS Deep Non-Parametric Time Series Forecaster (`DeepNPTS`) is a baseline model for time-series forecasting. This model generates predictions by (weighted) sampling from the empirical distribution according to a learnable strategy. The strategy is learned by exploiting the information across multiple related time series. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `hidden_size` | [int](#int) | hidden size of dense layers. | 32 | | `batch_norm` | [bool](#bool) | if True, applies Batch Normalization after each dense layer in the network. | True | | `dropout` | [float](#float) | dropout. | 0.1 | | `n_layers` | [int](#int) | number of dense layers. | 2 | | `stat_exog_list` | [list](#list) | static exogenous columns. | None | | `hist_exog_list` | [list](#list) | historic exogenous columns. | None | | `futr_exog_list` | [list](#list) | future exogenous columns. | None | | `cat_exog_list` | [list](#list) | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | 3 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'standard' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Rangapuram, Syama Sundar, Jan Gasthaus, Lorenzo Stella, Valentin Flunkert, David Salinas, Yuyang Wang, and Tim Januschowski (2023). "Deep Non-Parametric Time Series Forecaster". arXiv.](https://arxiv.org/abs/2312.14657)
#### `DeepNPTS.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `DeepNPTS.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import DeepNPTS from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test nf = NeuralForecast( models=[DeepNPTS(h=12, input_size=24, stat_exog_list=['airline1'], futr_exog_list=['trend'], max_steps=1000, val_check_steps=10, early_stop_patience_steps=3, scaler_type='robust', enable_progress_bar=True), ], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) Y_hat_df = nf.predict(futr_df=Y_test_df) # Plot quantile predictions Y_hat_df = Y_hat_df.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['DeepNPTS'], c='red', label='mean') plt.grid() plt.plot() ``` # Dilated RNN Source: https://nixtlaverse.nixtla.io/neuralforecast/models.dilated_rnn.html Dilated RNN: Recurrent neural network with dilated skip connections for modeling long sequences. Addresses vanishing gradients and improves computational efficiency. The Dilated Recurrent Neural Network ([`DilatedRNN`](./models.dilated_rnn.html#dilatedrnn)) addresses common challenges of modeling long sequences like vanishing gradients, computational efficiency, and improved model flexibility to model complex relationships while maintaining its parsimony. The [`DilatedRNN`](./models.dilated_rnn.html#dilatedrnn) builds a deep stack of RNN layers using skip conditions on the temporal and the network’s depth dimensions. The temporal dilated recurrent skip connections offer the capability to focus on multi-resolution inputs.The predictions are obtained by transforming the hidden states into contexts $\mathbf{c}_{[t+1:t+H]}$, that are decoded and adapted into $\mathbf{\hat{y}}_{[t+1:t+H],[q]}$ through MLPs. where $\mathbf{h}_{t}$, is the hidden state for time $t$, $\mathbf{y}_{t}$ is the input at time $t$ and $\mathbf{h}_{t-1}$ is the hidden state of the previous layer at $t-1$, $\mathbf{x}^{(s)}$ are static exogenous inputs, $\mathbf{x}^{(h)}_{t}$ historic exogenous, $\mathbf{x}^{(f)}_{[:t+H]}$ are future exogenous available at the time of the prediction. **References** * [Shiyu Chang, et al. “Dilated Recurrent Neural Networks”.](https://arxiv.org/abs/1710.02224) * [Yao Qin, et al. “A Dual-Stage Attention-Based recurrent neural network for time series prediction”.](https://arxiv.org/abs/1704.02971) * [Kashif Rasul, et al. “Zalando Research: PyTorch Dilated Recurrent Neural Networks”.](https://arxiv.org/abs/1710.02224) Figure 1. Three layer DilatedRNN with dilation 1, 2, 4. *Figure 1. Three layer DilatedRNN with dilation 1, 2, 4.* ## Dilated RNN ### `DilatedRNN` ```python theme={null} DilatedRNN( h, input_size=-1, inference_input_size=None, cell_type="LSTM", dilations=[[1, 2], [4, 8]], encoder_hidden_size=128, context_size=10, decoder_hidden_size=128, decoder_layers=2, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=3, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=128, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="robust", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) DilatedRNN **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. Default -1 uses 3 \* horizon | -1 | | `inference_input_size` | [int](#int) | maximum sequence length for truncated inference. Default None uses input\_size history. | None | | `cell_type` | [str](#str) | type of RNN cell to use. Options: 'GRU', 'RNN', 'LSTM', 'ResLSTM', 'AttentiveLSTM'. | 'LSTM' | | `dilations` | int list | dilations between layers. | \[\[1, 2], \[4, 8]] | | `encoder_hidden_size` | [int](#int) | units for the RNN's hidden state size. | 128 | | `context_size` | [int](#int) | size of context vector for each timestamp on the forecasting window. | 10 | | `decoder_hidden_size` | [int](#int) | size of hidden layer for the MLP decoder. | 128 | | `decoder_layers` | [int](#int) | number of layers for the MLP decoder. | 2 | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | 3 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 128 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#typing.List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'robust' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [DilatedRNN: Dilated Recurrent Neural Networks for Time-Series Forecasting](https://arxiv.org/pdf/1710.02224)
#### `DilatedRNN.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `DilatedRNN.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import DilatedRNN from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test fcst = NeuralForecast( models=[DilatedRNN(h=12, input_size=-1, loss=DistributionLoss(distribution='Normal', level=[80, 90]), scaler_type='robust', encoder_hidden_size=100, max_steps=200, futr_exog_list=['y_[lag12]'], hist_exog_list=None, stat_exog_list=['airline1'], ) ], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic) forecasts = fcst.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['DilatedRNN-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['DilatedRNN-lo-90'][-12:].values, y2=plot_df['DilatedRNN-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() plt.plot() ``` # DLinear Source: https://nixtlaverse.nixtla.io/neuralforecast/models.dlinear.html DLinear model: Simple, fast linear architecture with trend-seasonality decomposition for accurate long-horizon time series forecasting with minimal complexity. DLinear is a simple and fast yet accurate time series forecasting model for long-horizon forecasting. The architecture has the following distinctive features: - Uses Autoformmer’s trend and seasonality decomposition. - Simple linear layers for trend and seasonality component. **References** * [Zeng, Ailing, et al. “Are transformers effective for time series forecasting?.” Proceedings of the AAAI conference on artificial intelligence. Vol. 37. No. 9. 2023.”](https://ojs.aaai.org/index.php/AAAI/article/view/26317) Figure 1. DLinear Architecture. *Figure 1. DLinear Architecture.* ## 1. DLinear ### `DLinear` ```python theme={null} DLinear( h, input_size, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, exclude_insample_y=False, moving_avg_window=25, loss=MAE(), valid_loss=None, max_steps=5000, learning_rate=0.0001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) DLinear **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. | *required* | | `stat_exog_list` | str list | static exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `futr_exog_list` | str list | future exogenous columns. | None | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `moving_avg_window` | [int](#int) | window size for trend-seasonality decomposition. Should be uneven. | 25 | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 5000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.0001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified optimizer. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified lr\_scheduler. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Zeng, Ailing, et al. "Are transformers effective for time series forecasting?." Proceedings of the AAAI conference on artificial intelligence. Vol. 37. No. 9. 2023."](https://ojs.aaai.org/index.php/AAAI/article/view/26317)
#### `DLinear.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `DLinear.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import DLinear from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic, augment_calendar_df AirPassengersPanel, calendar_cols = augment_calendar_df(df=AirPassengersPanel, freq='M') Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = DLinear(h=12, input_size=24, loss=MAE(), scaler_type='robust', learning_rate=1e-3, max_steps=500, val_check_steps=50, early_stop_patience_steps=2) nf = NeuralForecast( models=[model], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = nf.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) if model.loss.is_distribution_output: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['DLinear-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['DLinear-lo-90'][-12:].values, y2=plot_df['DLinear-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.legend() plt.plot() else: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['DLinear'], c='blue', label='Forecast') plt.legend() plt.grid() ``` ## 2. Auxilary Functions ### `SeriesDecomp` ```python theme={null} SeriesDecomp(kernel_size) ``` Bases: [Module](#torch.nn.Module) Series decomposition block ### `MovingAvg` ```python theme={null} MovingAvg(kernel_size, stride) ``` Bases: [Module](#torch.nn.Module) Moving average block to highlight the trend of time series # FEDformer Source: https://nixtlaverse.nixtla.io/neuralforecast/models.fedformer.html FEDformer: Frequency Enhanced Decomposition transformer for long-term forecasting using Fourier transform and sparse attention in frequency domain. The FEDformer model tackles the challenge of finding reliable dependencies on intricate temporal patterns of long-horizon forecasting. The architecture has the following distinctive features: * In-built progressive decomposition in trend and seasonal components based on a moving average filter. * Frequency Enhanced Block and Frequency Enhanced Attention to perform attention in the sparse representation on basis such as Fourier transform. * Classic encoder-decoder proposed by Vaswani et al. (2017) with a multi-head attention mechanism. The FEDformer model utilizes a three-component approach to define its embedding: * It employs encoded autoregressive features obtained from a convolution network. * Absolute positional embeddings obtained from calendar features are utilized. **References** * [Zhou, Tian, Ziqing Ma, Qingsong Wen, Xue Wang, Liang Sun, and Rong Jin.. “FEDformer: Frequency enhanced decomposed transformer for long-term series forecasting”](https://proceedings.mlr.press/v162/zhou22g.html) Figure 1. FEDformer Architecture. *Figure 1. FEDformer Architecture.* ## 1. FEDformer ### `FEDformer` ```python theme={null} FEDformer( h, input_size, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", decoder_input_size_multiplier=0.5, version="Fourier", modes=64, mode_select="random", hidden_size=128, dropout=0.05, n_head=8, conv_hidden_size=32, activation="gelu", encoder_layers=2, decoder_layers=1, MovingAvg_window=25, loss=MAE(), valid_loss=None, max_steps=5000, learning_rate=0.0001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) FEDformer The FEDformer model tackles the challenge of finding reliable dependencies on intricate temporal patterns of long-horizon forecasting. The architecture has the following distinctive features: * In-built progressive decomposition in trend and seasonal components based on a moving average filter. * Frequency Enhanced Block and Frequency Enhanced Attention to perform attention in the sparse representation on basis such as Fourier transform. * Classic encoder-decoder proposed by Vaswani et al. (2017) with a multi-head attention mechanism. The FEDformer model utilizes a three-component approach to define its embedding: * It employs encoded autoregressive features obtained from a convolution network. * Absolute positional embeddings obtained from calendar features are utilized. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. | *required* | | `stat_exog_list` | [List](#List)\[[str](#str)] | static exogenous columns. | None | | `hist_exog_list` | [List](#List)\[[str](#str)] | historic exogenous columns. | None | | `futr_exog_list` | [List](#List)\[[str](#str)] | future exogenous columns. | None | | `cat_exog_list` | [List](#List)\[[str](#str)] | exogenous columns (from `hist_exog_list` / `futr_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `decoder_input_size_multiplier` | [float](#float) | multiplier for the input size of the decoder. | 0.5 | | `version` | [str](#str) | version of the model. | 'Fourier' | | `modes` | [int](#int) | number of modes for the Fourier block. | 64 | | `mode_select` | [str](#str) | method to select the modes for the Fourier block. | 'random' | | `hidden_size` | [int](#int) | units of embeddings and encoders. | 128 | | `dropout` | [float](#float) | dropout throughout Autoformer architecture. | 0.05 | | `n_head` | [int](#int) | controls number of multi-head's attention. | 8 | | `conv_hidden_size` | [int](#int) | channels of the convolutional encoder. | 32 | | `activation` | [str](#str) | activation from \['ReLU', 'Softplus', 'Tanh', 'SELU', 'LeakyReLU', 'PReLU', 'Sigmoid', 'GELU']. | 'gelu' | | `encoder_layers` | [int](#int) | number of layers for the TCN encoder. | 2 | | `decoder_layers` | [int](#int) | number of layers for the MLP decoder. | 1 | | `MovingAvg_window` | [int](#int) | window size for the moving average filter. | 25 | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated validation loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 5000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.0001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified optimizer. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified lr\_scheduler. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Tian Zhou, et al. "FEDformer: Frequency Enhanced Decomposed Transformer for Long-term Series Forecasting" Proceedings of the AAAI conference on artificial intelligence. Vol. 37. No. 9. 2023."](https://arxiv.org/abs/2201.12740)
#### `FEDformer.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `FEDformer.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import FEDformer from neuralforecast.utils import AirPassengersPanel, augment_calendar_df AirPassengersPanel, calendar_cols = augment_calendar_df(df=AirPassengersPanel, freq='M') Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = FEDformer(h=12, input_size=24, modes=64, hidden_size=64, conv_hidden_size=128, n_head=8, loss=MAE(), futr_exog_list=calendar_cols, scaler_type='robust', learning_rate=1e-3, max_steps=500, batch_size=2, windows_batch_size=32, val_check_steps=50, early_stop_patience_steps=2) nf = NeuralForecast( models=[model], freq='ME', ) nf.fit(df=Y_train_df, static_df=None, val_size=12) forecasts = nf.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) if model.loss.is_distribution_output: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['FEDformer-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['FEDformer-lo-90'][-12:].values, y2=plot_df['FEDformer-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.legend() plt.plot() else: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['FEDformer'], c='blue', label='Forecast') plt.legend() plt.grid() ``` ## 2. Auxiliary functions ### `AutoCorrelationLayer` ```python theme={null} AutoCorrelationLayer( correlation, hidden_size, n_head, d_keys=None, d_values=None ) ``` Bases: [Module](#torch.nn.Module) Auto Correlation Layer ### `LayerNorm` ```python theme={null} LayerNorm(channels) ``` Bases: [Module](#torch.nn.Module) Special designed layernorm for the seasonal part ### `Decoder` ```python theme={null} Decoder(layers, norm_layer=None, projection=None) ``` Bases: [Module](#torch.nn.Module) FEDformer decoder ### `DecoderLayer` ```python theme={null} DecoderLayer( self_attention, cross_attention, hidden_size, c_out, conv_hidden_size=None, MovingAvg=25, dropout=0.1, activation="relu", ) ``` Bases: [Module](#torch.nn.Module) FEDformer decoder layer with the progressive decomposition architecture ### `Encoder` ```python theme={null} Encoder(attn_layers, conv_layers=None, norm_layer=None) ``` Bases: [Module](#torch.nn.Module) FEDformer encoder ### `EncoderLayer` ```python theme={null} EncoderLayer( attention, hidden_size, conv_hidden_size=None, MovingAvg=25, dropout=0.1, activation="relu", ) ``` Bases: [Module](#torch.nn.Module) FEDformer encoder layer with the progressive decomposition architecture ### `FourierCrossAttention` ```python theme={null} FourierCrossAttention( in_channels, out_channels, seq_len_q, seq_len_kv, modes=64, mode_select_method="random", activation="tanh", policy=0, ) ``` Bases: [Module](#torch.nn.Module) Fourier Cross Attention layer ### `FourierBlock` ```python theme={null} FourierBlock( in_channels, out_channels, seq_len, modes=0, mode_select_method="random" ) ``` Bases: [Module](#torch.nn.Module) Fourier block #### `FourierBlock.compl_mul1d` ```python theme={null} compl_mul1d(input, weights) ``` #### `FourierBlock.forward` ```python theme={null} forward(q, k, v, mask) ``` #### `FourierBlock.index` ```python theme={null} index = get_frequency_modes( seq_len, modes=modes, mode_select_method=mode_select_method ) ``` #### `FourierBlock.scale` ```python theme={null} scale = 1 / (in_channels * out_channels) ``` #### `FourierBlock.weights1` ```python theme={null} weights1 = nn.Parameter( self.scale * torch.rand( 8, in_channels // 8, out_channels // 8, len(self.index), dtype=(torch.cfloat), ) ) ``` ### `get_frequency_modes` ```python theme={null} get_frequency_modes(seq_len, modes=64, mode_select_method='random') ```
Get modes on frequency domain 'random' for sampling randomly 'else' for sampling the lowest modes;
# GRU Source: https://nixtlaverse.nixtla.io/neuralforecast/models.gru.html GRU: Gated Recurrent Unit model for sequential forecasting. Improves upon LSTM with simplified gating mechanism and MLP decoder for time series predictions. Cho et. al proposed the Gated Recurrent Unit ([`GRU`](./models.gru.html#gru)) to improve on LSTM and Elman cells. The predictions at each time are given by a MLP decoder. This architecture follows closely the original Multi Layer Elman [`RNN`](./models.rnn.html#rnn) with the main difference being its use of the GRU cells. The predictions are obtained by transforming the hidden states into contexts $\mathbf{c}_{[t+1:t+H]}$, that are decoded and adapted into $\mathbf{\hat{y}}_{[t+1:t+H],[q]}$ through MLPs. where $\mathbf{h}_{t}$, is the hidden state for time $t$, $\mathbf{y}_{t}$ is the input at time $t$ and $\mathbf{h}_{t-1}$ is the hidden state of the previous layer at $t-1$, $\mathbf{x}^{(s)}$ are static exogenous inputs, $\mathbf{x}^{(h)}_{t}$ historic exogenous, $\mathbf{x}^{(f)}_{[:t+H]}$ are future exogenous available at the time of the prediction. **References** * [Junyoung Chung, Caglar Gulcehre, KyungHyun Cho, Yoshua Bengio (2014). “Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling”.](https://arxiv.org/abs/1412.3555) * [Kyunghyun Cho, Bart van Merrienboer, Dzmitry Bahdanau, Yoshua Bengio (2014). “On the Properties of Neural Machine Translation: Encoder-Decoder Approaches”.](https://arxiv.org/abs/1409.1259) Figure 1. Gated Recurrent Unit Cell. *Figure 1. Gated Recurrent Unit Cell.* ## GRU ### `GRU` ```python theme={null} GRU( h, input_size=-1, inference_input_size=None, h_train=1, encoder_n_layers=2, encoder_hidden_size=200, encoder_activation=None, encoder_bias=True, encoder_dropout=0.0, context_size=None, decoder_hidden_size=128, decoder_layers=2, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, recurrent=False, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=128, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="robust", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) GRU Multi Layer Recurrent Network with Gated Units (GRU), and MLP decoder. The network has non-linear activation functions, it is trained using ADAM stochastic gradient descent. The network accepts static, historic and future exogenous data, flattens the inputs. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. Default -1 uses 3 \* horizon. | -1 | | `inference_input_size` | [int](#int) | maximum sequence length for truncated inference. Default None uses input\_size history. | None | | `h_train` | [int](#int) | maximum sequence length for truncated train backpropagation. Default 1. | 1 | | `encoder_n_layers` | [int](#int) | number of layers for the GRU. | 2 | | `encoder_hidden_size` | [int](#int) | units for the GRU's hidden state size. | 200 | | `encoder_activation` | [Optional](#typing.Optional)\[[str](#str)] | Deprecated. Activation function in GRU is frozen in PyTorch. | None | | `encoder_bias` | [bool](#bool) | whether or not to use biases b\_ih, b\_hh within GRU units. | True | | `encoder_dropout` | [float](#float) | dropout regularization applied to GRU outputs. | 0.0 | | `context_size` | [Optional](#typing.Optional)\[[int](#int)] | deprecated. | None | | `decoder_hidden_size` | [int](#int) | size of hidden layer for the MLP decoder. | 128 | | `decoder_layers` | [int](#int) | number of layers for the MLP decoder. | 2 | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | whether to exclude the target variable from the input. | False | | `recurrent` | [bool](#bool) | whether to produce forecasts recursively (True) or direct (False). | False | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 128 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'robust' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified optimizer. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified lr\_scheduler. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Junyoung Chung, Caglar Gulcehre, KyungHyun Cho, Yoshua Bengio (2014). "Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling".](https://arxiv.org/abs/1412.3555) * [Kyunghyun Cho, Bart van Merrienboer, Dzmitry Bahdanau, Yoshua Bengio (2014). "On the Properties of Neural Machine Translation: Encoder-Decoder Approaches".](https://arxiv.org/abs/1409.1259)
#### `GRU.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `GRU.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast # from neuralforecast.models import GRU from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test fcst = NeuralForecast( models=[GRU(h=12, input_size=24, loss=DistributionLoss(distribution='Normal', level=[80, 90]), scaler_type='robust', encoder_n_layers=2, encoder_hidden_size=128, decoder_hidden_size=128, decoder_layers=2, max_steps=200, futr_exog_list=None, hist_exog_list=['y_[lag12]'], stat_exog_list=['airline1'], ) ], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic) forecasts = fcst.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['GRU-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['GRU-lo-90'][-12:].values, y2=plot_df['GRU-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() plt.plot() ``` # HINT Source: https://nixtlaverse.nixtla.io/neuralforecast/models.hint.html HINT: Hierarchical Mixture Networks for coherent probabilistic forecasting. Combines neural architectures with reconciliation for hierarchical time series. The Hierarchical Mixture Networks (HINT) are a highly modular framework that combines SoTA neural forecast architectures with task-specialized mixture probability and advanced hierarchical reconciliation strategies. This powerful combination allows HINT to produce accurate and coherent probabilistic forecasts. HINT’s incorporates a `TemporalNorm` module into any neural forecast architecture, the module normalizes inputs into the network’s non-linearities operating range and recomposes its output’s scales through a global skip connection, improving accuracy and training robustness. HINT ensures the forecast coherence via bootstrap sample reconciliation that restores the aggregation constraints into its base samples. **References** * [Kin G. Olivares, David Luo, Cristian Challu, Stefania La Vattiata, Max Mergenthaler, Artur Dubrawski (2023). “HINT: Hierarchical Mixture Networks For Coherent Probabilistic Forecasting”. Neural Information Processing Systems, submitted. Working Paper version available at arxiv.](https://arxiv.org/abs/2305.07089) * [Kin G. Olivares, O. Nganba Meetei, Ruijun Ma, Rohan Reddy, Mengfei Cao, Lee Dicker (2022).”Probabilistic Hierarchical Forecasting with Deep Poisson Mixtures”. International Journal Forecasting, accepted paper available at arxiv.](https://arxiv.org/pdf/2110.13179.pdf) * [Kin G. Olivares, Federico Garza, David Luo, Cristian Challu, Max Mergenthaler, Souhaib Ben Taieb, Shanika Wickramasuriya, and Artur Dubrawski (2022). “HierarchicalForecast: A reference framework for hierarchical forecasting in python”. Journal of Machine Learning Research, submitted, abs/2207.03517, 2022b.](https://arxiv.org/abs/2207.03517) Figure 1. Hierarchical Mixture Networks (HINT). *Figure 1. Hierarchical Mixture Networks (HINT).* ## 1. HINT ### `HINT` ```python theme={null} HINT(h, S, model, reconciliation, alias=None) ``` HINT The Hierarchical Mixture Networks (HINT) are a highly modular framework that combines SoTA neural forecast architectures with a task-specialized mixture probability and advanced hierarchical reconciliation strategies. This powerful combination allows HINT to produce accurate and coherent probabilistic forecasts. HINT's incorporates a `TemporalNorm` module into any neural forecast architecture, the module normalizes inputs into the network's non-linearities operating range and recomposes its output's scales through a global skip connection, improving accuracy and training robustness. HINT ensures the forecast coherence via bootstrap sample reconciliation that restores the aggregation constraints into its base samples.
Available reconciliations * BottomUp * MinTraceOLS * MinTraceWLS
* Identity **Parameters:** | Name | Type | Description | Default | | ---------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `model` | NeuralForecast model | Instantiated model class from [architecture collection](./models.html). | *required* | | `S` | [ndarray](#numpy.ndarray) | Dumming matrix of size (`base`, `bottom`) see HierarchicalForecast's [aggregate method](../hierarchicalforecast/utils.html#aggregate). | *required* | | `reconciliation` | [str](#str) | HINT's reconciliation method from \['BottomUp', 'MinTraceOLS', 'MinTraceWLS']. | *required* | | `alias` | [str](#str) | Custom name of the model. | None | #### `HINT.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` HINT.fit HINT trains on the entire hierarchical dataset, by minimizing a composite log likelihood objective. HINT framework integrates `TemporalNorm` into the neural forecast architecture for a scale-decoupled optimization that robustifies cross-learning the hierarchy's series scales. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | ------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset` see details [here](./tsdataset.html) | *required* | | `val_size` | [int](#int) | size of the validation set, (default 0). | 0 | | `test_size` | [int](#int) | size of the test set, (default 0). | 0 | | `random_seed` | [int](#int) | random seed for the prediction. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------- | | `self` | | A fitted base `NeuralForecast` model. | #### `HINT.predict` ```python theme={null} predict(dataset, step_size=1, random_seed=None, **data_module_kwargs) ``` HINT.predict After fitting a base model on the entire hierarchical dataset. HINT restores the hierarchical aggregation constraints using bootstrapped sample reconciliation. **Parameters:** | Name | Type | Description | Default | | -------------- | ---------------------------------------------------- | ------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset` see details [here](./tsdataset.html) | *required* | | `step_size` | [int](#int) | steps between sequential predictions, (default 1). | 1 | | `random_seed` | [int](#int) | random seed for the prediction. | None | | `**data_kwarg` | | additional parameters for the dataset module. | *required* | **Returns:** | Name | Type | Description | | ------- | ---- | ------------------------------------------------ | | `y_hat` | | numpy predictions of the `NeuralForecast` model. | ### Usage Example In this example we will use HINT for the hierarchical forecast task, a multivariate regression problem with aggregation constraints. The aggregation constraints can be compactcly represented by the summing matrix $\mathbf{S}_{[i][b]}$, the Figure belows shows an example. In this example we will make coherent predictions for the TourismL dataset. Outline: 1. Import packages 2. Load hierarchical dataset 3. Fit and Predict HINT 4. Forecast Plot ```python theme={null} import matplotlib.pyplot as plt from neuralforecast.losses.pytorch import GMM, sCRPS from datasetsforecast.hierarchical import HierarchicalData # Auxiliary sorting def sort_df_hier(Y_df, S_df): # NeuralForecast core, sorts unique_id lexicographically # by default, this class matches S_df and Y_hat_df order. Y_df.unique_id = Y_df.unique_id.astype('category') Y_df.unique_id = Y_df.unique_id.cat.set_categories(S_df.index) Y_df = Y_df.sort_values(by=['unique_id', 'ds']) return Y_df # Load TourismSmall dataset horizon = 12 Y_df, S_df, tags = HierarchicalData.load('./data', 'TourismLarge') Y_df['ds'] = pd.to_datetime(Y_df['ds']) Y_df = sort_df_hier(Y_df, S_df) level = [80,90] # Instantiate HINT # BaseNetwork + Distribution + Reconciliation nhits = NHITS(h=horizon, input_size=24, loss=GMM(n_components=10, level=level), max_steps=2000, early_stop_patience_steps=10, val_check_steps=50, scaler_type='robust', learning_rate=1e-3, valid_loss=sCRPS(level=level)) model = HINT(h=horizon, S=S_df.values, model=nhits, reconciliation='BottomUp') # Fit and Predict nf = NeuralForecast(models=[model], freq='MS') Y_hat_df = nf.cross_validation(df=Y_df, val_size=12, n_windows=1) Y_hat_df = Y_hat_df.reset_index() ``` ```python theme={null} # Plot coherent probabilistic forecast unique_id = 'TotalAll' Y_plot_df = Y_df[Y_df.unique_id==unique_id] plot_df = Y_hat_df[Y_hat_df.unique_id==unique_id] plot_df = Y_plot_df.merge(plot_df, on=['ds', 'unique_id'], how='left') n_years = 5 plt.plot(plot_df['ds'][-12*n_years:], plot_df['y_x'][-12*n_years:], c='black', label='True') plt.plot(plot_df['ds'][-12*n_years:], plot_df['HINT'][-12*n_years:], c='purple', label='mean') plt.plot(plot_df['ds'][-12*n_years:], plot_df['HINT-median'][-12*n_years:], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12*n_years:], y1=plot_df['HINT-lo-90'][-12*n_years:].values, y2=plot_df['HINT-hi-90'][-12*n_years:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() plt.plot() ``` ## 2. Reconciliation Methods ### `get_identity_P` ```python theme={null} get_identity_P(S) ``` ### `get_bottomup_P` ```python theme={null} get_bottomup_P(S) ``` BottomUp Reconciliation Matrix. Creates BottomUp hierarchical "projection" matrix is defined as: ```math theme={null} \mathbf{P}_{\text{BU}} = [\mathbf{0}_{\mathrm{[b],[a]}}\;|\;\mathbf{I}_{\mathrm{[b][b]}}] ``` **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------- | ------------------------------------------ | ---------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | **Returns:** | Type | Description | | ------------------------------------------------------------- | ----------- | | np.ndarray: Reconciliation matrix of size (`bottom`, `base`). | |
References * [Orcutt, G.H., Watts, H.W., & Edwards, J.B.(1968). "Data aggregation and information loss". The American Economic Review, 58 , 773(787)](http://www.jstor.org/stable/1815532).
### `get_mintrace_ols_P` ```python theme={null} get_mintrace_ols_P(S) ``` MinTraceOLS Reconciliation Matrix. Creates MinTraceOLS reconciliation matrix as proposed by Wickramasuriya et al. ```math theme={null} \mathbf{P}_{\text{MinTraceOLS}}=\left(\mathbf{S}^{\intercal}\mathbf{S}\right)^{-1}\mathbf{S}^{\intercal} ``` **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------- | ------------------------------------------ | ---------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | **Returns:** | Type | Description | | ------------------------------------------------------------- | ----------- | | np.ndarray: Reconciliation matrix of size (`bottom`, `base`). | |
References * [Wickramasuriya, S.L., Turlach, B.A. & Hyndman, R.J. (2020). "Optimal non-negative forecast reconciliation". Stat Comput 30, 1167-1182, https://doi.org/10.1007/s11222-020-09930-0](https://robjhyndman.com/publications/nnmint/).
### `get_mintrace_wls_P` ```python theme={null} get_mintrace_wls_P(S) ``` MinTraceOLS Reconciliation Matrix. Creates MinTraceOLS reconciliation matrix as proposed by Wickramasuriya et al. Depending on a weighted GLS estimator and an estimator of the covariance matrix of the coherency errors $\\mathbf{W}\_{h}$. ```math theme={null} \mathbf{W}_{h} = \mathrm{Diag}(\mathbf{S} \mathbb{1}_{[b]}) ``` ```math theme={null} \mathbf{P}_{\text{MinTraceWLS}}=\left(\mathbf{S}^{\intercal}\mathbf{W}_{h}\mathbf{S}\right)^{-1} \mathbf{S}^{\intercal}\mathbf{W}^{-1}_{h} ``` **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------- | ------------------------------------------ | ---------- | | `S` | [ndarray](#numpy.ndarray) | Summing matrix of size (`base`, `bottom`). | *required* | **Returns:** | Type | Description | | ------------------------------------------------------------- | ----------- | | np.ndarray: Reconciliation matrix of size (`bottom`, `base`). | |
References * [Wickramasuriya, S.L., Turlach, B.A. & Hyndman, R.J. (2020). "Optimal non-negative forecast reconciliation". Stat Comput 30, 1167-1182, https://doi.org/10.1007/s11222-020-09930-0](https://robjhyndman.com/publications/nnmint/).
# Automatic Forecasting Source: https://nixtlaverse.nixtla.io/neuralforecast/models.html AutoModel classes for NeuralForecast hyperparameter optimization. Automated grid search, Bayesian optimization with Ray Tune for 34 forecasting architectures. ## 1. Introduction All `NeuralForecast` models work out of the box with sensible default parameters. However, to achieve optimal forecasting performance on your specific dataset, hyperparameter optimization is highly recommended. **Hyperparameter optimization** is the process of automatically finding the best configuration for a model by systematically exploring different combinations of parameters such as learning rate, hidden layer sizes, number of layers, and other architectural choices. Unlike model parameters that are learned during training, hyperparameters must be set before training begins. NeuralForecast provides `AutoModel` classes that automate this optimization process. Each `AutoModel` wraps a corresponding forecasting model and uses techniques like grid search, random search, or Bayesian optimization to explore the hyperparameter space and identify the configuration that minimizes validation loss. ## BaseAuto Class All `AutoModel` classes inherit from `BaseAuto`, which provides a unified interface for hyperparameter optimization. `BaseAuto` handles the complete optimization workflow: 1. **Search Space Definition**: Defines which hyperparameters to explore and their ranges 2. **Temporal Cross-Validation**: Splits data temporally to avoid look-ahead bias 3. **Training & Evaluation**: Runs multiple trials with different hyperparameter configurations 4. **Model Selection**: Selects the configuration with the best validation performance 5. **Refitting**: Trains the final model with optimal hyperparameters The optimization process uses temporal cross-validation where the validation set sequentially precedes the test set. This ensures that hyperparameter selection is based on realistic forecasting scenarios. The validation loss guides the selection process, so it's important that the validation period is representative of future forecasting conditions. ### `BaseAuto` ```python theme={null} BaseAuto( cls_model, h, loss, valid_loss, config, search_alg=BasicVariantGenerator(random_state=1), num_samples=10, time_budget=None, refit_with_val=False, verbose=False, alias=None, backend="ray", callbacks=None, ray_options=None, optuna_options=None, cpus=None, gpus=None, ) ``` Bases: [LightningModule](#pytorch_lightning.LightningModule) Class for Automatic Hyperparameter Optimization, it builds on top of `ray` to give access to a wide variety of hyperparameter optimization tools ranging from classic grid search, to Bayesian optimization and HyperBand algorithm. The validation loss to be optimized is defined by the `config['loss']` dictionary value, the config also contains the rest of the hyperparameter search space. It is important to note that the success of this hyperparameter optimization heavily relies on a strong correlation between the validation and test periods. **Parameters:** | Name | Type | Description | Default | | ---------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | | `cls_model` | PyTorch/PyTorchLightning model | See `neuralforecast.models` [collection here](./models.html). | *required* | | `h` | [int](#int) | Forecast horizon | *required* | | `loss` | PyTorch module | Instantiated train loss class from [losses collection](./losses.pytorch.html). | *required* | | `valid_loss` | PyTorch module | Instantiated valid loss class from [losses collection](./losses.pytorch.html). | *required* | | `config` | [dict](#dict) or [callable](#callable) | Dictionary with ray.tune defined search space or function that takes an optuna trial and returns a configuration dict. The config must include every parameter of the underlying model that has no default value (e.g. `input_size`, and `n_series` for multivariate models), either as a fixed value or as a search variable. `h`, `loss`, and `valid_loss` are injected automatically and must not be set in `config`. | *required* | | `search_alg` | ray.tune.search variant or optuna.sampler | For ray see [https://docs.ray.io/en/latest/tune/api\_docs/suggestion.html](https://docs.ray.io/en/latest/tune/api_docs/suggestion.html) For optuna see [https://optuna.readthedocs.io/en/stable/reference/samplers/index.html](https://optuna.readthedocs.io/en/stable/reference/samplers/index.html). | [BasicVariantGenerator](#ray.tune.search.basic_variant.BasicVariantGenerator)(random\_state=1) | | `num_samples` | [int](#int) | Number of hyperparameter optimization steps/samples. | 10 | | `time_budget` | [int](#int) | Time budget in seconds for the hyperparameter search. | None | | `refit_with_val` | [bool](#bool) | Refit of best model should preserve val\_size. | False | | `verbose` | [bool](#bool) | Track progress. | False | | `alias` | [str](#str) | Custom name of the model. | None | | `backend` | [str](#str) | Backend to use for searching the hyperparameter space, can be either 'ray' or 'optuna'. | 'ray' | | `callbacks` | list of callable | List of functions to call during the optimization process. ray reference: [https://docs.ray.io/en/latest/tune/tutorials/tune-metrics.html](https://docs.ray.io/en/latest/tune/tutorials/tune-metrics.html) optuna reference: [https://optuna.readthedocs.io/en/stable](https://optuna.readthedocs.io/en/stable) | None | | `ray_options` | [RayOptions](#neuralforecast.common._base_auto.RayOptions) | Container for Ray-only options. See `RayOptions` for the supported fields (`run_config`, `scheduler`, `cpus`, `gpus`). Only used with `backend='ray'`. | None | | `optuna_options` | [OptunaOptions](#neuralforecast.common._base_auto.OptunaOptions) | Container for Optuna-only options. See `OptunaOptions` for the supported fields (`study_kwargs`, `create_study_kwargs`). Only used with `backend='optuna'`. | None | | `cpus` | | No longer supported as of v3.2.0. Pin neuralforecast to v3.1.9, or pass `ray_options=RayOptions(cpus=...)` instead. | None | | `gpus` | | No longer supported as of v3.2.0. Pin neuralforecast to v3.1.9, or pass `ray_options=RayOptions(gpus=...)` instead. | None | ## 2. Available AutoModels NeuralForecast provides 35 `AutoModel` variants, each wrapping a specific forecasting model with automatic hyperparameter optimization. Each `AutoModel` has a `default_config` attribute that defines sensible search spaces for its corresponding model. ### RNN-Based Models Recurrent neural networks for sequential forecasting: * `AutoRNN`: [Basic recurrent neural network](./models.rnn.html) * `AutoLSTM`: [Long Short-Term Memory network](./models.lstm.html) * `AutoGRU`: [Gated Recurrent Unit network](./models.gru.html) * `AutoDilatedRNN`: [RNN with dilated recurrent connections for capturing long-range dependencies](./models.dilated_rnn.html) * `AutoxLSTM`: Extended LSTM with enhanced memory capabilities ### Transformer-Based Models Attention-based architectures for capturing complex temporal patterns: * `AutoTFT`: [Temporal Fusion Transformer with multi-horizon forecasting](./models.tft.html) * `AutoVanillaTransformer`: [Standard transformer architecture](./models.vanillatransformer.html) * `AutoInformer`: [Efficient transformer for long sequence forecasting](./models.informer.html) * `AutoAutoformer`: [Auto-correlation based transformer](./models.autoformer.html) * `AutoFEDformer`: [Frequency enhanced decomposition transformer](./models.fedformer.html) * `AutoPatchTST`: [Patched time series transformer](./models.patchtst.html) * `AutoiTransformer`: [Inverted transformer for multivariate forecasting](./models.itransformer.html) * `AutoTimeXer`: [Cross-series attention transformer](./models.timemixer.html) ### CNN-Based Models Convolutional architectures for local pattern recognition: * `AutoTCN`: [Temporal Convolutional Network with causal convolutions](./models.tcn.html) * `AutoBiTCN`: [Bidirectional TCN](./models.bitcn.html) * `AutoTimesNet`: [Multi-periodic convolution network](./models.timesnet.html) ### Linear and MLP Models Simple yet effective linear and feed-forward architectures: * `AutoMLP`: [Multi-layer Perceptron](./models.mlp.html) * `AutoDLinear`: [Decomposition linear model](./models.dlinear.html) * `AutoNLinear`: [Normalized linear model](./models.nlinear.html) * `AutoTSMixer`: [Time Series Mixer architecture](./models.tsmixer.html) * `AutoTSMixerx`: [TSMixer with exogenous variable support](./models.tsmixerx.html) * `AutoMLPMultivariate`: [MLP for multivariate time series](./models.mlpmultivariate.html) ### Specialized Models Models designed for specific forecasting scenarios: * `AutoNBEATS`: [Neural Basis Expansion Analysis for interpretable forecasting](./models.nbeats.html) * `AutoNBEATSx`: [NBEATS with exogenous variables](./models.nbeatsx.html) * `AutoNHITS`: [Neural Hierarchical Interpolation for multi-horizon forecasting](./models.nhits.html) * `AutoDeepAR`: [Probabilistic forecasting with autoregressive RNN](./models.deepar.html) * `AutoDeepNPTS`: [Deep Non-Parametric Time Series model](./models.deepnpts.html) * `AutoTiDE`: [Time-series Dense Encoder](./models.tide.html) * `AutoKAN`: [Kolmogorov-Arnold Network for time series](./models.kan.html) * `AutoStemGNN`: [Graph neural network for multivariate forecasting](./models.stemgnn.html) * `AutoSOFTS`: [Spectral Optimal Fourier Transform model](./models.softs.html) * `AutoSOFTSSharp`: [SOFTS extension with stochastic variable-position encoding](./models.softssharp.html) * `AutoTimeMixer`: [Temporal mixing architecture](./models.timemixer.html) * `AutoRMoK`: [Random Mixture of Kernels](./models.rmok.html) * `AutoHINT`: [Hierarchical forecasting with automatic reconciliation](./models.hint.html) ## 3. Usage Examples ### Data Preparation First, prepare your time series data and create a `TimeSeriesDataset`: ```python theme={null} import numpy as np import pandas as pd from neuralforecast.tsdataset import TimeSeriesDataset from neuralforecast.utils import AirPassengersDF as Y_df # Split data temporally: train and test Y_train_df = Y_df[Y_df.ds <= '1959-12-31'] # 132 train observations Y_test_df = Y_df[Y_df.ds > '1959-12-31'] # 12 test observations # Create TimeSeriesDataset dataset, *_ = TimeSeriesDataset.from_df(Y_train_df) ``` ### Basic Usage The simplest way to use an `AutoModel` is with its default search space: ```python theme={null} from neuralforecast.auto import AutoRNN # Use your own config or AutoRNN.default_config config = dict(max_steps=1, val_check_steps=1, input_size=-1, encoder_hidden_size=8) model = AutoRNN(h=12, config=config, num_samples=1, cpus=1) # Fit and predict model.fit(dataset=dataset, val_size=12) y_hat = model.predict(dataset=dataset) ``` ### Hierarchical Forecasting with AutoHINT `AutoHINT` combines hyperparameter optimization with hierarchical reconciliation. This is useful when forecasting hierarchical time series (e.g., product hierarchies, geographic hierarchies). #### Optimize Model, Then Apply Fixed Reconciliation ```python theme={null} from neuralforecast.auto import AutoNHITS from neuralforecast.models.hint import HINT from neuralforecast.losses.pytorch import GMM, sCRPS base_model = AutoNHITS( h=4, loss=GMM(n_components=2, level=[80, 90]), # Probabilistic loss num_samples=10 ) # Apply hierarchical reconciliation with the optimized model # S: summing matrix defining the hierarchical structure model = HINT( h=4, S=S_df.values, model=base_model, reconciliation='MinTraceOLS' ) model.fit(dataset=dataset, val_size=4) y_hat = model.predict(dataset=dataset) ``` #### Joint Optimization of Model and Reconciliation Method ```python theme={null} from neuralforecast.auto import AutoHINT from neuralforecast.models.nhits import NHITS from ray import tune # Perform a conjunct hyperparameter optimization with # NHITS + HINT reconciliation configurations nhits_config = { "learning_rate": tune.choice([1e-3]), # Initial Learning rate "max_steps": tune.choice([1]), # Number of SGD steps "val_check_steps": tune.choice([1]), # Number of steps between validation "input_size": tune.choice([5 * 12]), # input_size = multiplier * horizon "batch_size": tune.choice([7]), # Number of series in windows "windows_batch_size": tune.choice([256]), # Number of windows in batch "n_pool_kernel_size": tune.choice([[2, 2, 2], [16, 8, 1]]), # MaxPool's Kernelsize "n_freq_downsample": tune.choice([[168, 24, 1], [24, 12, 1], [1, 1, 1]]), # Interpolation expressivity ratios "activation": tune.choice(['ReLU']), # Type of non-linear activation "n_blocks": tune.choice([[1, 1, 1]]), # Blocks per each 3 stacks "mlp_units": tune.choice([[[512, 512], [512, 512], [512, 512]]]), # 2 512-Layers per block for each stack "interpolation_mode": tune.choice(['linear']), # Type of multi-step interpolation "random_seed": tune.randint(1, 10), "reconciliation": tune.choice(['BottomUp', 'MinTraceOLS', 'MinTraceWLS']) } model = AutoHINT( h=4, S=S_df.values, cls_model=NHITS, config=nhits_config, loss=GMM(n_components=2, level=[80, 90]), valid_loss=sCRPS(level=[80, 90]), num_samples=20 ) model.fit(dataset=dataset, val_size=4) y_hat = model.predict(dataset=dataset) ``` # Informer Time Series Forecasting in Python Source: https://nixtlaverse.nixtla.io/neuralforecast/models.informer.html Build long horizon forecasts with Informer in NeuralForecast. Learn ProbSparse attention, architecture, parameters, and a complete Python workflow. The Informer model tackles the vanilla Transformer computational complexity challenges for long-horizon forecasting. The architecture has three distinctive features: * A ProbSparse self-attention mechanism with an O time and memory complexity Llog(L). - A self-attention distilling process that prioritizes attention and efficiently handles long input sequences. * An MLP multi-step decoder that predicts long time-series sequences in a single forward operation rather than step-by-step. The Informer model utilizes a three-component approach to define its embedding: * It employs encoded autoregressive features obtained from a convolution network. * It uses window-relative positional embeddings derived from harmonic functions. * Absolute positional embeddings obtained from calendar features are utilized. **References** * [Haoyi Zhou, Shanghang Zhang, Jieqi Peng, Shuai Zhang, Jianxin Li, Hui Xiong, Wancai Zhang. “Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting”](https://arxiv.org/abs/2012.07436) Figure 1. Temporal Fusion Transformer Architecture. *Figure 1. Temporal Fusion Transformer Architecture.* ## 1. Informer ### `Informer` ```python theme={null} Informer( h, input_size, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, decoder_input_size_multiplier=0.5, hidden_size=128, dropout=0.05, factor=3, n_head=4, conv_hidden_size=32, activation="gelu", encoder_layers=2, decoder_layers=1, distil=True, loss=MAE(), valid_loss=None, max_steps=5000, learning_rate=0.0001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) Informer ``` The Informer model tackles the vanilla Transformer computational complexity challenges for long-horizon forecasting. The architecture has three distinctive features: 1) A ProbSparse self-attention mechanism with an O time and memory complexity Llog(L). 2) A self-attention distilling process that prioritizes attention and efficiently handles long input sequences. 3) An MLP multi-step decoder that predicts long time-series sequences in a single forward operation rather than step-by-step. ```
The Informer model utilizes a three-component approach to define its embedding 1. It employs encoded autoregressive features obtained from a convolution network. 2. It uses window-relative positional embeddings derived from harmonic functions. 3. Absolute positional embeddings obtained from calendar features are utilized.
**Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `decoder_input_size_multiplier` | [float](#float) | multiplier for the input size of the decoder. | 0.5 | | `hidden_size` | [int](#int) | units of embeddings and encoders. | 128 | | `dropout` | [float](#float) | dropout throughout Informer architecture. | 0.05 | | `factor` | [int](#int) | Probsparse attention factor. | 3 | | `n_head` | [int](#int) | controls number of multi-head's attention. | 4 | | `conv_hidden_size` | [int](#int) | channels of the convolutional encoder. | 32 | | `activation` | [str](#str) | activation from \['ReLU', 'Softplus', 'Tanh', 'SELU', 'LeakyReLU', 'PReLU', 'Sigmoid', 'GELU']. | 'gelu' | | `encoder_layers` | [int](#int) | number of layers for the TCN encoder. | 2 | | `decoder_layers` | [int](#int) | number of layers for the MLP decoder. | 1 | | `distil` | [bool](#bool) | whether the Informer decoder uses bottlenecks. | True | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 5000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.0001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None |
References * [Haoyi Zhou, Shanghang Zhang, Jieqi Peng, Shuai Zhang, Jianxin Li, Hui Xiong, Wancai Zhang. "Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting"](https://arxiv.org/abs/2012.07436)
#### `Informer.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `Informer.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import Informer from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic, augment_calendar_df AirPassengersPanel, calendar_cols = augment_calendar_df(df=AirPassengersPanel, freq='M') Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = Informer(h=12, input_size=24, hidden_size = 16, conv_hidden_size = 32, n_head = 2, loss=MAE(), futr_exog_list=calendar_cols, scaler_type='robust', learning_rate=1e-3, max_steps=200, val_check_steps=50, early_stop_patience_steps=2) nf = NeuralForecast( models=[model], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = nf.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) if model.loss.is_distribution_output: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['Informer-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['Informer-lo-90'][-12:].values, y2=plot_df['Informer-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.legend() plt.plot() else: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['Informer'], c='blue', label='Forecast') plt.legend() plt.grid() ``` ## 2. Auxiliary Functions ### `ConvLayer` ```python theme={null} ConvLayer(c_in) ``` Bases: [Module](#torch.nn.Module) ConvLayer ### `ProbAttention` ```python theme={null} ProbAttention( mask_flag=True, factor=5, scale=None, attention_dropout=0.1, output_attention=False, ) ``` Bases: [Module](#torch.nn.Module) ProbAttention ### `ProbMask` ```python theme={null} ProbMask(B, H, L, index, scores, device='cpu') ``` ProbMask # iTransformer Time Series Forecasting in Python Source: https://nixtlaverse.nixtla.io/neuralforecast/models.itransformer.html Build multivariate forecasts with iTransformer in NeuralForecast. Learn its inverted attention architecture, parameters, and Python workflow. The iTransformer model simply takes the Transformer architecture but it applies the attention and feed-forward network on the inverted dimensions. This means that time points of each individual series are embedded into tokens. That way, the attention mechanisms learn multivariate correlation and the feed-forward network learns non-linear relationships. ## References * [Yong Liu, Tengge Hu, Haoran Zhang, Haixu Wu, Shiyu Wang, Lintao Ma, Mingsheng Long. “iTransformer: Inverted Transformers Are Effective for Time Series Forecasting”](https://arxiv.org/abs/2310.06625) ## 1. iTransformer ### `iTransformer` ```python theme={null} iTransformer( h, input_size, n_series, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, exclude_insample_y=False, hidden_size=512, n_heads=8, e_layers=2, d_layers=1, d_ff=2048, factor=1, dropout=0.1, use_norm=True, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) iTransformer **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `n_series` | [int](#int) | number of time-series. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `hidden_size` | [int](#int) | dimension of the model. | 512 | | `n_heads` | [int](#int) | number of heads. | 8 | | `e_layers` | [int](#int) | number of encoder layers. | 2 | | `d_layers` | [int](#int) | number of decoder layers. | 1 | | `d_ff` | [int](#int) | dimension of fully-connected layer. | 2048 | | `factor` | [int](#int) | attention factor. | 1 | | `dropout` | [float](#float) | dropout rate. | 0.1 | | `use_norm` | [bool](#bool) | whether to normalize or not. | True | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 32 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Yong Liu, Tengge Hu, Haoran Zhang, Haixu Wu, Shiyu Wang, Lintao Ma, Mingsheng Long. "iTransformer: Inverted Transformers Are Effective for Time Series Forecasting"](https://arxiv.org/abs/2310.06625)
#### `iTransformer.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `iTransformer.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import iTransformer from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic from neuralforecast.losses.pytorch import MSE Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = iTransformer(h=12, input_size=24, n_series=2, hidden_size=128, n_heads=2, e_layers=2, d_layers=1, d_ff=4, factor=1, dropout=0.1, use_norm=True, loss=MSE(), valid_loss=MAE(), early_stop_patience_steps=3, batch_size=32, max_steps=100) fcst = NeuralForecast(models=[model], freq='ME') fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['iTransformer'], c='blue', label='Forecast') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` # KAN Time Series Forecasting in Python Source: https://nixtlaverse.nixtla.io/neuralforecast/models.kan.html Build time series forecasts with Kolmogorov Arnold Networks in NeuralForecast. Compare KAN with MLP models and run a complete Python example. Kolmogorov-Arnold Networks (KANs) are an alternative to Multi-Layer Perceptrons (MLPs). This model uses KANs similarly as our MLP model. **References** * [Ziming Liu, Yixuan Wang, Sachin Vaidya, Fabian Ruehle, James Halverson, Marin Soljačić, Thomas Y. Hou, Max Tegmark. “KAN: Kolmogorov–Arnold Networks”](https://arxiv.org/html/2404.19756v1) Figure 1. KAN compared to MLP. *Figure 1. KAN compared to MLP.* ## 1. KAN ### `KAN` ```python theme={null} KAN( h, input_size, grid_size=5, spline_order=3, scale_noise=0.1, scale_base=1.0, scale_spline=1.0, enable_standalone_scale_spline=True, grid_eps=0.02, grid_range=[-1, 1], n_hidden_layers=1, hidden_size=512, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=-1, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) KAN Simple Kolmogorov-Arnold Network (KAN). This network uses the Kolmogorov-Arnold approximation theorem, where splines are learned to approximate more complex functions. Unlike the MLP, the non-linear function are learned at the edges, and the nodes simply sum the different learned functions. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | considered autorregresive inputs (lags), y=\[1,2,3,4] input\_size=2 -> lags=\[1,2]. | *required* | | `grid_size` | [int](#int) | number of intervals used by the splines to approximate the function. | 5 | | `spline_order` | [int](#int) | order of the B-splines. | 3 | | `scale_noise` | [float](#float) | regularization coefficient for the splines. | 0.1 | | `scale_base` | [float](#float) | scaling coefficient for the base function. | 1.0 | | `scale_spline` | [float](#float) | scaling coefficient for the splines. | 1.0 | | `enable_standalone_scale_spline` | [bool](#bool) | whether each spline is scaled individually. | True | | `grid_eps` | [float](#float) | used for numerical stability. | 0.02 | | `grid_range` | [list](#list) | range of the grid used for spline approximation. | \[-1, 1] | | `n_hidden_layers` | [int](#int) | number of hidden layers for the KAN. | 1 | | `hidden_size` | [int](#int) or [list](#list) | number of units for each hidden layer of the KAN. If an integer, all hidden layers will have the same size. Use a list to specify the size of each hidden layer. | 512 | | `stat_exog_list` | str list | static exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `futr_exog_list` | str list | future exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | -1 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#typing.Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Ziming Liu, Yixuan Wang, Sachin Vaidya, Fabian Ruehle, James Halverson, Marin Soljačić, Thomas Y. Hou, Max Tegmark. "KAN: Kolmogorov-Arnold Networks"](https://arxiv.org/abs/2404.19756)
#### `KAN.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `KAN.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import KAN from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test fcst = NeuralForecast( models=[ KAN(h=12, input_size=24, loss = DistributionLoss(distribution="Normal"), max_steps=100, scaler_type='standard', futr_exog_list=['y_[lag12]'], hist_exog_list=None, stat_exog_list=['airline1'], ), ], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic) forecasts = fcst.predict(futr_df=Y_test_df) # Plot quantile predictions Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['KAN-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['KAN-lo-90'][-12:].values, y2=plot_df['KAN-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() ``` ## 2. Auxiliary functions ### `KANLinear` ```python theme={null} KANLinear( in_features, out_features, grid_size=5, spline_order=3, scale_noise=0.1, scale_base=1.0, scale_spline=1.0, enable_standalone_scale_spline=True, base_activation=torch.nn.SiLU, grid_eps=0.02, grid_range=[-1, 1], ) ``` Bases: [Module](#torch.nn.Module) KANLinear # LSTM Source: https://nixtlaverse.nixtla.io/neuralforecast/models.lstm.html LSTM: Long Short-Term Memory network for sequential forecasting. Multilayer encoder-decoder architecture that addresses vanishing gradients in time series. The Long Short-Term Memory Recurrent Neural Network ([`LSTM`](./models.lstm.html#lstm)), uses a multilayer [`LSTM`](./models.lstm.html#lstm) encoder and an [`MLP`](./models.mlp.html#mlp) decoder. It builds upon the LSTM-cell that improves the exploding and vanishing gradients of classic [`RNN`](./models.rnn.html#rnn)’s. This network has been extensively used in sequential prediction tasks like language modeling, phonetic labeling, and forecasting. The predictions are obtained by transforming the hidden states into contexts $\mathbf{c}_{[t+1:t+H]}$, that are decoded and adapted into $\mathbf{\hat{y}}_{[t+1:t+H],[q]}$ through MLPs. where $\mathbf{h}_{t}$, is the hidden state for time $t$, $\mathbf{y}_{t}$ is the input at time $t$ and $\mathbf{h}_{t-1}$ is the hidden state of the previous layer at $t-1$, $\mathbf{x}^{(s)}$ are static exogenous inputs, $\mathbf{x}^{(h)}_{t}$ historic exogenous, $\mathbf{x}^{(f)}_{[:t+H]}$ are future exogenous available at the time of the prediction. **References** * [Jeffrey L. Elman (1990). “Finding Structure in Time”.](https://onlinelibrary.wiley.com/doi/abs/10.1207/s15516709cog1402_1) * [Haşim Sak, Andrew Senior, Françoise Beaufays (2014). “Long Short-Term Memory Based Recurrent Neural Network Architectures for Large Vocabulary Speech Recognition.”](https://arxiv.org/abs/1402.1128) Figure 1. Long Short-Term Memory Cell. *Figure 1. Long Short-Term Memory Cell.* ## 1. LSTM ### `LSTM` ```python theme={null} LSTM( h, input_size=-1, inference_input_size=None, h_train=1, encoder_n_layers=2, encoder_hidden_size=128, encoder_bias=True, encoder_dropout=0.0, context_size=None, decoder_hidden_size=128, decoder_layers=2, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, recurrent=False, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=128, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="robust", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) LSTM LSTM encoder, with MLP decoder. The network has `tanh` or `relu` non-linearities, it is trained using ADAM stochastic gradient descent. The network accepts static, historic and future exogenous data. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. Default -1 uses 3 \* horizon | -1 | | `inference_input_size` | [int](#int) | maximum sequence length for truncated inference. Default None uses input\_size history. | None | | `h_train` | [int](#int) | maximum sequence length for truncated train backpropagation. Default 1. | 1 | | `encoder_n_layers` | [int](#int) | number of layers for the LSTM. | 2 | | `encoder_hidden_size` | [int](#int) | units for the LSTM's hidden state size. | 128 | | `encoder_bias` | [bool](#bool) | whether or not to use biases b\_ih, b\_hh within LSTM units. | True | | `encoder_dropout` | [float](#float) | dropout regularization applied to LSTM outputs. | 0.0 | | `context_size` | [deprecated](#deprecated) | deprecated. | None | | `decoder_hidden_size` | [int](#int) | size of hidden layer for the MLP decoder. | 128 | | `decoder_layers` | [int](#int) | number of layers for the MLP decoder. | 2 | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | whether to exclude the target variable from the input. | False | | `recurrent` | [bool](#bool) | whether to produce forecasts recursively (True) or direct (False). | False | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of differentseries in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 128 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'robust' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | | #### `LSTM.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `LSTM.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import LSTM from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test nf = NeuralForecast( models=[LSTM(h=12, input_size=8, loss=DistributionLoss(distribution="Normal", level=[80, 90]), scaler_type='robust', encoder_n_layers=2, encoder_hidden_size=128, decoder_hidden_size=128, decoder_layers=2, max_steps=200, futr_exog_list=['y_[lag12]'], stat_exog_list=['airline1'], recurrent=True, h_train=1, ) ], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic) Y_hat_df = nf.predict(futr_df=Y_test_df) # Plots Y_hat_df = Y_hat_df.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['LSTM-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['LSTM-lo-90'][-12:].values, y2=plot_df['LSTM-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.plot() ``` # MLP Source: https://nixtlaverse.nixtla.io/neuralforecast/models.mlp.html MLP: Multi-Layer Perceptron for time series forecasting. Simple feedforward neural network with ReLU activations and autoregressive structure for predictions. One of the simplest neural architectures are Multi Layer Perceptrons (`MLP`) composed of stacked Fully Connected Neural Networks trained with backpropagation. Each node in the architecture is capable of modeling non-linear relationships granted by their activation functions. Novel activations like Rectified Linear Units (`ReLU`) have greatly improved the ability to fit deeper networks overcoming gradient vanishing problems that were associated with `Sigmoid` and `TanH` activations. For the forecasting task the last layer is changed to follow a auto-regression problem. **References** -[Rosenblatt, F. (1958). "The perceptron: A probabilistic model for information storage and organization in the brain."](https://psycnet.apa.org/record/1959-09865-001) -[Fukushima, K. (1975). "Cognitron: A self-organizing multilayered neural network."](https://pascal-francis.inist.fr/vibad/index.php?action=getRecordDetail\&idt=PASCAL7750396723) -[Vinod Nair, Geoffrey E. Hinton (2010). "Rectified Linear Units Improve Restricted Boltzmann Machines"](https://www.cs.toronto.edu/~fritz/absps/reluICML.pdf) Figure 1. Three layer MLP with autorregresive inputs. *Figure 1. Three layer MLP with autorregresive inputs.* ## MLP ### `MLP` ```python theme={null} MLP( h, input_size, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, num_layers=2, hidden_size=1024, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=-1, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) MLP Simple Multi Layer Perceptron architecture (MLP). This deep neural network has constant units through its layers, each with ReLU non-linearities, it is trained using ADAM stochastic gradient descent. The network accepts static, historic and future exogenous data, flattens the inputs and learns fully connected relationships against the target variable. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | considered autorregresive inputs (lags), y=\[1,2,3,4] input\_size=2 -> lags=\[1,2]. | *required* | | `stat_exog_list` | str list | static exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `futr_exog_list` | str list | future exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `num_layers` | [int](#int) | number of layers for the MLP. | 2 | | `hidden_size` | [int](#int) | number of units for each layer of the MLP. | 1024 | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | -1 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | | #### `MLP.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `MLP.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import MLP from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = MLP(h=12, input_size=24, loss=DistributionLoss(distribution='Normal', level=[80, 90]), scaler_type='robust', learning_rate=1e-3, max_steps=200, val_check_steps=10, early_stop_patience_steps=2) fcst = NeuralForecast( models=[model], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['MLP-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['MLP-lo-90'][-12:].values, y2=plot_df['MLP-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.legend() plt.plot() ``` # MLPMultivariate Source: https://nixtlaverse.nixtla.io/neuralforecast/models.mlpmultivariate.html MLPMultivariate: Multi-Layer Perceptron for joint multivariate forecasting. Predicts all time series simultaneously with shared feedforward neural network layers. One of the simplest neural architectures are Multi Layer Perceptrons (`MLP`) composed of stacked Fully Connected Neural Networks trained with backpropagation. Each node in the architecture is capable of modeling non-linear relationships granted by their activation functions. Novel activations like Rectified Linear Units (`ReLU`) have greatly improved the ability to fit deeper networks overcoming gradient vanishing problems that were associated with `Sigmoid` and `TanH` activations. For the forecasting task the last layer is changed to follow a auto-regression problem. This version is multivariate, indicating that it will predict all time series of the forecasting problem jointly. **References** -[Rosenblatt, F. (1958). "The perceptron: A probabilistic model for information storage and organization in the brain."](https://psycnet.apa.org/record/1959-09865-001) -[Fukushima, K. (1975). "Cognitron: A self-organizing multilayered neural network."](https://pascal-francis.inist.fr/vibad/index.php?action=getRecordDetail\&idt=PASCAL7750396723) -[Vinod Nair, Geoffrey E. Hinton (2010). "Rectified Linear Units Improve Restricted Boltzmann Machines"](https://www.cs.toronto.edu/~fritz/absps/reluICML.pdf) Figure 1. Three layer MLP with autorregresive inputs. *Figure 1. Three layer MLP with autorregresive inputs.* ## MLPMultivariate ### `MLPMultivariate` ```python theme={null} MLPMultivariate( h, input_size, n_series, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, num_layers=2, hidden_size=1024, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) MLPMultivariate Simple Multi Layer Perceptron architecture (MLP) for multivariate forecasting. This deep neural network has constant units through its layers, each with ReLU non-linearities, it is trained using ADAM stochastic gradient descent. The network accepts static, historic and future exogenous data, flattens the inputs and learns fully connected relationships against the target variables. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | considered autorregresive inputs (lags), y=\[1,2,3,4] input\_size=2 -> lags=\[1,2]. | *required* | | `n_series` | [int](#int) | number of time-series. | *required* | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `hist_exog_list` | str list | historic exogenous columns. | None | | `futr_exog_list` | str list | future exogenous columns. | None | | `num_layers` | [int](#int) | number of layers for the MLP. | 2 | | `hidden_size` | [int](#int) | number of units for each layer of the MLP. | 1024 | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 32 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | | #### `MLPMultivariate.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `MLPMultivariate.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import MLPMultivariate from neuralforecast.losses.pytorch import MAE from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = MLPMultivariate(h=12, input_size=24, n_series=2, stat_exog_list=['airline1'], futr_exog_list=['trend'], loss = MAE(), scaler_type='robust', learning_rate=1e-3, stat_exog_list=['airline1'], max_steps=200, val_check_steps=10, early_stop_patience_steps=2) fcst = NeuralForecast( models=[model], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['MLPMultivariate'], c='blue', label='median') plt.grid() plt.legend() plt.plot() ``` # NBEATS Source: https://nixtlaverse.nixtla.io/neuralforecast/models.nbeats.html NBEATS: Neural Basis Expansion Analysis with interpretable or generic configurations. MLP-based architecture with residual links for M3/M4 competition performance. The Neural Basis Expansion Analysis ([`NBEATS`](./models.nbeats.html#nbeats)) is an [`MLP`](./models.mlp.html#mlp)-based deep neural architecture with backward and forward residual links. The network has two variants: (1) in its interpretable configuration, [`NBEATS`](./models.nbeats.html#nbeats) sequentially projects the signal into polynomials and harmonic basis to learn trend and seasonality components; (2) in its generic configuration, it substitutes the polynomial and harmonic basis for identity basis and larger network’s depth. The Neural Basis Expansion Analysis with Exogenous ([`NBEATSx`](./models.nbeatsx.html#nbeatsx)), incorporates projections to exogenous temporal variables available at the time of the prediction. This method proved state-of-the-art performance on the M3, M4, and Tourism Competition datasets, improving accuracy by 3% over the `ESRNN` M4 competition winner. **References** * [Boris N. Oreshkin, Dmitri Carpov, Nicolas Chapados, Yoshua Bengio (2019). “N-BEATS: Neural basis expansion analysis for interpretable time series forecasting”.](https://arxiv.org/abs/1905.10437) Figure 1. Neural Basis Expansion Analysis. *Figure 1. Neural Basis Expansion Analysis.* ## NBEATS ### `NBEATS` ```python theme={null} NBEATS( h, input_size, n_harmonics=2, n_polynomials=None, n_basis=2, basis="polynomial", stack_types=["identity", "trend", "seasonality"], n_blocks=[1, 1, 1], mlp_units=3 * [[512, 512]], dropout_prob_theta=0.0, activation="ReLU", shared_weights=False, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=3, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=-1, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) NBEATS The Neural Basis Expansion Analysis for Time Series (NBEATS), is a simple and yet effective architecture, it is built with a deep stack of MLPs with the doubly residual connections. It has a generic and interpretable architecture depending on the blocks it uses. Its interpretable architecture is recommended for scarce data settings, as it regularizes its predictions through projections unto harmonic and trend basis well-suited for most forecasting tasks. **Parameters:** `h`: int, forecast horizon. `input_size`: int, considered autorregresive inputs (lags), y=\[1,2,3,4] input\_size=2 -> lags=\[1,2]. `n_harmonics`: int, Number of harmonic terms for seasonality stack type. Note that len(n\_harmonics) = len(stack\_types). Note that it will only be used if a seasonality stack is used. `n_polynomials`: int, DEPRECATED - polynomial degree for trend stack. Note that len(n\_polynomials) = len(stack\_types). Note that it will only be used if a trend stack is used. `basis`: str, Type of basis function to use in the trend stack. Choose one from \['legendre', 'polynomial', 'changepoint', 'piecewise\_linear', 'linear\_hat', 'spline', 'chebyshev'] `n_basis`: int, the degree of the basis function for the trend stack. Note that it will only be used if a trend stack is used. `stack_types`: List\[str], List of stack types. Subset from \['seasonality', 'trend', 'identity']. `n_blocks`: List\[int], Number of blocks for each stack. Note that len(n\_blocks) = len(stack\_types). `mlp_units`: List\[List\[int]], Structure of hidden layers for each stack type. Each internal list should contain the number of units of each hidden layer. Note that len(n\_hidden) = len(stack\_types). `dropout_prob_theta`: float, Float between (0, 1). Dropout for N-BEATS basis. `activation`: str, activation from \['ReLU', 'Softplus', 'Tanh', 'SELU', 'LeakyReLU', 'PReLU', 'Sigmoid']. `shared_weights`: bool, If True, all blocks within each stack will share parameters. `loss`: PyTorch module, instantiated train loss class from [losses collection](./losses.pytorch.html). `valid_loss`: PyTorch module=`loss`, instantiated valid loss class from [losses collection](./losses.pytorch.html). `max_steps`: int=1000, maximum number of training steps. `learning_rate`: float=1e-3, Learning rate between (0, 1). `num_lr_decays`: int=3, Number of learning rate decays, evenly distributed across max\_steps. `early_stop_patience_steps`: int=-1, Number of validation iterations before early stopping. `val_monitor`: str="ptl/val\_loss", metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". `val_check_steps`: int=100, Number of training steps between every validation loss check. `batch_size`: int=32, number of different series in each batch. `valid_batch_size`: int=None, number of different series in each validation and test batch, if None uses batch\_size. `windows_batch_size`: int=1024, number of windows to sample in each training batch, default uses all. `inference_windows_batch_size`: int=-1, number of windows to sample in each inference batch, -1 uses all. `start_padding_enabled`: bool=False, if True, the model will pad the time series with zeros at the beginning, by input size. `training_data_availability_threshold`: Union\[float, List\[float]]=0.0, minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). `step_size`: int=1, step size between each window of temporal data. `scaler_type`: str='identity', type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). `random_seed`: int, random\_seed for pytorch initializer and numpy generators. `drop_last_loader`: bool=False, if True `TimeSeriesDataLoader` drops last non-full batch. `alias`: str, optional, Custom name of the model. `optimizer`: Subclass of 'torch.optim.Optimizer', optional, user specified optimizer instead of the default choice (Adam). `optimizer_kwargs`: dict, optional, list of parameters used by the user specified `optimizer`. `lr_scheduler`: Subclass of 'torch.optim.lr\_scheduler.LRScheduler', optional, user specified lr\_scheduler instead of the default choice (StepLR). `lr_scheduler_kwargs`: dict, optional, list of parameters used by the user specified `lr_scheduler`. `dataloader_kwargs`: dict, optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. `**trainer_kwargs`: int, keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). **References:** -[Boris N. Oreshkin, Dmitri Carpov, Nicolas Chapados, Yoshua Bengio (2019). "N-BEATS: Neural basis expansion analysis for interpretable time series forecasting".](https://arxiv.org/abs/1905.10437) #### `NBEATS.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `NBEATS.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import NBEATS from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = NBEATS(h=12, input_size=24, basis='changepoint', n_basis=2, loss=DistributionLoss(distribution='Poisson', level=[80, 90]), stack_types = ['identity', 'trend', 'seasonality'], max_steps=100, val_check_steps=10, early_stop_patience_steps=2) fcst = NeuralForecast( models=[model], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot quantile predictions Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['NBEATS-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['NBEATS-lo-90'][-12:].values, y2=plot_df['NBEATS-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.legend() plt.plot() ``` # NBEATSx Source: https://nixtlaverse.nixtla.io/neuralforecast/models.nbeatsx.html NBEATSx: Neural Basis Expansion Analysis with exogenous variables. MLP-based architecture with interpretable trend-seasonality blocks for forecasting. The Neural Basis Expansion Analysis ([`NBEATS`](./models.nbeats.html#nbeats)) is an [`MLP`](./models.mlp.html#mlp)-based deep neural architecture with backward and forward residual links. The network has two variants: (1) in its interpretable configuration, [`NBEATS`](./models.nbeats.html#nbeats) sequentially projects the signal into polynomials and harmonic basis to learn trend and seasonality components; (2) in its generic configuration, it substitutes the polynomial and harmonic basis for identity basis and larger network’s depth. The Neural Basis Expansion Analysis with Exogenous ([`NBEATSx`](./models.nbeatsx.html#nbeatsx)), incorporates projections to exogenous temporal variables available at the time of the prediction. This method proved state-of-the-art performance on the M3, M4, and Tourism Competition datasets, improving accuracy by 3% over the `ESRNN` M4 competition winner. For Electricity Price Forecasting tasks [`NBEATSx`](./models.nbeatsx.html#nbeatsx) model improved accuracy by 20% and 5% over `ESRNN` and [`NBEATS`](./models.nbeats.html#nbeats), and 5% on task-specialized architectures. **References** * [Boris N. Oreshkin, Dmitri Carpov, Nicolas Chapados, Yoshua Bengio (2019). “N-BEATS: Neural basis expansion analysis for interpretable time series forecasting”.](https://arxiv.org/abs/1905.10437) * [Kin G. Olivares, Cristian Challu, Grzegorz Marcjasz, Rafał Weron, Artur Dubrawski (2021). “Neural basis expansion analysis with exogenous variables: Forecasting electricity prices with NBEATSx”.](https://arxiv.org/abs/2104.05522) Figure 1. Neural Basis Expansion Analysis with Exogenous Variables. *Figure 1. Neural Basis Expansion Analysis with Exogenous Variables.* ## NBEATSx ### `NBEATSx` ```python theme={null} NBEATSx( h, input_size, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, n_harmonics=2, n_polynomials=2, stack_types=["identity", "trend", "seasonality"], n_blocks=[1, 1, 1], mlp_units=3 * [[512, 512]], dropout_prob_theta=0.0, activation="ReLU", shared_weights=False, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=3, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=-1, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) NBEATSx The Neural Basis Expansion Analysis with Exogenous variables (NBEATSx) is a simple and effective deep learning architecture. It is built with a deep stack of MLPs with doubly residual connections. The NBEATSx architecture includes additional exogenous blocks, extending NBEATS capabilities and interpretability. With its interpretable version, NBEATSx decomposes its predictions on seasonality, trend, and exogenous effects. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `n_harmonics` | [int](#int) | Number of harmonic oscillations in the SeasonalityBasis \[cos(i \* t/n\_harmonics), sin(i \* t/n\_harmonics)]. Note that it will only be used if 'seasonality' is in `stack_types`. | 2 | | `n_polynomials` | [int](#int) | Number of polynomial terms for TrendBasis \[1,t,...,t^n\_poly]. Note that it will only be used if 'trend' is in `stack_types`. | 2 | | `stack_types` | [List](#List)\[[str](#str)] | List of stack types. Subset from \['seasonality', 'trend', 'identity', 'exogenous']. | \['identity', 'trend', 'seasonality'] | | `n_blocks` | [List](#List)\[[int](#int)] | Number of blocks for each stack. Note that len(n\_blocks) = len(stack\_types). | \[1, 1, 1] | | `mlp_units` | [List](#List)\[[List](#List)\[[int](#int)]] | Structure of hidden layers for each stack type. Each internal list should contain the number of units of each hidden layer. Note that len(n\_hidden) = len(stack\_types). | 3 \* \[\[512, 512]] | | `dropout_prob_theta` | [float](#float) | Float between (0, 1). Dropout for N-BEATS basis. | 0.0 | | `activation` | [str](#str) | activation from \['ReLU', 'Softplus', 'Tanh', 'SELU', 'LeakyReLU', 'PReLU', 'Sigmoid']. | 'ReLU' | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | 3 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | -1 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random seed initialization for replicability. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Kin G. Olivares, Cristian Challu, Grzegorz Marcjasz, Rafał Weron, Artur Dubrawski (2021). "Neural basis expansion analysis with exogenous variables: Forecasting electricity prices with NBEATSx".](https://arxiv.org/abs/2104.05522)
#### `NBEATSx.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `NBEATSx.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import NBEATSx from neuralforecast.losses.pytorch import MQLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = NBEATSx(h=12, input_size=24, loss=MQLoss(level=[80, 90]), scaler_type='robust', dropout_prob_theta=0.5, stat_exog_list=['airline1'], futr_exog_list=['trend'], stack_types = ["identity", "trend", "seasonality", "exogenous"], n_blocks = [1,1,1,1], max_steps=200, val_check_steps=10, early_stop_patience_steps=2) nf = NeuralForecast( models=[model], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) Y_hat_df = nf.predict(futr_df=Y_test_df) # Plot quantile predictions Y_hat_df = Y_hat_df.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['NBEATSx-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['NBEATSx-lo-90'][-12:].values, y2=plot_df['NBEATSx-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() plt.plot() ``` # NHITS Source: https://nixtlaverse.nixtla.io/neuralforecast/models.nhits.html NHITS: Neural Hierarchical Interpolation for Time Series. MLP architecture with multi-rate processing for long-horizon forecasting, 50x faster than Informer. Long-horizon forecasting is challenging because of the *volatility* of the predictions and the *computational complexity*. To solve this problem we created the Neural Hierarchical Interpolation for Time Series (NHITS). [`NHITS`](./models.nhits.html#nhits) builds upon [`NBEATS`](./models.nbeats.html#nbeats) and specializes its partial outputs in the different frequencies of the time series through hierarchical interpolation and multi-rate input processing. On the long-horizon forecasting task [`NHITS`](./models.nhits.html#nhits) improved accuracy by 25% on AAAI’s best paper award the [`Informer`](./models.informer.html#informer), while being 50x faster. The model is composed of several MLPs with ReLU non-linearities. Blocks are connected via doubly residual stacking principle with the backcast $\mathbf{\tilde{y}}_{t-L:t,l}$ and forecast $\mathbf{\hat{y}}_{t+1:t+H,l}$ outputs of the l-th block. Multi-rate input pooling, hierarchical interpolation and backcast residual connections together induce the specialization of the additive predictions in different signal bands, reducing memory footprint and computational time, thus improving the architecture parsimony and accuracy. **References** * [Boris N. Oreshkin, Dmitri Carpov, Nicolas Chapados, Yoshua Bengio (2019). “N-BEATS: Neural basis expansion analysis for interpretable time series forecasting”.](https://arxiv.org/abs/1905.10437) * [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2023). “NHITS: Neural Hierarchical Interpolation for Time Series Forecasting”. Accepted at the Thirty-Seventh AAAI Conference on Artificial Intelligence.](https://arxiv.org/abs/2201.12886) * [Zhou, H.; Zhang, S.; Peng, J.; Zhang, S.; Li, J.; Xiong, H.; and Zhang, W. (2020). “Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting”. Association for the Advancement of Artificial Intelligence Conference 2021 (AAAI 2021).](https://arxiv.org/abs/2012.07436) Figure 1. Neural Hierarchical Interpolation for Time Series (NHITS). *Figure 1. Neural Hierarchical Interpolation for Time Series (NHITS).* ## NHITS ### `NHITS` ```python theme={null} NHITS( h, input_size, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, stack_types=["identity", "identity", "identity"], n_blocks=[1, 1, 1], mlp_units=3 * [[512, 512]], n_pool_kernel_size=[2, 2, 1], n_freq_downsample=[4, 2, 1], pooling_mode="MaxPool1d", interpolation_mode="linear", dropout_prob_theta=0.0, activation="ReLU", loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=3, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=-1, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) NHITS The Neural Hierarchical Interpolation for Time Series (NHITS), is an MLP-based deep neural architecture with backward and forward residual links. NHITS tackles volatility and memory complexity challenges, by locally specializing its sequential predictions into the signals frequencies with hierarchical interpolation and pooling. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `stack_types` | [List](#List)\[[str](#str)] | stacks list in the form N \* \['identity'], to be deprecated in favor of `n_stacks`. Note that len(stack\_types)=len(n\_freq\_downsample)=len(n\_pool\_kernel\_size). | \['identity', 'identity', 'identity'] | | `n_blocks` | [List](#List)\[[int](#int)] | Number of blocks for each stack. Note that len(n\_blocks) = len(stack\_types). | \[1, 1, 1] | | `mlp_units` | [List](#List)\[[List](#List)\[[int](#int)]] | Structure of hidden layers for each stack type. Each internal list should contain the number of units of each hidden layer. Note that len(n\_hidden) = len(stack\_types). | 3 \* \[\[512, 512]] | | `n_pool_kernel_size` | [List](#List)\[[int](#int)] | list with the size of the windows to take a max/avg over. Note that len(stack\_types)=len(n\_freq\_downsample)=len(n\_pool\_kernel\_size). | \[2, 2, 1] | | `n_freq_downsample` | [List](#List)\[[int](#int)] | list with the stack's coefficients (inverse expressivity ratios). Note that len(stack\_types)=len(n\_freq\_downsample)=len(n\_pool\_kernel\_size). | \[4, 2, 1] | | `pooling_mode` | [str](#str) | input pooling module from \['MaxPool1d', 'AvgPool1d']. | 'MaxPool1d' | | `interpolation_mode` | [str](#str) | interpolation basis from \['linear', 'nearest', 'cubic']. | 'linear' | | `dropout_prob_theta` | [float](#float) | Float between (0, 1). Dropout for NHITS basis. | 0.0 | | `activation` | [str](#str) | activation from \['ReLU', 'Softplus', 'Tanh', 'SELU', 'LeakyReLU', 'PReLU', 'Sigmoid']. | 'ReLU' | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | 3 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | -1 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2023). "NHITS: Neural Hierarchical Interpolation for Time Series Forecasting". Accepted at the Thirty-Seventh AAAI Conference on Artificial Intelligence.](https://arxiv.org/abs/2201.12886)
#### `NHITS.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `NHITS.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import NHITS from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = NHITS(h=12, input_size=24, loss=DistributionLoss(distribution='StudentT', level=[80, 90], return_params=True), stat_exog_list=['airline1'], futr_exog_list=['trend'], n_freq_downsample=[2, 1, 1], scaler_type='robust', max_steps=200, early_stop_patience_steps=2, inference_windows_batch_size=1, val_check_steps=10, learning_rate=1e-3) fcst = NeuralForecast(models=[model], freq='ME') fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot quantile predictions Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['NHITS-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['NHITS-lo-90'][-12:].values, y2=plot_df['NHITS-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() plt.plot() ``` # NLinear Source: https://nixtlaverse.nixtla.io/neuralforecast/models.nlinear.html NLinear: Normalized linear model for long-horizon forecasting. Handles distribution shifts with simple subtraction-addition normalization for robust predictions. NLinear is a simple and fast yet accurate time series forecasting model for long-horizon forecasting. The architecture aims to boost the performance when there is a distribution shift in the dataset: 1. NLinear first subtracts the input by the last value of the sequence; 2. Then, the input goes through a linear layer, and the subtracted part is added back before making the final prediction. **References** * [Zeng, Ailing, et al. “Are transformers effective for time series forecasting?.” Proceedings of the AAAI conference on artificial intelligence. Vol. 37. No. 9. 2023.”](https://ojs.aaai.org/index.php/AAAI/article/view/26317) Figure 1. DLinear Architecture. *Figure 1. DLinear Architecture.* ## NLinear ### `NLinear` ```python theme={null} NLinear( h, input_size, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, exclude_insample_y=False, loss=MAE(), valid_loss=None, max_steps=5000, learning_rate=0.0001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) NLinear **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. | *required* | | `stat_exog_list` | str list | static exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `futr_exog_list` | str list | future exogenous columns. | None | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 5000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.0001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Zeng, Ailing, et al. "Are transformers effective for time series forecasting?." Proceedings of the AAAI conference on artificial intelligence. Vol. 37. No. 9. 2023."](https://ojs.aaai.org/index.php/AAAI/article/view/26317)
#### `NLinear.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `NLinear.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import NLinear from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic, augment_calendar_df AirPassengersPanel, calendar_cols = augment_calendar_df(df=AirPassengersPanel, freq='M') Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = NLinear(h=12, input_size=24, loss=DistributionLoss(distribution='StudentT', level=[80, 90], return_params=True), scaler_type='robust', learning_rate=1e-3, max_steps=500, val_check_steps=50, early_stop_patience_steps=2) nf = NeuralForecast( models=[model], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = nf.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) if model.loss.is_distribution_output: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['NLinear-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['NLinear-lo-90'][-12:].values, y2=plot_df['NLinear-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.legend() plt.plot() else: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['NLinear'], c='blue', label='Forecast') plt.legend() plt.grid() ``` # PatchTST Time Series Forecasting in Python Source: https://nixtlaverse.nixtla.io/neuralforecast/models.patchtst.html Build long horizon forecasts with PatchTST in NeuralForecast. Learn how temporal patches and channel independence work, then run a complete Python example. The PatchTST model is an efficient Transformer-based model for multivariate time series forecasting. It is based on two key components: - segmentation of time series into windows (patches) which are served as input tokens to Transformer - channel-independence. where each channel contains a single univariate time series. **References** * [Nie, Y., Nguyen, N. H., Sinthong, P., & Kalagnanam, J. (2022). “A Time Series is Worth 64 Words: Long-term Forecasting with Transformers”](https://arxiv.org/pdf/2211.14730.pdf) Figure 1. PatchTST. *Figure 1. PatchTST.* ## 1. PatchTST ### `PatchTST` ```python theme={null} PatchTST( h, input_size, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, exclude_insample_y=False, encoder_layers=3, n_heads=16, hidden_size=128, linear_hidden_size=256, dropout=0.2, fc_dropout=0.2, head_dropout=0.0, attn_dropout=0.0, patch_len=16, stride=8, revin=True, revin_affine=False, revin_subtract_last=True, activation="gelu", res_attention=True, batch_normalization=False, learn_pos_embed=True, loss=MAE(), valid_loss=None, max_steps=5000, learning_rate=0.0001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) PatchTST The PatchTST model is an efficient Transformer-based model for multivariate time series forecasting. It is based on two key components: * segmentation of time series into windows (patches) which are served as input tokens to Transformer * channel-independence, where each channel contains a single univariate time series. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `stat_exog_list` | str list | static exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `futr_exog_list` | str list | future exogenous columns. | None | | `exclude_insample_y` | [bool](#bool) | the model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `encoder_layers` | [int](#int) | number of layers for encoder. | 3 | | `n_heads` | [int](#int) | number of multi-head's attention. | 16 | | `hidden_size` | [int](#int) | units of embeddings and encoders. | 128 | | `linear_hidden_size` | [int](#int) | units of linear layer. | 256 | | `dropout` | [float](#float) | dropout rate for residual connection. | 0.2 | | `fc_dropout` | [float](#float) | dropout rate for linear layer. | 0.2 | | `head_dropout` | [float](#float) | dropout rate for Flatten head layer. | 0.0 | | `attn_dropout` | [float](#float) | dropout rate for attention layer. | 0.0 | | `patch_len` | [int](#int) | length of patch. Note: patch\_len = min(patch\_len, input\_size + stride). | 16 | | `stride` | [int](#int) | stride of patch. | 8 | | `revin` | [bool](#bool) | bool to use RevIn. | True | | `revin_affine` | [bool](#bool) | bool to use affine in RevIn. | False | | `revin_subtract_last` | [bool](#bool) | bool to use subtract last in RevIn. | True | | `activation` | [str](#str) | activation from \['gelu','relu']. | 'gelu' | | `res_attention` | [bool](#bool) | bool to use residual attention. | True | | `batch_normalization` | [bool](#bool) | bool to use batch normalization. | False | | `learn_pos_embed` | [bool](#bool) | bool to learn positional embedding. | True | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 5000 | | `learning_rate` | [float](#float) | learning rate between (0, 1). | 0.0001 | | `num_lr_decays` | [int](#int) | number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Nie, Y., Nguyen, N. H., Sinthong, P., & Kalagnanam, J. (2022). "A Time Series is Worth 64 Words: Long-term Forecasting with Transformers"](https://arxiv.org/pdf/2211.14730.pdf)
#### `PatchTST.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `PatchTST.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import PatchTST from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic, augment_calendar_df AirPassengersPanel, calendar_cols = augment_calendar_df(df=AirPassengersPanel, freq='M') Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = PatchTST(h=12, input_size=104, patch_len=24, stride=24, revin=False, hidden_size=16, n_heads=4, scaler_type='robust', loss=DistributionLoss(distribution='StudentT', level=[80, 90]), learning_rate=1e-3, max_steps=500, val_check_steps=50, early_stop_patience_steps=2) nf = NeuralForecast( models=[model], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = nf.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) if model.loss.is_distribution_output: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['PatchTST-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['PatchTST-lo-90'][-12:].values, y2=plot_df['PatchTST-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.legend() plt.plot() else: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['PatchTST'], c='blue', label='Forecast') plt.legend() plt.grid() ``` ## 2. Backbone ### Auxiliary Functions ### `get_activation_fn` ```python theme={null} get_activation_fn(activation) ``` ### `Transpose` ```python theme={null} Transpose(*dims, contiguous=False) ``` Bases: [Module](#torch.nn.Module) Transpose ### Positional Encoding ### `positional_encoding` ```python theme={null} positional_encoding(pe, learn_pe, q_len, hidden_size) ``` ### `Coord1dPosEncoding` ```python theme={null} Coord1dPosEncoding(q_len, exponential=False, normalize=True) ``` ### `Coord2dPosEncoding` ```python theme={null} Coord2dPosEncoding( q_len, hidden_size, exponential=False, normalize=True, eps=0.001 ) ``` ### `PositionalEncoding` ```python theme={null} PositionalEncoding(q_len, hidden_size, normalize=True) ``` ### Encoder ### `TSTEncoderLayer` ```python theme={null} TSTEncoderLayer( q_len, hidden_size, n_heads, d_k=None, d_v=None, linear_hidden_size=256, store_attn=False, norm="BatchNorm", attn_dropout=0, dropout=0.0, bias=True, activation="gelu", res_attention=False, pre_norm=False, ) ``` Bases: [Module](#torch.nn.Module) TSTEncoderLayer ### `TSTEncoder` ```python theme={null} TSTEncoder( q_len, hidden_size, n_heads, d_k=None, d_v=None, linear_hidden_size=None, norm="BatchNorm", attn_dropout=0.0, dropout=0.0, activation="gelu", res_attention=False, n_layers=1, pre_norm=False, store_attn=False, ) ``` Bases: [Module](#torch.nn.Module) TSTEncoder ### `TSTiEncoder` ```python theme={null} TSTiEncoder( c_in, patch_num, patch_len, max_seq_len=1024, n_layers=3, hidden_size=128, n_heads=16, d_k=None, d_v=None, linear_hidden_size=256, norm="BatchNorm", attn_dropout=0.0, dropout=0.0, act="gelu", store_attn=False, key_padding_mask="auto", padding_var=None, attn_mask=None, res_attention=True, pre_norm=False, pe="zeros", learn_pe=True, ) ``` Bases: [Module](#torch.nn.Module) TSTiEncoder ### `Flatten_Head` ```python theme={null} Flatten_Head(individual, n_vars, nf, h, c_out, head_dropout=0) ``` Bases: [Module](#torch.nn.Module) Flatten\_Head ### `PatchTST_backbone` ```python theme={null} PatchTST_backbone( c_in, c_out, input_size, h, patch_len, stride, max_seq_len=1024, n_layers=3, hidden_size=128, n_heads=16, d_k=None, d_v=None, linear_hidden_size=256, norm="BatchNorm", attn_dropout=0.0, dropout=0.0, act="gelu", key_padding_mask="auto", padding_var=None, attn_mask=None, res_attention=True, pre_norm=False, store_attn=False, pe="zeros", learn_pe=True, fc_dropout=0.0, head_dropout=0, padding_patch=None, pretrain_head=False, head_type="flatten", individual=False, revin=True, affine=True, subtract_last=False, ) ``` Bases: [Module](#torch.nn.Module) PatchTST\_backbone # Reversible Mixture of KAN - RMoK Source: https://nixtlaverse.nixtla.io/neuralforecast/models.rmok.html RMoK: Reversible Mixture of Kolmogorov-Arnold Networks. Combines Taylor, Jacobi, and wavelet functions for expressive time series forecasting with reversibility. Figure 1. Architecture of RMoK. *Figure 1. Architecture of RMoK.* ## 1. Reversible Mixture of KAN - RMoK ### `RMoK` ```python theme={null} RMoK( h, input_size, n_series, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, taylor_order=3, jacobi_degree=6, wavelet_function="mexican_hat", dropout=0.1, revin_affine=True, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) Reversible Mixture of KAN **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `n_series` | [int](#int) | number of time-series. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `taylor_order` | [int](#int) | order of the Taylor polynomial. | 3 | | `jacobi_degree` | [int](#int) | degree of the Jacobi polynomial. | 6 | | `wavelet_function` | [str](#str) | wavelet function to use in the WaveKAN. Choose from \["mexican\_hat", "morlet", "dog", "meyer", "shannon"] | 'mexican\_hat' | | `dropout` | [float](#float) | dropout rate. | 0.1 | | `revin_affine` | [bool](#bool) | bool to use affine in RevIn. | True | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 32 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Xiao Han, Xinfeng Zhang, Yiling Wu, Zhenduo Zhang, Zhe Wu."KAN4TSF: Are KAN and KAN-based models Effective for Time Series Forecasting?". arXiv.](https://arxiv.org/abs/2408.11306)
#### `RMoK.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `RMoK.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import RMoK from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic from neuralforecast.losses.pytorch import MSE Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = RMoK(h=12, input_size=24, n_series=2, taylor_order=3, jacobi_degree=6, wavelet_function='mexican_hat', dropout=0.1, revin_affine=True, loss=MSE(), valid_loss=MAE(), early_stop_patience_steps=3, batch_size=32) fcst = NeuralForecast(models=[model], freq='ME') fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['RMoK'], c='blue', label='Forecast') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ## 2. Auxiliary functions ### `WaveKANLayer` ```python theme={null} WaveKANLayer( in_features, out_features, wavelet_type="mexican_hat", with_bn=True, device="cpu", ) ``` Bases: [Module](#torch.nn.Module) This is a sample code for the simulations of the paper: Bozorgasl, Zavareh and Chen, Hao, Wav-KAN: Wavelet Kolmogorov-Arnold Networks (May, 2024) [https://arxiv.org/abs/2405.12832](https://arxiv.org/abs/2405.12832) and also available at: [https://papers.ssrn.com/sol3/papers.cfm?abstract\_id=4835325](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4835325) We used efficient KAN notation and some part of the code:+ ### `TaylorKANLayer` ```python theme={null} TaylorKANLayer(input_dim, out_dim, order, addbias=True) ``` Bases: [Module](#torch.nn.Module) [https://github.com/Muyuzhierchengse/TaylorKAN/](https://github.com/Muyuzhierchengse/TaylorKAN/) ### `JacobiKANLayer` ```python theme={null} JacobiKANLayer(input_dim, output_dim, degree, a=1.0, b=1.0) ``` Bases: [Module](#torch.nn.Module) [https://github.com/SpaceLearner/JacobiKAN/blob/main/JacobiKANLayer.py](https://github.com/SpaceLearner/JacobiKAN/blob/main/JacobiKANLayer.py) # RNN Source: https://nixtlaverse.nixtla.io/neuralforecast/models.rnn.html RNN: Classic Elman Recurrent Neural Network for sequential forecasting. Multilayer architecture with tanh/ReLU activations and MLP decoder for time series. Elman proposed this classic recurrent neural network ([`RNN`](./models.rnn.html#rnn)) in 1990, where each layer uses the following recurrent transformation: $\mathbf{h}^{l}_{t} = \mathrm{Activation}([\mathbf{y}_{t},\mathbf{x}^{(h)}_{t},\mathbf{x}^{(s)}] W^{\intercal}_{ih} + b_{ih} + \mathbf{h}^{l}_{t-1} W^{\intercal}_{hh} + b_{hh})$ where $\mathbf{h}^{l}_{t}$, is the hidden state of RNN layer $l$ for time $t$, $\mathbf{y}_{t}$ is the input at time $t$ and $\mathbf{h}_{t-1}$ is the hidden state of the previous layer at $t-1$, $\mathbf{x}^{(s)}$ are static exogenous inputs, $\mathbf{x}^{(h)}_{t}$ historic exogenous, $\mathbf{x}^{(f)}_{[:t+H]}$ are future exogenous available at the time of the prediction. The available activations are `tanh`, and `relu`. The predictions are obtained by transforming the hidden states into contexts $\mathbf{c}_{[t+1:t+H]}$, that are decoded and adapted into $\mathbf{\hat{y}}_{[t+1:t+H],[q]}$ through MLPs. **References** * [Jeffrey L. Elman (1990). “Finding Structure in Time”.](https://onlinelibrary.wiley.com/doi/abs/10.1207/s15516709cog1402_1) * [Cho, K., van Merrienboer, B., Gülcehre, C., Bougares, F., Schwenk, H., & Bengio, Y. (2014). Learning phrase representations using RNN encoder-decoder for statistical machine translation.](http://arxiv.org/abs/1406.1078) Figure 1. Single Layer Elman RNN with MLP decoder. *Figure 1. Single Layer Elman RNN with MLP decoder.* ## RNN ### `RNN` ```python theme={null} RNN( h, input_size=-1, inference_input_size=None, h_train=1, encoder_n_layers=2, encoder_hidden_size=128, encoder_activation="tanh", encoder_bias=True, encoder_dropout=0.0, context_size=None, decoder_hidden_size=128, decoder_layers=2, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, recurrent=False, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=128, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="robust", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) RNN Multi Layer Elman RNN (RNN), with MLP decoder. The network has `tanh` or `relu` non-linearities, it is trained using ADAM stochastic gradient descent. The network accepts static, historic and future exogenous data. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. Default -1 uses 3 \* horizon. | -1 | | `inference_input_size` | [int](#int) | maximum sequence length for truncated inference. Default None uses input\_size history. | None | | `h_train` | [int](#int) | maximum sequence length for truncated train backpropagation. Default 1. | 1 | | `encoder_n_layers` | [int](#int) | number of layers for the RNN. | 2 | | `encoder_hidden_size` | [int](#int) | units for the RNN's hidden state size. | 128 | | `encoder_activation` | [str](#str) | type of RNN activation from `tanh` or `relu`. | 'tanh' | | `encoder_bias` | [bool](#bool) | whether or not to use biases b\_ih, b\_hh within RNN units. | True | | `encoder_dropout` | [float](#float) | dropout regularization applied to RNN outputs. | 0.0 | | `decoder_hidden_size` | [int](#int) | size of hidden layer for the MLP decoder. | 128 | | `decoder_layers` | [int](#int) | number of layers for the MLP decoder. | 2 | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | whether to exclude the target variable from the historic exogenous data. | False | | `recurrent` | [bool](#bool) | whether to produce forecasts recursively (True) or direct (False). | False | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of differentseries in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 128 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'robust' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | | #### `RNN.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `RNN.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ## Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import RNN from neuralforecast.losses.pytorch import MQLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test fcst = NeuralForecast( models=[RNN(h=12, input_size=24, inference_input_size=24, loss=MQLoss(level=[80, 90]), valid_loss=MQLoss(level=[80, 90]), scaler_type='standard', encoder_n_layers=2, encoder_hidden_size=128, decoder_hidden_size=128, decoder_layers=2, max_steps=200, futr_exog_list=['y_[lag12]'], stat_exog_list=['airline1'], ) ], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['RNN-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['RNN-lo-90'][-12:].values, y2=plot_df['RNN-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() plt.plot() ``` # SOFTS Source: https://nixtlaverse.nixtla.io/neuralforecast/models.softs.html SOFTS: Spectral Optimal Fourier Transform model for multivariate time series forecasting using frequency-domain analysis and temporal pattern recognition. ## 1. SOFTS ### `SOFTS` ```python theme={null} SOFTS( h, input_size, n_series, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, exclude_insample_y=False, hidden_size=512, d_core=512, e_layers=2, d_ff=2048, dropout=0.1, use_norm=True, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) SOFTS **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `n_series` | [int](#int) | number of time-series. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `hidden_size` | [int](#int) | dimension of the model. | 512 | | `d_core` | [int](#int) | dimension of core in STAD. | 512 | | `e_layers` | [int](#int) | number of encoder layers. | 2 | | `d_ff` | [int](#int) | dimension of fully-connected layer. | 2048 | | `dropout` | [float](#float) | dropout rate. | 0.1 | | `use_norm` | [bool](#bool) | whether to normalize or not. | True | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 32 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Lu Han, Xu-Yang Chen, Han-Jia Ye, De-Chuan Zhan. "SOFTS: Efficient Multivariate Time Series Forecasting with Series-Core Fusion"](https://arxiv.org/pdf/2404.14197)
#### `SOFTS.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `SOFTS.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import SOFTS from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic from neuralforecast.losses.pytorch import MASE Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = SOFTS(h=12, input_size=24, n_series=2, hidden_size=256, d_core=256, e_layers=2, d_ff=64, dropout=0.1, use_norm=True, loss=MASE(seasonality=4), early_stop_patience_steps=3, batch_size=32) fcst = NeuralForecast(models=[model], freq='ME') fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['SOFTS'], c='blue', label='Forecast') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ## 2. Auxiliary functions ### `DataEmbedding_inverted` ```python theme={null} DataEmbedding_inverted(c_in, d_model, dropout=0.1) ``` Bases: [Module](#torch.nn.Module) Data Embedding ### `STAD` ```python theme={null} STAD(d_series, d_core) ``` Bases: [Module](#torch.nn.Module) STar Aggregate Dispatch Module # SOFTSSharp Source: https://nixtlaverse.nixtla.io/neuralforecast/models.softssharp.html SOFTSSharp: SOFTS extension with stochastic variable-position encoding for multivariate time series forecasting. SOFTSSharp extends SOFTS by stochastically adding variable-position embeddings and multiple dropout layers inside the STAD aggregation-redistribution component, aiming to improve forecasting accuracy while preserving linear complexity. Figure 1. Architecture of SOFTSSharp *Figure 1. Architecture of SOFTSSharp* ## 1. SOFTSSharp ### `SOFTSSharp` ```python theme={null} SOFTSSharp( h, input_size, n_series, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, exclude_insample_y=False, hidden_size=512, d_core=512, e_layers=2, d_ff=2048, dropout=0.1, pe_keep_prob=0.5, use_norm=True, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) SOFTSSharp SOFTS# (SOFTSSharp) extends SOFTS by stochastically adding variable-position embeddings and multiple dropout layers inside the STAD component. **Parameters:** | Name | Type | Description | Default | | ------------------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | Autoregressive inputs size. | *required* | | `n_series` | [int](#int) | Number of time-series. | *required* | | `hidden_size` | [int](#int) | Dimension of the model. | 512 | | `d_core` | [int](#int) | Dimension of core in STADSharp. | 512 | | `e_layers` | [int](#int) | Number of encoder layers. | 2 | | `d_ff` | [int](#int) | Dimension of fully-connected layer. | 2048 | | `dropout` | [float](#float) | Dropout rate. | 0.1 | | `pe_keep_prob` | [float](#float) | probability of applying variable-position encoding during training. During inference, the positional encoding is scaled by this value. | 0.5 | | `use_norm` | [bool](#bool) | Whether to normalize or not. | True | | `loss` | PyTorch module | Instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | Instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | Maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | Metric to monitor for early stopping. | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | Number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | Number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | Number of windows to sample in each training batch, default uses all. | 32 | | `inference_windows_batch_size` | [int](#int) | Number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | If True, the model will pad the time series with zeros at the beginning, by input size. | False | | `step_size` | [int](#int) | Step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | Type of scaler for temporal inputs normalization. | 'identity' | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | If True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | Optional custom name of the model. | None |
References * [Hrvoje Ljubić. "SOFTSSharp: SOFTS extension with stochastic variable-position encoding", reference implementation; manuscript in preparation](https://github.com/hljubic/SOFTSsharp) * [Hrvoje Ljubić, Goran Martinović, Tomislav Volarić, Robert Rozić. "SOFTS++: Fast and accurate linear model for multivariate long-term time series forecasting", related work](https://doi.org/10.1177/1088467X251380055) * [Lu Han, Xu-Yang Chen, Han-Jia Ye, De-Chuan Zhan. "SOFTS: Efficient Multivariate Time Series Forecasting with Series-Core Fusion"](https://arxiv.org/pdf/2404.14197)
#### `SOFTSSharp.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `SOFTSSharp.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import SOFTSSharp from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic from neuralforecast.losses.pytorch import MASE Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) model = SOFTSSharp(h=12, input_size=24, n_series=2, hidden_size=256, d_core=256, e_layers=2, d_ff=64, dropout=0.1, pe_keep_prob=0.5, use_norm=True, loss=MASE(seasonality=4), early_stop_patience_steps=3, batch_size=32) fcst = NeuralForecast(models=[model], freq='ME') fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['SOFTSSharp'], c='blue', label='Forecast') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ## 2. Auxiliary functions ### `PositionalEmbedding` ```python theme={null} PositionalEmbedding(d_series, max_len=5000) ``` Bases: [Module](#torch.nn.Module) ### `STADSharp` ```python theme={null} STADSharp(d_series, d_core, dropout_rate=0.1, pe_keep_prob=0.5) ``` Bases: [Module](#torch.nn.Module) STar Aggregate Dispatch Module with stochastic variable-position encoding. # StemGNN Source: https://nixtlaverse.nixtla.io/neuralforecast/models.stemgnn.html StemGNN: Spectral Temporal Graph Neural Network for multivariate forecasting. Learns temporal dependencies and inter-series correlations in spectral domain. The Spectral Temporal Graph Neural Network ([`StemGNN`](./models.stemgnn.html#stemgnn)) is a Graph-based multivariate time-series forecasting model. [`StemGNN`](./models.stemgnn.html#stemgnn) jointly learns temporal dependencies and inter-series correlations in the spectral domain, by combining Graph Fourier Transform (GFT) and Discrete Fourier Transform (DFT). This method proved state-of-the-art performance on geo-temporal datasets such as `Solar`, `METR-LA`, and `PEMS-BAY`, and **References** * [Defu Cao, Yujing Wang, Juanyong Duan, Ce Zhang, Xia Zhu, Congrui Huang, Yunhai Tong, Bixiong Xu, Jing Bai, Jie Tong, Qi Zhang (2020). “Spectral Temporal Graph Neural Network for Multivariate Time-series Forecasting”.](https://proceedings.neurips.cc/paper/2020/hash/cdf6581cb7aca4b7e19ef136c6e601a5-Abstract.html) Figure 1. StemGNN. *Figure 1. StemGNN.* ## 1. StemGNN ### `StemGNN` ```python theme={null} StemGNN( h, input_size, n_series, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, exclude_insample_y=False, n_stacks=2, multi_layer=5, dropout_rate=0.5, leaky_rate=0.2, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=3, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="robust", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) StemGNN The Spectral Temporal Graph Neural Network (`StemGNN`) is a Graph-based multivariate time-series forecasting model. `StemGNN` jointly learns temporal dependencies and inter-series correlations in the spectral domain, by combining Graph Fourier Transform (GFT) and Discrete Fourier Transform (DFT). **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `n_series` | [int](#int) | number of time-series. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `n_stacks` | [int](#int) | number of stacks in the model. | 2 | | `multi_layer` | [int](#int) | multiplier for FC hidden size on StemGNN blocks. | 5 | | `dropout_rate` | [float](#float) | dropout rate. | 0.5 | | `leaky_rate` | [float](#float) | alpha for LeakyReLU layer on Latent Correlation layer. | 0.2 | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | 3 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of windows in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 32 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'robust' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | | #### `StemGNN.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `StemGNN.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Examples Train model and forecast future values with `predict` method. ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import StemGNN from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic from neuralforecast.losses.pytorch import MAE Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = StemGNN(h=12, input_size=24, n_series=2, scaler_type='standard', max_steps=500, early_stop_patience_steps=-1, val_check_steps=10, learning_rate=1e-3, loss=MAE(), valid_loss=MAE(), batch_size=32 ) fcst = NeuralForecast(models=[model], freq='ME') fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['StemGNN'], c='blue', label='Forecast') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` Using `cross_validation` to forecast multiple historic values. ```python theme={null} fcst = NeuralForecast(models=[model], freq='M') forecasts = fcst.cross_validation(df=AirPassengersPanel, static_df=AirPassengersStatic, n_windows=2, step_size=12) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.loc['Airline1'] Y_df = AirPassengersPanel[AirPassengersPanel['unique_id']=='Airline1'] plt.plot(Y_df['ds'], Y_df['y'], c='black', label='True') plt.plot(Y_hat_df['ds'], Y_hat_df['StemGNN'], c='blue', label='Forecast') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ## 2. Auxiliary functions ### `GLU` ```python theme={null} GLU(input_channel, output_channel) ``` Bases: [Module](#torch.nn.Module) GLU ### `StockBlockLayer` ```python theme={null} StockBlockLayer(time_step, unit, multi_layer, stack_cnt=0) ``` Bases: [Module](#torch.nn.Module) StockBlockLayer # TCN Source: https://nixtlaverse.nixtla.io/neuralforecast/models.tcn.html TCN: Temporal Convolutional Network with dilated causal convolutions for efficient sequential forecasting. Captures long-range dependencies with ReLU activations. For long time in deep learning, sequence modelling was synonymous with recurrent networks, yet several papers have shown that simple convolutional architectures can outperform canonical recurrent networks like LSTMs by demonstrating longer effective memory. By skipping temporal connections the causal convolution filters can be applied to larger time spans while remaining computationally efficient. The predictions are obtained by transforming the hidden states into contexts $\mathbf{c}_{[t+1:t+H]}$, that are decoded and adapted into $\mathbf{\hat{y}}_{[t+1:t+H],[q]}$ through MLPs. where $\mathbf{h}_{t}$, is the hidden state for time $t$, $\mathbf{y}_{t}$ is the input at time $t$ and $\mathbf{h}_{t-1}$ is the hidden state of the previous layer at $t-1$, $\mathbf{x}^{(s)}$ are static exogenous inputs, $\mathbf{x}^{(h)}_{t}$ historic exogenous, $\mathbf{x}^{(f)}_{[:t+H]}$ are future exogenous available at the time of the prediction. **References** * [van den Oord, A., Dieleman, S., Zen, H., Simonyan, K., Vinyals, O., Graves, A., Kalchbrenner, N., Senior, A. W., & Kavukcuoglu, K. (2016). Wavenet: A generative model for raw audio. Computing Research Repository, abs/1609.03499. URL: http://arxiv.org/abs/1609.03499. arXiv:1609.03499.](https://arxiv.org/abs/1609.03499) * [Shaojie Bai, Zico Kolter, Vladlen Koltun. (2018). An Empirical Evaluation of Generic Convolutional and Recurrent Networks for Sequence Modeling. Computing Research Repository, abs/1803.01271. URL: https://arxiv.org/abs/1803.01271.](https://arxiv.org/abs/1803.01271) Figure 1. Visualization of a stack of dilated causal convolutional layers. *Figure 1. Visualization of a stack of dilated causal convolutional layers.* ## TCN ### `TCN` ```python theme={null} TCN( h, input_size=-1, inference_input_size=None, kernel_size=2, dilations=[1, 2, 4, 8, 16], encoder_hidden_size=128, encoder_activation="ReLU", context_size=10, decoder_hidden_size=128, decoder_layers=2, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=128, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="robust", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) TCN Temporal Convolution Network (TCN), with MLP decoder. The historical encoder uses dilated skip connections to obtain efficient long memory, while the rest of the architecture allows for future exogenous alignment. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | maximum sequence length for truncated train backpropagation. Default -1 uses 3 \* horizon | -1 | | `inference_input_size` | [int](#int) | maximum sequence length for truncated inference. Default None uses input\_size history. | None | | `kernel_size` | [int](#int) | size of the convolving kernel. | 2 | | `dilations` | int list | controls the temporal spacing between the kernel points; also known as the à trous algorithm. | \[1, 2, 4, 8, 16] | | `encoder_hidden_size` | [int](#int) | units for the TCN's hidden state size. | 128 | | `encoder_activation` | [str](#str) | type of TCN activation from `tanh` or `relu`. | 'ReLU' | | `context_size` | [int](#int) | size of context vector for each timestamp on the forecasting window. | 10 | | `decoder_hidden_size` | [int](#int) | size of hidden layer for the MLP decoder. | 128 | | `decoder_layers` | [int](#int) | number of layers for the MLP decoder. | 2 | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of differentseries in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 128 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#typing.List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'robust' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | | #### `TCN.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `TCN.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import TCN from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test fcst = NeuralForecast( models=[TCN(h=12, input_size=-1, loss=DistributionLoss(distribution='Normal', level=[80, 90]), learning_rate=5e-4, kernel_size=2, dilations=[1,2,4,8,16], encoder_hidden_size=128, context_size=10, decoder_hidden_size=128, decoder_layers=2, max_steps=500, scaler_type='robust', futr_exog_list=['y_[lag12]'], hist_exog_list=None, stat_exog_list=['airline1'], ) ], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic) forecasts = fcst.predict(futr_df=Y_test_df) # Plot quantile predictions Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['TCN-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['TCN-lo-90'][-12:].values, y2=plot_df['TCN-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() plt.plot() ``` # Temporal Fusion Transformer Forecasting in Python Source: https://nixtlaverse.nixtla.io/neuralforecast/models.tft.html Build interpretable forecasts with Temporal Fusion Transformer in NeuralForecast using static, historic, and future variables in Python. In summary Temporal Fusion Transformer (TFT) combines gating layers, an LSTM recurrent encoder, with multi-head attention layers for a multi-step forecasting strategy decoder. TFT’s inputs are static exogenous $\mathbf{x}^{(s)}$, historic exogenous $\mathbf{x}^{(h)}_{[:t]}$, exogenous available at the time of the prediction $\mathbf{x}^{(f)}_{[:t+H]}$ and autorregresive features $\mathbf{y}_{[:t]}$, each of these inputs is further decomposed into categorical and continuous. The network uses a multi-quantile regression to model the following conditional probability:$\mathbb{P}(\mathbf{y}_{[t+1:t+H]}|\;\mathbf{y}_{[:t]},\; \mathbf{x}^{(h)}_{[:t]},\; \mathbf{x}^{(f)}_{[:t+H]},\; \mathbf{x}^{(s)})$ **References** * [Jan Golda, Krzysztof Kudrynski. “NVIDIA, Deep Learning Forecasting Examples”](https://github.com/NVIDIA/DeepLearningExamples/tree/master/PyTorch/Forecasting/TFT) * [Bryan Lim, Sercan O. Arik, Nicolas Loeff, Tomas Pfister, “Temporal Fusion Transformers for interpretable multi-horizon time series forecasting”](https://www.sciencedirect.com/science/article/pii/S0169207021000637) Figure 1. Temporal Fusion Transformer Architecture. *Figure 1. Temporal Fusion Transformer Architecture.* ## 1. Temporal Fusion Decoder ### `TFT` ```python theme={null} TFT( h, input_size, tgt_size=1, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, hidden_size=128, n_head=4, attn_dropout=0.0, grn_activation="ELU", n_rnn_layers=1, rnn_type="lstm", one_rnn_initial_state=False, dropout=0.1, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="robust", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) TFT The Temporal Fusion Transformer architecture (TFT) is an Sequence-to-Sequence model that combines static, historic and future available data to predict an univariate target. The method combines gating layers, an LSTM recurrent encoder, with and interpretable multi-head attention layer and a multi-step forecasting strategy decoder. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `tgt_size` | [int](#int) | target size. | 1 | | `stat_exog_list` | str list | static continuous columns. | None | | `hist_exog_list` | str list | historic continuous columns. | None | | `futr_exog_list` | str list | future continuous columns. | None | | `hidden_size` | [int](#int) | units of embeddings and encoders. | 128 | | `n_head` | [int](#int) | number of attention heads in temporal fusion decoder. | 4 | | `attn_dropout` | [float](#float) | dropout of fusion decoder's attention layer. | 0.0 | | `grn_activation` | [str](#str) | activation for the GRN module from \['ReLU', 'Softplus', 'Tanh', 'SELU', 'LeakyReLU', 'Sigmoid', 'ELU', 'GLU', 'GELU']. | 'ELU' | | `n_rnn_layers` | [int](#int) | number of RNN layers. | 1 | | `rnn_type` | [str](#str) | recurrent neural network (RNN) layer type from \["lstm","gru"]. | 'lstm' | | `one_rnn_initial_state` | [str](#str) | Initialize all rnn layers with the same initial states computed from static covariates. | False | | `dropout` | [float](#float) | dropout of inputs VSNs. | 0.1 | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch. | None | | `windows_batch_size` | [int](#int) | windows sampled from rolled data, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'robust' | | `random_seed` | [int](#int) | random seed initialization for replicability. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Bryan Lim, Sercan O. Arik, Nicolas Loeff, Tomas Pfister, "Temporal Fusion Transformers for interpretable multi-horizon time series forecasting"](https://www.sciencedirect.com/science/article/pii/S0169207021000637)
#### `TFT.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `TFT.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `TFT.feature_importances` ```python theme={null} feature_importances() ``` Compute the feature importances for historical, future, and static features. **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dict` | | A dictionary containing the feature importances for each feature type. The keys are 'hist\_vsn', 'future\_vsn', and 'static\_vsn', and the values are pandas DataFrames with the corresponding feature importances. | #### `TFT.attention_weights` ```python theme={null} attention_weights() ``` Batch average attention weights Returns: np.ndarray: A 1D array containing the attention weights for each time step. #### `TFT.feature_importance_correlations` ```python theme={null} feature_importance_correlations() ``` Compute the correlation between the past and future feature importances and the mean attention weights. Returns: pd.DataFrame: A DataFrame containing the correlation coefficients between the past feature importances and the mean attention weights. ### Usage Example ```python theme={null} import matplotlib.pyplot as plt import numpy as np import pandas as pd from neuralforecast import NeuralForecast # from neuralforecast.models import TFT from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic AirPassengersPanel["month"] = AirPassengersPanel.ds.dt.month Y_train_df = AirPassengersPanel[ AirPassengersPanel.ds < AirPassengersPanel["ds"].values[-12] ] # 132 train Y_test_df = AirPassengersPanel[ AirPassengersPanel.ds >= AirPassengersPanel["ds"].values[-12] ].reset_index(drop=True) # 12 test nf = NeuralForecast( models=[ TFT( h=12, input_size=48, hidden_size=20, grn_activation="ELU", rnn_type="lstm", n_rnn_layers=1, one_rnn_initial_state=False, loss=DistributionLoss(distribution="StudentT", level=[80, 90]), learning_rate=0.005, stat_exog_list=["airline1"], futr_exog_list=["y_[lag12]", "month"], hist_exog_list=["trend"], max_steps=300, val_check_steps=10, early_stop_patience_steps=10, scaler_type="robust", windows_batch_size=None, enable_progress_bar=True, ), ], freq="ME", ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) Y_hat_df = nf.predict(futr_df=Y_test_df) # Plot quantile predictions Y_hat_df = Y_hat_df.reset_index(drop=False).drop(columns=["unique_id", "ds"]) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id == "Airline1"].drop("unique_id", axis=1) plt.plot(plot_df["ds"], plot_df["y"], c="black", label="True") plt.plot(plot_df["ds"], plot_df["TFT"], c="purple", label="mean") plt.plot(plot_df["ds"], plot_df["TFT-median"], c="blue", label="median") plt.fill_between( x=plot_df["ds"][-12:], y1=plot_df["TFT-lo-90"][-12:].values, y2=plot_df["TFT-hi-90"][-12:].values, alpha=0.4, label="level 90", ) plt.legend() plt.grid() plt.plot() ``` ## 2. TFT Architecture The first TFT’s step is embed the original input $\{\mathbf{x}^{(s)}, \mathbf{x}^{(h)}, \mathbf{x}^{(f)}\}$ into a high dimensional space $\{\mathbf{E}^{(s)}, \mathbf{E}^{(h)}, \mathbf{E}^{(f)}\}$, after which each embedding is gated by a variable selection network (VSN). The static embedding $\mathbf{E}^{(s)}$ is used as context for variable selection and as initial condition to the LSTM. Finally the encoded variables are fed into the multi-head attention decoder. ### 2.1 Static Covariate Encoder The static embedding $\mathbf{E}^{(s)}$ is transformed by the StaticCovariateEncoder into contexts $c_{s}, c_{e}, c_{h}, c_{c}$. Where $c_{s}$ are temporal variable selection contexts, $c_{e}$ are TemporalFusionDecoder enriching contexts, and $c_{h}, c_{c}$ are LSTM’s hidden/contexts for the TemporalCovariateEncoder. ### 2.2 Temporal Covariate Encoder TemporalCovariateEncoder encodes the embeddings $\mathbf{E}^{(h)}, \mathbf{E}^{(f)}$ and contexts $(c_{h}, c_{c})$ with an LSTM. An analogous process is repeated for the future data, with the main difference that $\mathbf{E}^{(f)}$ contains the future available information. ### 2.3 Temporal Fusion Decoder The TemporalFusionDecoder enriches the LSTM’s outputs with $c_{e}$ and then uses an attention layer, and multi-step adapter. ## 3. Interpretability ### 3.1 Attention Weights ```python theme={null} attention = nf.models[0].attention_weights() ``` ```python theme={null} def plot_attention( self, plot: str = "time", output: str = "plot", width: int = 800, height: int = 400 ): """ Plot the attention weights. Args: plot (str, optional): The type of plot to generate. Can be one of the following: - 'time': Display the mean attention weights over time. - 'all': Display the attention weights for each horizon. - 'heatmap': Display the attention weights as a heatmap. - An integer in the range [1, model.h) to display the attention weights for a specific horizon. output (str, optional): The type of output to generate. Can be one of the following: - 'plot': Display the plot directly. - 'figure': Return the plot as a figure object. width (int, optional): Width of the plot in pixels. Default is 800. height (int, optional): Height of the plot in pixels. Default is 400. Returns: matplotlib.figure.Figure: If `output` is 'figure', the function returns the plot as a figure object. """ attention = ( self.mean_on_batch(self.interpretability_params["attn_wts"]) .mean(dim=0) .cpu() .numpy() ) fig, ax = plt.subplots(figsize=(width / 100, height / 100)) if plot == "time": attention = attention[self.input_size :, :].mean(axis=0) ax.plot(np.arange(-self.input_size, self.h), attention) ax.axvline( x=0, color="black", linewidth=3, linestyle="--", label="prediction start" ) ax.set_title("Mean Attention") ax.set_xlabel("time") ax.set_ylabel("Attention") ax.legend() elif plot == "all": for i in range(self.input_size, attention.shape[0]): ax.plot( np.arange(-self.input_size, self.h), attention[i, :], label=f"horizon {i-self.input_size+1}", ) ax.axvline( x=0, color="black", linewidth=3, linestyle="--", label="prediction start" ) ax.set_title("Attention per horizon") ax.set_xlabel("time") ax.set_ylabel("Attention") ax.legend() elif plot == "heatmap": cax = ax.imshow( attention, aspect="auto", cmap="viridis", extent=[-self.input_size, self.h, -self.input_size, self.h], ) fig.colorbar(cax) ax.set_title("Attention Heatmap") ax.set_xlabel("Attention (current time step)") ax.set_ylabel("Attention (previous time step)") elif isinstance(plot, int) and (plot in np.arange(1, self.h + 1)): i = self.input_size + plot - 1 ax.plot( np.arange(-self.input_size, self.h), attention[i, :], label=f"horizon {plot}", ) ax.axvline( x=0, color="black", linewidth=3, linestyle="--", label="prediction start" ) ax.set_title(f"Attention weight for horizon {plot}") ax.set_xlabel("time") ax.set_ylabel("Attention") ax.legend() else: raise ValueError( 'plot has to be in ["time","all","heatmap"] or integer in range(1,model.h)' ) plt.tight_layout() if output == "plot": plt.show() elif output == "figure": return fig else: raise ValueError(f"Invalid output: {output}. Expected 'plot' or 'figure'.") ``` ##### 3.1.1 Mean attention ```python theme={null} plot_attention(nf.models[0], plot="time") ``` ##### 3.1.2 Attention of all future time steps ```python theme={null} plot_attention(nf.models[0], plot="all") ``` ##### 3.1.3 Attention of a specific future time step ```python theme={null} plot_attention(nf.models[0], plot=8) ``` ### 3.2 Feature Importance #### 3.2.1 Global feature importance ```python theme={null} feature_importances = nf.models[0].feature_importances() feature_importances.keys() ``` ##### Static variable importances ```python theme={null} feature_importances["Static covariates"].sort_values(by="importance").plot(kind="barh") ``` ##### Past variable importances ```python theme={null} feature_importances["Past variable importance over time"].mean().sort_values().plot( kind="barh" ) ``` ##### Future variable importances ```python theme={null} feature_importances["Future variable importance over time"].mean().sort_values().plot( kind="barh" ) ``` #### 3.2.2 Variable importances over time ##### Future variable importance over time Importance of each future covariate at each future time step ```python theme={null} df = feature_importances["Future variable importance over time"] fig, ax = plt.subplots(figsize=(20, 10)) bottom = np.zeros(len(df.index)) for col in df.columns: p = ax.bar(np.arange(-len(df), 0), df[col].values, 0.6, label=col, bottom=bottom) bottom += df[col] ax.set_title("Future variable importance over time ponderated by attention") ax.set_ylabel("Importance") ax.set_xlabel("Time") ax.grid(True) ax.legend() plt.show() ``` ##### Past variable importance over time ```python theme={null} df = feature_importances["Past variable importance over time"] fig, ax = plt.subplots(figsize=(20, 10)) bottom = np.zeros(len(df.index)) for col in df.columns: p = ax.bar(np.arange(-len(df), 0), df[col].values, 0.6, label=col, bottom=bottom) bottom += df[col] ax.set_title("Past variable importance over time") ax.set_ylabel("Importance") ax.set_xlabel("Time") ax.legend() ax.grid(True) plt.show() ``` ##### Past variable importance over time ponderated by attention Decomposition of the importance of each time step based on importance of each variable at that time step ```python theme={null} df = feature_importances["Past variable importance over time"] mean_attention = ( nf.models[0] .attention_weights()[nf.models[0].input_size :, :] .mean(axis=0)[: nf.models[0].input_size] ) df = df.multiply(mean_attention, axis=0) fig, ax = plt.subplots(figsize=(20, 10)) bottom = np.zeros(len(df.index)) for col in df.columns: p = ax.bar(np.arange(-len(df), 0), df[col].values, 0.6, label=col, bottom=bottom) bottom += df[col] ax.set_title("Past variable importance over time ponderated by attention") ax.set_ylabel("Importance") ax.set_xlabel("Time") ax.legend() ax.grid(True) plt.plot( np.arange(-len(df), 0), mean_attention, color="black", marker="o", linestyle="-", linewidth=2, label="mean_attention", ) plt.legend() plt.show() ``` #### 3.2.3 Variable importance correlations over time Variables which gain and lose importance at same moments ```python theme={null} nf.models[0].feature_importance_correlations() ``` ## 4. Auxiliary Functions ### 4.1 Gating Mechanisms The Gated Residual Network (GRN) provides adaptive depth and network complexity capable of accommodating different size datasets. As residual connections allow for the network to skip the non-linear transformation of input $\mathbf{a}$ and context $\mathbf{c}$. The Gated Linear Unit (GLU) provides the flexibility of suppressing unnecessary parts of the GRN. Consider GRN’s output $\gamma$ then GLU transformation is defined by: $\mathrm{GLU}(\gamma) = \sigma(\mathbf{W}_{4}\gamma +b_{4}) \odot (\mathbf{W}_{5}\gamma +b_{5})$ Figure 2. Gated Residual Network. *Figure 2. Gated Residual Network.* ### 4.2 Variable Selection Networks TFT includes automated variable selection capabilities, through its variable selection network (VSN) components. The VSN takes the original input $\{\mathbf{x}^{(s)}, \mathbf{x}^{(h)}_{[:t]}, \mathbf{x}^{(f)}_{[:t]}\}$ and transforms it through embeddings or linear transformations into a high dimensional space $\{\mathbf{E}^{(s)}, \mathbf{E}^{(h)}_{[:t]}, \mathbf{E}^{(f)}_{[:t+H]}\}$. For the observed historic data, the embedding matrix $\mathbf{E}^{(h)}_{t}$ at time $t$ is a concatenation of $j$ variable $e^{(h)}_{t,j}$ embeddings: The variable selection weights are given by: $s^{(h)}_{t}=\mathrm{SoftMax}(\mathrm{GRN}(\mathbf{E}^{(h)}_{t},\mathbf{E}^{(s)}))$ The VSN processed features are then: $\tilde{\mathbf{E}}^{(h)}_{t}= \sum_{j} s^{(h)}_{j} \tilde{e}^{(h)}_{t,j}$ Figure 3. Variable Selection Network *Figure 3. Variable Selection Network* ### 4.3. Multi-Head Attention To avoid information bottlenecks from the classic Seq2Seq architecture, TFT incorporates a decoder-encoder attention mechanism inherited transformer architectures ([Li et. al 2019](https://arxiv.org/abs/1907.00235), [Vaswani et. al 2017](https://arxiv.org/abs/1706.03762)). It transforms the outputs of the LSTM encoded temporal features, and helps the decoder better capture long-term relationships. The original multihead attention for each component $H_{m}$ and its query, key, and value representations are denoted by $Q_{m}, K_{m}, V_{m}$, its transformation is given by: TFT modifies the original multihead attention to improve its interpretability. To do it it uses shared values $\tilde{V}$ across heads and employs additive aggregation, $\mathrm{InterpretableMultiHead}(Q,K,V) = \tilde{H} W_{M}$. The mechanism has a great resemblence to a single attention layer, but it allows for $M$ multiple attention weights, and can be therefore be interpreted as the average ensemble of $M$ single attention layers. # TiDE Source: https://nixtlaverse.nixtla.io/neuralforecast/models.tide.html TiDE: Time-series Dense Encoder with MLP-based architecture. Encoder-decoder model for long-term univariate forecasting with exogenous input support. Time-series Dense Encoder (`TiDE`) is a MLP-based univariate time-series forecasting model. `TiDE` uses Multi-layer Perceptrons (MLPs) in an encoder-decoder model for long-term time-series forecasting. In addition, this model can handle exogenous inputs. Figure 1. TiDE architecture. *Figure 1. TiDE architecture.* ## 1. TiDE ### `TiDE` ```python theme={null} TiDE( h, input_size, hidden_size=512, decoder_output_dim=32, temporal_decoder_dim=128, dropout=0.3, layernorm=True, num_encoder_layers=1, num_decoder_layers=1, temporal_width=4, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=1024, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) TiDE Time-series Dense Encoder (`TiDE`) is a MLP-based univariate time-series forecasting model. `TiDE` uses Multi-layer Perceptrons (MLPs) in an encoder-decoder model for long-term time-series forecasting. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | considered autorregresive inputs (lags), y=\[1,2,3,4] input\_size=2 -> lags=\[1,2]. | *required* | | `hidden_size` | [int](#int) | number of units for the dense MLPs. | 512 | | `decoder_output_dim` | [int](#int) | number of units for the output of the decoder. | 32 | | `temporal_decoder_dim` | [int](#int) | number of units for the hidden sizeof the temporal decoder. | 128 | | `dropout` | [float](#float) | dropout rate between (0, 1) . | 0.3 | | `layernorm` | [bool](#bool) | if True uses Layer Normalization on the MLP residual block outputs. | True | | `num_encoder_layers` | [int](#int) | number of encoder layers. | 1 | | `num_decoder_layers` | [int](#int) | number of decoder layers. | 1 | | `temporal_width` | [int](#int) | lower temporal projected dimension. | 4 | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | whether to exclude the target variable from the historic exogenous data. | False | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 1024 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Das, Abhimanyu, Weihao Kong, Andrew Leach, Shaan Mathur, Rajat Sen, and Rose Yu (2024). "Long-term Forecasting with TiDE: Time-series Dense Encoder."](http://arxiv.org/abs/2304.08424)
#### `TiDE.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `TiDE.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Examples ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import TiDE from neuralforecast.losses.pytorch import GMM from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test fcst = NeuralForecast( models=[ TiDE(h=12, input_size=24, loss=GMM(n_components=7, return_params=True, level=[80,90], weighted=True), max_steps=100, scaler_type='standard', futr_exog_list=['y_[lag12]'], hist_exog_list=None, stat_exog_list=['airline1'], ), ], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic) forecasts = fcst.predict(futr_df=Y_test_df) # Plot quantile predictions Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['TiDE-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['TiDE-lo-90'][-12:].values, y2=plot_df['TiDE-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.legend() plt.grid() ``` ## 2. Auxiliary Functions ### `MLPResidual` ```python theme={null} MLPResidual(input_dim, hidden_size, output_dim, dropout, layernorm) ``` Bases: [Module](#torch.nn.Module) MLPResidual # Time-LLM Source: https://nixtlaverse.nixtla.io/neuralforecast/models.timellm.html Time-LLM: Reprograms large language models for time series forecasting. Transforms forecasting tasks into language tasks using off-the-shelf LLM backbones. Time-LLM is a reprogramming framework to repurpose LLMs for general time series forecasting with the backbone language models kept intact. In other words, it transforms a forecasting task into a “language task” that can be tackled by an off-the-shelf LLM. **References** * [Ming Jin, Shiyu Wang, Lintao Ma, Zhixuan Chu, James Y. Zhang, Xiaoming Shi, Pin-Yu Chen, Yuxuan Liang, Yuan-Fang Li, Shirui Pan, Qingsong Wen. “Time-LLM: Time Series Forecasting by Reprogramming Large Language Models”](https://arxiv.org/abs/2310.01728) Figure 1. Time-LLM Architecture. *Figure 1. Time-LLM Architecture.* ## 1. Time-LLM ### Usage example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import TimeLLM from neuralforecast.utils import AirPassengersPanel Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test prompt_prefix = "The dataset contains data on monthly air passengers. There is a yearly seasonality" timellm = TimeLLM(h=12, input_size=36, llm='openai-community/gpt2', prompt_prefix=prompt_prefix, batch_size=16, valid_batch_size=16, windows_batch_size=16) nf = NeuralForecast( models=[timellm], freq='ME' ) nf.fit(df=Y_train_df, val_size=12) forecasts = nf.predict(futr_df=Y_test_df) ``` ## 2. Auxiliary Functions ### `ReprogrammingLayer` ```python theme={null} ReprogrammingLayer( d_model, n_heads, d_keys=None, d_llm=None, attention_dropout=0.1 ) ``` Bases: [Module](#torch.nn.Module) ReprogrammingLayer ### `FlattenHead` ```python theme={null} FlattenHead(n_vars, nf, target_window, head_dropout=0) ``` Bases: [Module](#torch.nn.Module) FlattenHead ### `PatchEmbedding` ```python theme={null} PatchEmbedding(d_model, patch_len, stride, dropout) ``` Bases: [Module](#torch.nn.Module) PatchEmbedding ### `TokenEmbedding` ```python theme={null} TokenEmbedding(c_in, d_model) ``` Bases: [Module](#torch.nn.Module) TokenEmbedding ### `ReplicationPad1d` ```python theme={null} ReplicationPad1d(padding) ``` Bases: [Module](#torch.nn.Module) ReplicationPad1d # TimeMixer Source: https://nixtlaverse.nixtla.io/neuralforecast/models.timemixer.html TimeMixer: Temporal mixing architecture for multivariate time series forecasting with multi-scale decomposition and frequency-domain feature extraction. Figure 1. Architecture of SOFTS. *Figure 1. Architecture of SOFTS.* ## 1. TimeMixer ### `TimeMixer` ```python theme={null} TimeMixer( h, input_size, n_series, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, d_model=32, d_ff=32, dropout=0.1, e_layers=4, top_k=5, decomp_method="moving_avg", moving_avg=25, channel_independence=0, down_sampling_layers=1, down_sampling_window=2, down_sampling_method="avg", use_norm=True, decoder_input_size_multiplier=0.5, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) TimeMixer **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `n_series` | [int](#int) | number of time-series. | *required* | | `stat_exog_list` | [list](#list) | static exogenous columns. | None | | `hist_exog_list` | [list](#list) | historic exogenous columns. | None | | `futr_exog_list` | [list](#list) | future exogenous columns. | None | | `d_model` | [int](#int) | dimension of the model. | 32 | | `d_ff` | [int](#int) | dimension of the fully-connected network. | 32 | | `dropout` | [float](#float) | dropout rate. | 0.1 | | `e_layers` | [int](#int) | number of encoder layers. | 4 | | `top_k` | [int](#int) | number of selected frequencies. | 5 | | `decomp_method` | [str](#str) | method of series decomposition \[moving\_avg, dft\_decomp]. | 'moving\_avg' | | `moving_avg` | [int](#int) | window size of moving average. | 25 | | `channel_independence` | [int](#int) | 0: channel dependence, 1: channel independence. | 0 | | `down_sampling_layers` | [int](#int) | number of downsampling layers. | 1 | | `down_sampling_window` | [int](#int) | size of downsampling window. | 2 | | `down_sampling_method` | [str](#str) | down sampling method \[avg, max, conv]. | 'avg' | | `use_norm` | [bool](#bool) | whether to normalize or not. | True | | `decoder_input_size_multiplier` | [float](#float) | 0.5. | 0.5 | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 32 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [keyword](#keyword) | trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Shiyu Wang, Haixu Wu, Xiaoming Shi, Tengge Hu, Huakun Luo, Lintao Ma, James Y. Zhang, Jun Zhou."TimeMixer: Decomposable Multiscale Mixing For Time Series Forecasting"](https://openreview.net/pdf?id=7oLshfEIC2)
#### `TimeMixer.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `TimeMixer.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import TimeMixer from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic from neuralforecast.losses.pytorch import MAE Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = TimeMixer(h=12, input_size=24, n_series=2, scaler_type='standard', max_steps=500, early_stop_patience_steps=-1, val_check_steps=5, learning_rate=1e-3, loss = MAE(), valid_loss=MAE(), batch_size=32 ) fcst = NeuralForecast(models=[model], freq='ME') fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['TimeMixer'], c='blue', label='median') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` Using `cross_validation` to forecast multiple historic values. ```python theme={null} fcst = NeuralForecast(models=[model], freq='M') forecasts = fcst.cross_validation(df=AirPassengersPanel, static_df=AirPassengersStatic, n_windows=2, step_size=12) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.loc['Airline1'] Y_df = AirPassengersPanel[AirPassengersPanel['unique_id']=='Airline1'] plt.plot(Y_df['ds'], Y_df['y'], c='black', label='True') plt.plot(Y_hat_df['ds'], Y_hat_df['TimeMixer'], c='blue', label='Forecast') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ## 2. Auxiliary Functions ### 2.1 Embedding ### `DataEmbedding_wo_pos` ```python theme={null} DataEmbedding_wo_pos(c_in, d_model, dropout=0.1, embed_type='fixed', freq='h') ``` Bases: [Module](#torch.nn.Module) DataEmbedding\_wo\_pos ### `DFT_series_decomp` ```python theme={null} DFT_series_decomp(top_k) ``` Bases: [Module](#torch.nn.Module) Series decomposition block ### 2.2 Mixing ### `PastDecomposableMixing` ```python theme={null} PastDecomposableMixing( seq_len, pred_len, down_sampling_window, down_sampling_layers, d_model, dropout, channel_independence, decomp_method, d_ff, moving_avg, top_k, ) ``` Bases: [Module](#torch.nn.Module) PastDecomposableMixing ### `MultiScaleTrendMixing` ```python theme={null} MultiScaleTrendMixing(seq_len, down_sampling_window, down_sampling_layers) ``` Bases: [Module](#torch.nn.Module) Top-down mixing trend pattern ### `MultiScaleSeasonMixing` ```python theme={null} MultiScaleSeasonMixing(seq_len, down_sampling_window, down_sampling_layers) ``` Bases: [Module](#torch.nn.Module) Bottom-up mixing season pattern # TimesNet Source: https://nixtlaverse.nixtla.io/neuralforecast/models.timesnet.html TimesNet: 2D-variation modeling with Inception blocks for capturing intraperiod and interperiod temporal patterns in univariate time series forecasting. The TimesNet univariate model tackles the challenge of modeling multiple intraperiod and interperiod temporal variations. The architecture has the following distinctive features: - An embedding layer that maps the input sequence into a latent space. - Transformation of 1D time series into 2D tensors, based on periods found by FFT. - A convolutional Inception block that captures temporal variations at different scales and between periods. **References** * [Haixu Wu and Tengge Hu and Yong Liu and Hang Zhou and Jianmin Wang and Mingsheng Long. TimesNet: Temporal 2D-Variation Modeling for General Time Series Analysis](https://openreview.net/pdf?id=ju_Uqw384Oq) - Based on the implementation in [https://github.com/thuml/Time-Series-Library](https://github.com/thuml/Time-Series-Library) (license: [https://github.com/thuml/Time-Series-Library/blob/main/LICENSE](https://github.com/thuml/Time-Series-Library/blob/main/LICENSE)) Figure 1. TimesNet Architecture. *Figure 1. TimesNet Architecture.* ## 1. TimesNet ### `TimesNet` ```python theme={null} TimesNet( h, input_size, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, hidden_size=64, dropout=0.1, conv_hidden_size=64, top_k=5, num_kernels=6, encoder_layers=2, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.0001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=64, inference_windows_batch_size=256, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="standard", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) TimesNet The TimesNet univariate model tackles the challenge of modeling multiple intraperiod and interperiod temporal variations. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | Length of input window (lags). | *required* | | `stat_exog_list` | list of str | optional (default=None), Static exogenous columns. | None | | `hist_exog_list` | list of str | optional (default=None), Historic exogenous columns. | None | | `futr_exog_list` | list of str | optional (default=None), Future exogenous columns. | None | | `cat_exog_list` | list of str | optional (default=None), exogenous columns (from `hist_exog_list` / `futr_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | optional (default=None), mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | The model skips the autoregressive features y\[t-input\_size:t] if True. | False | | `hidden_size` | [int](#int) | Size of embedding for embedding and encoders. | 64 | | `dropout` | [float](#float) | Dropout for embeddings. | 0.1 | | `conv_hidden_size` | [int](#int) | Channels of the Inception block. | 64 | | `top_k` | [int](#int) | Number of periods. | 5 | | `num_kernels` | [int](#int) | Number of kernels for the Inception block. | 6 | | `encoder_layers` | [int](#int) | Number of encoder layers. | 2 | | `loss` | PyTorch module | Instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | Instantiated validation loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | Maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate. | 0.0001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. If -1, no learning rate decay is performed. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. If -1, no early stopping is performed. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | Number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | Number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | Number of windows to sample in each training batch. | 64 | | `inference_windows_batch_size` | [int](#int) | Number of windows to sample in each inference batch. | 256 | | `start_padding_enabled` | [bool](#bool) | If True, the model will pad the time series with zeros at the beginning by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | Step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | Type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'standard' | | `random_seed` | [int](#int) | Random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | If True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional (default=None), Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional (default=None), User specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional (default=None), List of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional (default=None), List of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Haixu Wu and Tengge Hu and Yong Liu and Hang Zhou and Jianmin Wang and Mingsheng Long. TimesNet: Temporal 2D-Variation Modeling for General Time Series Analysis. https://openreview.net/pdf?id=ju\_Uqw384Oq](https://openreview.net/pdf?id=ju_Uqw384Oq)
#### `TimesNet.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `TimesNet.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.losses.pytorch import DistributionLoss from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = TimesNet(h=12, input_size=24, hidden_size = 16, conv_hidden_size = 32, loss=DistributionLoss(distribution='Normal', level=[80, 90]), scaler_type='standard', learning_rate=1e-3, max_steps=100, val_check_steps=50, early_stop_patience_steps=2) nf = NeuralForecast( models=[model], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = nf.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) if model.loss.is_distribution_output: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['TimesNet-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['TimesNet-lo-90'][-12:].values, y2=plot_df['TimesNet-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.legend() plt.plot() else: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['TimesNet'], c='blue', label='Forecast') plt.legend() plt.grid() ``` ## 2. Auxiliary Functions ### `Inception_Block_V1` ```python theme={null} Inception_Block_V1(in_channels, out_channels, num_kernels=6, init_weight=True) ``` Bases: [Module](#torch.nn.Module) Inception\_Block\_V1 ### `TimesBlock` ```python theme={null} TimesBlock(input_size, h, k, hidden_size, conv_hidden_size, num_kernels) ``` Bases: [Module](#torch.nn.Module) TimesBlock ### `FFT_for_Period` ```python theme={null} FFT_for_Period(x, k=2) ``` # TimeXer Source: https://nixtlaverse.nixtla.io/neuralforecast/models.timexer.html TimeXer: Cross-series attention transformer for multivariate forecasting with patch-based processing and exogenous variable support for complex temporal patterns. Figure 1. Architecture of TimeXer. *Figure 1. Architecture of TimeXer.* ## 1. TimeXer ### `TimeXer` ```python theme={null} TimeXer( h, input_size, n_series, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, patch_len=16, hidden_size=512, n_heads=8, e_layers=2, d_ff=2048, factor=1, dropout=0.1, use_norm=True, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) TimeXer **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | autorregresive inputs size, y=\[1,2,3,4] input\_size=2 -> y\_\[t-2:t]=\[1,2]. | *required* | | `n_series` | [int](#int) | number of time-series. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `patch_len` | [int](#int) | length of patches. | 16 | | `hidden_size` | [int](#int) | dimension of the model. | 512 | | `n_heads` | [int](#int) | number of heads. | 8 | | `e_layers` | [int](#int) | number of encoder layers. | 2 | | `d_ff` | [int](#int) | dimension of fully-connected layer. | 2048 | | `factor` | [int](#int) | attention factor. | 1 | | `dropout` | [float](#float) | dropout rate. | 0.1 | | `use_norm` | [bool](#bool) | whether to normalize or not. | True | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows in each batch. | 32 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Yuxuan Wang, Haixu Wu, Jiaxiang Dong, Guo Qin, Haoran Zhang, Yong Liu, Yunzhong Qiu, Jianmin Wang, Mingsheng Long. "TimeXer: Empowering Transformers for Time Series Forecasting with Exogenous Variables"](https://arxiv.org/abs/2402.19072)
#### `TimeXer.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `TimeXer.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import TimeXer from neuralforecast.losses.pytorch import MAE, MSE from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic, augment_calendar_df AirPassengersPanel, calendar_cols = augment_calendar_df(df=AirPassengersPanel, freq='M') Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = TimeXer(h=12, input_size=24, n_series=2, stat_exog_list=['airline1'], patch_len=12, hidden_size=128, n_heads=16, e_layers=2, d_ff=256, factor=1, dropout=0.1, use_norm=True, loss=MSE(), valid_loss=MAE(), early_stop_patience_steps=3, batch_size=32) fcst = NeuralForecast(models=[model], freq='ME') fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['TimeXer'], c='blue', label='Forecast') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ## 2. Auxiliary Functions ### `FlattenHead` ```python theme={null} FlattenHead(n_vars, nf, target_window, head_dropout=0) ``` Bases: [Module](#torch.nn.Module) ### `Encoder` ```python theme={null} Encoder(layers, norm_layer=None, projection=None) ``` Bases: [Module](#torch.nn.Module) ### `EncoderLayer` ```python theme={null} EncoderLayer( self_attention, cross_attention, d_model, d_ff=None, dropout=0.1, activation="relu", ) ``` Bases: [Module](#torch.nn.Module) ### `EnEmbedding` ```python theme={null} EnEmbedding(n_vars, d_model, patch_len, dropout) ``` Bases: [Module](#torch.nn.Module) # TSMixer Source: https://nixtlaverse.nixtla.io/neuralforecast/models.tsmixer.html TSMixer: MLP-based multivariate forecasting with time and feature mixing. Stacked mixing layers learn temporal and cross-sectional representations jointly. Time-Series Mixer (`TSMixer`) is a MLP-based multivariate time-series forecasting model. `TSMixer` jointly learns temporal and cross-sectional representations of the time-series by repeatedly combining time- and feature information using stacked mixing layers. A mixing layer consists of a sequential time- and feature Multi Layer Perceptron (`MLP`). Note: this model cannot handle exogenous inputs. If you want to use additional exogenous inputs, use `TSMixerx`. Figure 1. TSMixer for multivariate time series forecasting. *Figure 1. TSMixer for multivariate time series forecasting.* ## 1. TSMixer ### `TSMixer` ```python theme={null} TSMixer( h, input_size, n_series, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, exclude_insample_y=False, n_block=2, ff_dim=64, dropout=0.9, revin=True, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) TSMixer Time-Series Mixer (`TSMixer`) is a MLP-based multivariate time-series forecasting model. `TSMixer` jointly learns temporal and cross-sectional representations of the time-series by repeatedly combining time- and feature information using stacked mixing layers. A mixing layer consists of a sequential time- and feature Multi Layer Perceptron (`MLP`). **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | considered autorregresive inputs (lags), y=\[1,2,3,4] input\_size=2 -> lags=\[1,2]. | *required* | | `n_series` | [int](#int) | number of time-series. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `exclude_insample_y` | [bool](#bool) | if True excludes the target variable from the input features. | False | | `n_block` | [int](#int) | number of mixing layers in the model. | 2 | | `ff_dim` | [int](#int) | number of units for the second feed-forward layer in the feature MLP. | 64 | | `dropout` | [float](#float) | dropout rate between (0, 1) . | 0.9 | | `revin` | [bool](#bool) | if True uses Reverse Instance Normalization to process inputs and outputs. | True | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 32 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Chen, Si-An, Chun-Liang Li, Nate Yoder, Sercan O. Arik, and Tomas Pfister (2023). "TSMixer: An All-MLP Architecture for Time Series Forecasting."](http://arxiv.org/abs/2303.06053)
#### `TSMixer.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `TSMixer.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Examples Train model and forecast future values with `predict` method. ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import TSMixer from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic from neuralforecast.losses.pytorch import MAE, MQLoss Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = TSMixer(h=12, input_size=24, n_series=2, n_block=4, ff_dim=4, dropout=0, revin=True, scaler_type='standard', max_steps=500, early_stop_patience_steps=-1, val_check_steps=5, learning_rate=1e-3, loss=MQLoss(), batch_size=32 ) fcst = NeuralForecast(models=[model], freq='ME') fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline2'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['TSMixer-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['TSMixer-lo-90'][-12:].values, y2=plot_df['TSMixer-hi-90'][-12:].values, alpha=0.4, label='level 90') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` Using `cross_validation` to forecast multiple historic values. ```python theme={null} fcst = NeuralForecast(models=[model], freq='M') forecasts = fcst.cross_validation(df=AirPassengersPanel, static_df=AirPassengersStatic, n_windows=2, step_size=12) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.loc['Airline1'] Y_df = AirPassengersPanel[AirPassengersPanel['unique_id']=='Airline1'] plt.plot(Y_df['ds'], Y_df['y'], c='black', label='True') plt.plot(Y_hat_df['ds'], Y_hat_df['TSMixer-median'], c='blue', label='Forecast') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ## 2. Auxiliary Functions ### 2.1 Mixing layers A mixing layer consists of a sequential time- and feature Multi Layer Perceptron ([`MLP`](./models.mlp.html#mlp)). ### `MixingLayer` ```python theme={null} MixingLayer(n_series, input_size, dropout, ff_dim) ``` Bases: [Module](#torch.nn.Module) MixingLayer ### `FeatureMixing` ```python theme={null} FeatureMixing(n_series, input_size, dropout, ff_dim) ``` Bases: [Module](#torch.nn.Module) FeatureMixing ### `TemporalMixing` ```python theme={null} TemporalMixing(n_series, input_size, dropout) ``` Bases: [Module](#torch.nn.Module) TemporalMixing # TSMixerx Source: https://nixtlaverse.nixtla.io/neuralforecast/models.tsmixerx.html TSMixerx: TSMixer with exogenous variables. MLP-based multivariate forecasting combines temporal-feature mixing with static and future covariate support. Time-Series Mixer exogenous (`TSMixerx`) is a MLP-based multivariate time-series forecasting model, with capability for additional exogenous inputs. `TSMixerx` jointly learns temporal and cross-sectional representations of the time-series by repeatedly combining time and feature information using stacked mixing layers. A mixing layer consists of a sequential time- and feature Multi Layer Perceptron (`MLP`). Figure 2. TSMixerX for multivariate time series forecasting. *Figure 2. TSMixerX for multivariate time series forecasting.* ## 1. TSMixerx ### `TSMixerx` ```python theme={null} TSMixerx( h, input_size, n_series, futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, n_block=2, ff_dim=64, dropout=0.0, revin=True, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) TSMixerx Time-Series Mixer exogenous (`TSMixerx`) is a MLP-based multivariate time-series forecasting model, with capability for additional exogenous inputs. `TSMixerx` jointly learns temporal and cross-sectional representations of the time-series by repeatedly combining time- and feature information using stacked mixing layers. A mixing layer consists of a sequential time- and feature Multi Layer Perceptron (`MLP`). **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | considered autorregresive inputs (lags), y=\[1,2,3,4] input\_size=2 -> lags=\[1,2]. | *required* | | `n_series` | [int](#int) | number of time-series. | *required* | | `futr_exog_list` | str list | future exogenous columns. | None | | `hist_exog_list` | str list | historic exogenous columns. | None | | `stat_exog_list` | str list | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | if True excludes insample\_y from the model. | False | | `n_block` | [int](#int) | number of mixing layers in the model. | 2 | | `ff_dim` | [int](#int) | number of units for the second feed-forward layer in the feature MLP. | 64 | | `dropout` | [float](#float) | dropout rate between (0, 1) . | 0.0 | | `revin` | [bool](#bool) | if True uses Reverse Instance Normalization on `insample_y` and applies it to the outputs. | True | | `loss` | PyTorch module | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch. | 32 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Chen, Si-An, Chun-Liang Li, Nate Yoder, Sercan O. Arik, and Tomas Pfister (2023). "TSMixer: An All-MLP Architecture for Time Series Forecasting."](http://arxiv.org/abs/2303.06053)
#### `TSMixerx.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `TSMixerx.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Examples Train model and forecast future values with `predict` method. ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import TSMixerx from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic from neuralforecast.losses.pytorch import GMM Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = TSMixerx(h=12, input_size=24, n_series=2, stat_exog_list=['airline1'], futr_exog_list=['trend'], n_block=4, ff_dim=4, revin=True, scaler_type='robust', max_steps=500, early_stop_patience_steps=-1, val_check_steps=5, learning_rate=1e-3, loss = GMM(n_components=10, weighted=True), batch_size=32 ) fcst = NeuralForecast(models=[model], freq='ME') fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['TSMixerx-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['TSMixerx-lo-90'][-12:].values, y2=plot_df['TSMixerx-hi-90'][-12:].values, alpha=0.4, label='level 90') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` Using `cross_validation` to forecast multiple historic values. ```python theme={null} fcst = NeuralForecast(models=[model], freq='M') forecasts = fcst.cross_validation(df=AirPassengersPanel, static_df=AirPassengersStatic, n_windows=2, step_size=12) # Plot predictions fig, ax = plt.subplots(1, 1, figsize = (20, 7)) Y_hat_df = forecasts.loc['Airline1'] Y_df = AirPassengersPanel[AirPassengersPanel['unique_id']=='Airline1'] plt.plot(Y_df['ds'], Y_df['y'], c='black', label='True') plt.plot(Y_hat_df['ds'], Y_hat_df['TSMixerx-median'], c='blue', label='Forecast') ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Year', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ## 2. Auxiliary Functions ### 2.1 Mixing layers A mixing layer consists of a sequential time- and feature Multi Layer Perceptron ([`MLP`](./models.mlp.html#mlp)). ### `MixingLayerWithStaticExogenous` ```python theme={null} MixingLayerWithStaticExogenous(h, dropout, ff_dim, stat_input_size) ``` Bases: [Module](#torch.nn.Module) MixingLayerWithStaticExogenous ### `MixingLayer` ```python theme={null} MixingLayer(in_features, out_features, h, dropout, ff_dim) ``` Bases: [Module](#torch.nn.Module) MixingLayer ### `FeatureMixing` ```python theme={null} FeatureMixing(in_features, out_features, h, dropout, ff_dim) ``` Bases: [Module](#torch.nn.Module) FeatureMixing ### `TemporalMixing` ```python theme={null} TemporalMixing(num_features, h, dropout) ``` Bases: [Module](#torch.nn.Module) TemporalMixing ### 2.2 Reversible InstanceNormalization An Instance Normalization Layer that is reversible, based on [this reference implementation](https://github.com/google-research/google-research/blob/master/tsmixer/tsmixer_basic/models/rev_in.py). # Vanilla Transformer Source: https://nixtlaverse.nixtla.io/neuralforecast/models.vanillatransformer.html Vanilla Transformer: Classic attention-based architecture for time series. Full O(L^2) attention mechanism with encoder-decoder for long-sequence forecasting. Vanilla Transformer, following implementation of the Informer paper, used as baseline. The architecture has three distinctive features: * Full-attention mechanism with O(L^2) time and memory complexity. * Classic encoder-decoder proposed by Vaswani et al. (2017) with a multi-head attention mechanism. * An MLP multi-step decoder that predicts long time-series sequences in a single forward operation rather than step-by-step. The Vanilla Transformer model utilizes a three-component approach to define its embedding: * It employs encoded autoregressive features obtained from a convolution network. * It uses window-relative positional embeddings derived from harmonic functions. * Absolute positional embeddings obtained from calendar features are utilized. **References** * [Haoyi Zhou, Shanghang Zhang, Jieqi Peng, Shuai Zhang, Jianxin Li, Hui Xiong, Wancai Zhang. “Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting”](https://arxiv.org/abs/2012.07436) Figure 1. Transformer Architecture. *Figure 1. Transformer Architecture.* ## Vanilla Transformer ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import VanillaTransformer from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = VanillaTransformer(h=12, input_size=24, hidden_size=16, conv_hidden_size=32, n_head=2, loss=MAE(), scaler_type='robust', learning_rate=1e-3, max_steps=500, val_check_steps=50, early_stop_patience_steps=2) nf = NeuralForecast( models=[model], freq='ME' ) nf.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = nf.predict(futr_df=Y_test_df) Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) if model.loss.is_distribution_output: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['VanillaTransformer-median'], c='blue', label='median') plt.fill_between(x=plot_df['ds'][-12:], y1=plot_df['VanillaTransformer-lo-90'][-12:].values, y2=plot_df['VanillaTransformer-hi-90'][-12:].values, alpha=0.4, label='level 90') plt.grid() plt.legend() plt.plot() else: plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['VanillaTransformer'], c='blue', label='Forecast') plt.legend() plt.grid() ``` # XLinear Source: https://nixtlaverse.nixtla.io/neuralforecast/models.xlinear.html XLinear: A MLP-based model for multivariate forecasting with exogenous features. XLinear is a MLP-based model for multivariate time series forecasting that uses gating mechanisms for temporal and cross-channel interactions. The architecture consists of temporal gating with a global token to capture global temporal patterns, followed by cross-channel gating to model dependencies between different time series. **References** * [Xinyang, C., et al. "XLinear: A Lightweight and Accurate MLP-Based Model for Long-Term Time Series Forecasting with Exogenous Inputs"](https://arxiv.org/abs/2601.09237) Figure 1. Architecture of XLinear *Figure 1. Architecture of XLinear* ## XLinear ### `XLinear` ```python theme={null} XLinear( h, input_size, n_series, stat_exog_list=None, hist_exog_list=None, futr_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, hidden_size=128, temporal_ff=256, channel_ff=8, temporal_dropout=0.0, channel_dropout=0.0, embed_dropout=0.0, head_dropout=0.0, use_norm=True, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=32, inference_windows_batch_size=32, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="identity", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) XLinear XLinear is a linear-based model for multivariate time series forecasting that uses gating mechanisms for temporal and cross-channel interactions. The architecture consists of temporal gating with a global token to capture global temporal patterns, followed by cross-channel gating to model dependencies between different time series. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `input_size` | [int](#int) | Input size, y=\[1,2,3,4] input\_size=2 -> lags=\[1,2]. | *required* | | `n_series` | [int](#int) | Number of time series. | *required* | | `stat_exog_list` | str list | Static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `hist_exog_list` | str list | Historic exogenous columns. | None | | `futr_exog_list` | str list | Future exogenous columns. | None | | `hidden_size` | [int](#int) | Dimension of the model embedding. | 128 | | `temporal_ff` | [int](#int) | Dimension of temporal feedforward layer in gating block. | 256 | | `channel_ff` | [int](#int) | Dimension of cross-channel feedforward layer in gating block. | 8 | | `temporal_dropout` | [float](#float) | Dropout rate for temporal gating. | 0.0 | | `channel_dropout` | [float](#float) | Dropout rate for cross-channel gating. | 0.0 | | `embed_dropout` | [float](#float) | Dropout rate for embedding projection. | 0.0 | | `head_dropout` | [float](#float) | Dropout rate for output head. | 0.0 | | `use_norm` | [bool](#bool) | Whether to use RevIN normalization. | True | | `loss` | PyTorch module | Instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | PyTorch module | Instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | Maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | Number of different series in each batch. | 32 | | `valid_batch_size` | [int](#int) | Number of different series in each validation and test batch, if None uses batch\_size. | None | | `windows_batch_size` | [int](#int) | Number of windows to sample in each training batch. | 32 | | `inference_windows_batch_size` | [int](#int) | Number of windows to sample in each inference batch, -1 uses all. | 32 | | `start_padding_enabled` | [bool](#bool) | If True, the model will pad the time series with zeros at the beginning. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | Step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'identity' | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | If True, TimeSeriesDataLoader drops last non-full batch. | False | | `alias` | [str](#str) | Optional custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | Optional user specified optimizer. | None | | `optimizer_kwargs` | [dict](#dict) | Optional list of parameters used by the user specified optimizer. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | Optional user specified lr\_scheduler. | None | | `lr_scheduler_kwargs` | [dict](#dict) | Optional list of parameters used by the user specified lr\_scheduler. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [keyword](#keyword) | trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Xinyang, C., et al. "XLinear: A Lightweight and Accurate MLP-Based Model for Long-Term Time Series Forecasting with Exogenous Inputs"](https://arxiv.org/abs/2601.09237)
#### `XLinear.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `XLinear.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import XLinear from neuralforecast.losses.pytorch import MAE from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = XLinear(h=12, input_size=24, n_series=2, stat_exog_list=['airline1'], hist_exog_list=["y_[lag12]"], futr_exog_list=['trend'], loss = MAE(), scaler_type='robust', learning_rate=1e-3, max_steps=200, val_check_steps=10, early_stop_patience_steps=2) fcst = NeuralForecast( models=[model], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['XLinear'], c='blue', label='median') plt.grid() plt.legend() plt.plot() ``` # xLSTM Source: https://nixtlaverse.nixtla.io/neuralforecast/models.xlstm.html xLSTM: An extension of the LSTM model. xLSTM is an RNN-based model for sequence modeling that extends the classical Long Short-Term Memory architecture with exponential gating and novel memory structures. The architecture introduces two new memory cell variants: sLSTM, which uses a scalar memory with a scalar update rule and a new cross-cell memory mixing mechanism, and mLSTM, which replaces the scalar cell state with a matrix memory updated via a covariance (outer product) rule, enabling full parallelizability. These cell variants are integrated into residual blocks to form xLSTM blocks, which are then stacked into full xLSTM architectures. **References** * [Maximilian, B., et al. "xLSTM: Extended Long Short-Term Memory"](https://arxiv.org/abs/2405.04517) Figure 1. Architecture of xLSTM *Figure 1. Architecture of xLSTM* ## xLSTM ### `xLSTM` ```python theme={null} xLSTM( h, input_size=-1, inference_input_size=None, h_train=1, encoder_n_blocks=2, encoder_hidden_size=128, encoder_bias=True, encoder_dropout=0.1, decoder_hidden_size=128, decoder_layers=2, decoder_dropout=0.0, decoder_activation="GELU", backbone="mLSTM", futr_exog_list=None, hist_exog_list=None, stat_exog_list=None, cat_exog_list=None, categorical_cardinalities=None, cat_emb_dim="fastai", exclude_insample_y=False, recurrent=False, loss=MAE(), valid_loss=None, max_steps=1000, learning_rate=0.001, num_lr_decays=-1, early_stop_patience_steps=-1, val_monitor="ptl/val_loss", val_check_steps=100, batch_size=32, valid_batch_size=None, windows_batch_size=128, inference_windows_batch_size=1024, start_padding_enabled=False, training_data_availability_threshold=0.0, step_size=1, scaler_type="robust", random_seed=1, drop_last_loader=False, alias=None, optimizer=None, optimizer_kwargs=None, lr_scheduler=None, lr_scheduler_kwargs=None, dataloader_kwargs=None, **trainer_kwargs ) ``` Bases: [BaseModel](#neuralforecast.common._base_model.BaseModel) xLSTM xLSTM encoder, with MLP decoder. **Parameters:** | Name | Type | Description | Default | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | `h` | [int](#int) | forecast horizon. | *required* | | `input_size` | [int](#int) | considered autorregresive inputs (lags), y=\[1,2,3,4] input\_size=2 -> lags=\[1,2]. | -1 | | `encoder_n_blocks` | [int](#int) | number of blocks for the xLSTM. | 2 | | `encoder_hidden_size` | [int](#int) | units for the xLSTM's hidden state size. | 128 | | `encoder_bias` | [bool](#bool) | whether or not to use biases within xLSTM blocks. | True | | `encoder_dropout` | [float](#float) | dropout regularization applied within xLSTM blocks. | 0.1 | | `decoder_hidden_size` | [int](#int) | size of hidden layer for the MLP decoder. | 128 | | `decoder_layers` | [int](#int) | number of layers for the MLP decoder. | 2 | | `decoder_dropout` | [float](#float) | dropout regularization applied within the MLP decoder. | 0.0 | | `decoder_activation` | [str](#str) | activation function for the MLP decoder, see [activations collection](https://docs.pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity). | 'GELU' | | `backbone` | [str](#str) | backbone for the xLSTM, either 'sLSTM' or 'mLSTM'. | 'mLSTM' | | `futr_exog_list` | [List](#List)\[[str](#str)] | future exogenous columns. | None | | `hist_exog_list` | [list](#list) | historic exogenous columns. | None | | `stat_exog_list` | [list](#list) | static exogenous columns. | None | | `cat_exog_list` | str list | exogenous columns (from `hist_exog_list` / `futr_exog_list` / `stat_exog_list`) to embed instead of scale. | None | | `categorical_cardinalities` | [dict](#dict) | mapping from each categorical column to its number of distinct categories. | None | | `cat_emb_dim` | [str](#str) or [int](#int) | categorical embedding size strategy ('fastai', 'sqrt', 'half') or an explicit integer. | 'fastai' | | `exclude_insample_y` | [bool](#bool) | whether to exclude the target variable from the input. | False | | `recurrent` | [bool](#bool) | whether to produce forecasts recursively (True) or direct (False). | False | | `loss` | [Module](#torch.nn.Module) | instantiated train loss class from [losses collection](./losses.pytorch.html). | [MAE](#neuralforecast.losses.pytorch.MAE)() | | `valid_loss` | [Module](#torch.nn.Module) | instantiated valid loss class from [losses collection](./losses.pytorch.html). | None | | `max_steps` | [int](#int) | maximum number of training steps. | 1000 | | `learning_rate` | [float](#float) | Learning rate between (0, 1). | 0.001 | | `num_lr_decays` | [int](#int) | Number of learning rate decays, evenly distributed across max\_steps. | -1 | | `early_stop_patience_steps` | [int](#int) | Number of validation iterations before early stopping. | -1 | | `val_monitor` | [str](#str) | metric to monitor for early stopping. Valid options: "ptl/val\_loss", "valid\_loss", "train\_loss". Default: "ptl/val\_loss". | 'ptl/val\_loss' | | `val_check_steps` | [int](#int) | Number of training steps between every validation loss check. | 100 | | `batch_size` | [int](#int) | number of differentseries in each batch. | 32 | | `valid_batch_size` | [int](#int) | number of different series in each validation and test batch. | None | | `windows_batch_size` | [int](#int) | number of windows to sample in each training batch, default uses all. | 128 | | `inference_windows_batch_size` | [int](#int) | number of windows to sample in each inference batch, -1 uses all. | 1024 | | `start_padding_enabled` | [bool](#bool) | if True, the model will pad the time series with zeros at the beginning, by input size. | False | | `training_data_availability_threshold` | [Union](#Union)\[[float](#float), [List](#List)\[[float](#float)]] | minimum fraction of valid data points required for training windows. Single float applies to both insample and outsample; list of two floats specifies \[insample\_fraction, outsample\_fraction]. Default 0.0 allows windows with only 1 valid data point (current behavior). | 0.0 | | `step_size` | [int](#int) | step size between each window of temporal data. | 1 | | `scaler_type` | [str](#str) | type of scaler for temporal inputs normalization see [temporal scalers](https://github.com/Nixtla/neuralforecast/blob/main/neuralforecast/common/_scalers.py). | 'robust' | | `random_seed` | [int](#int) | random\_seed for pytorch initializer and numpy generators. | 1 | | `drop_last_loader` | [bool](#bool) | if True `TimeSeriesDataLoader` drops last non-full batch. | False | | `alias` | [str](#str) | optional, Custom name of the model. | None | | `optimizer` | Subclass of 'torch.optim.Optimizer' | optional, user specified optimizer instead of the default choice (Adam). | None | | `optimizer_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `optimizer`. | None | | `lr_scheduler` | Subclass of 'torch.optim.lr\_scheduler.LRScheduler' | optional, user specified lr\_scheduler instead of the default choice (StepLR). | None | | `lr_scheduler_kwargs` | [dict](#dict) | optional, list of parameters used by the user specified `lr_scheduler`. | None | | `dataloader_kwargs` | [dict](#dict) | optional, list of parameters passed into the PyTorch Lightning dataloader by the `TimeSeriesDataLoader`. | None | | `**trainer_kwargs` | [int](#int) | keyword trainer arguments inherited from [PyTorch Lightning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). | |
References * [Maximilian Beck, Korbinian Pöppel, Markus Spanring, Andreas Auer, Oleksandra Prudnikova, Michael Kopp, Günter Klambauer, Johannes Brandstetter, Sepp Hochreiter (2024). "xLSTM: Extended Long Short-Term Memory"](https://arxiv.org/abs/2405.04517)
#### `xLSTM.fit` ```python theme={null} fit( dataset, val_size=0, test_size=0, random_seed=None, distributed_config=None ) ``` Fit. The `fit` method, optimizes the neural network's weights using the initialization parameters (`learning_rate`, `windows_batch_size`, ...) and the `loss` function as defined during the initialization. Within `fit` we use a PyTorch Lightning `Trainer` that inherits the initialization's `self.trainer_kwargs`, to customize its inputs, see [PL's trainer arguments](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer). The method is designed to be compatible with SKLearn-like classes and in particular to be compatible with the StatsForecast library. By default the `model` is not saving training checkpoints to protect disk memory, to get them change `enable_checkpointing=True` in `__init__`. **Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `val_size` | [int](#int) | Validation size for temporal cross-validation. | 0 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `test_size` | [int](#int) | Test size for temporal cross-validation. | 0 | **Returns:** | Type | Description | | ---- | ----------- | | None | | #### `xLSTM.predict` ```python theme={null} predict( dataset, test_size=None, step_size=1, random_seed=None, quantiles=None, h=None, explainer_config=None, **data_module_kwargs ) ``` Predict. Neural network prediction with PL's `Trainer` execution of `predict_step`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `dataset` | [TimeSeriesDataset](#TimeSeriesDataset) | NeuralForecast's `TimeSeriesDataset`, see [documentation](./tsdataset.html). | *required* | | `test_size` | [int](#int) | Test size for temporal cross-validation. | None | | `step_size` | [int](#int) | Step size between each window. | 1 | | `random_seed` | [int](#int) | Random seed for pytorch initializer and numpy generators, overwrites model.**init**'s. | None | | `quantiles` | [list](#list) | Target quantiles to predict. | None | | `h` | [int](#int) | Prediction horizon, if None, uses the model's fitted horizon. Defaults to None. | None | | `explainer_config` | [dict](#dict) | configuration for explanations. | None | | `**data_module_kwargs` | [dict](#dict) | PL's TimeSeriesDataModule args, see [documentation](https://pytorch-lightning.readthedocs.io/en/1.6.1/extensions/datamodules.html#using-a-datamodule). | | **Returns:** | Type | Description | | ---- | ----------- | | None | | ### Usage Example ```python theme={null} import pandas as pd import matplotlib.pyplot as plt from neuralforecast import NeuralForecast from neuralforecast.models import xLSTM from neuralforecast.losses.pytorch import MAE from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test model = xLSTM(h=12, input_size=24, stat_exog_list=['airline1'], hist_exog_list=["y_[lag12]"], futr_exog_list=['trend'], loss = MAE(), scaler_type='robust', learning_rate=1e-3, max_steps=200, val_check_steps=10, early_stop_patience_steps=2) fcst = NeuralForecast( models=[model], freq='ME' ) fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12) forecasts = fcst.predict(futr_df=Y_test_df) # Plot predictions Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds']) plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1) plot_df = pd.concat([Y_train_df, plot_df]) plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1) plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True') plt.plot(plot_df['ds'], plot_df['xLSTM'], c='blue', label='median') plt.grid() plt.legend() plt.plot() ``` # PyTorch Dataset/Loader Source: https://nixtlaverse.nixtla.io/neuralforecast/tsdataset.html PyTorch Dataset and DataLoader classes for time series. TimeSeriesDataset and TimeSeriesDataModule for efficient batch processing with Lightning integration. ## Torch Time Series Dataset ### `TimeSeriesLoader` ```python theme={null} TimeSeriesLoader(dataset, **kwargs) ``` Bases: [DataLoader](#torch.utils.data.DataLoader) TimeSeriesLoader DataLoader. Small change to PyTorch's Data loader. Combines a dataset and a sampler, and provides an iterable over the given dataset. The class `~torch.utils.data.DataLoader` supports both map-style and iterable-style datasets with single- or multi-process loading, customizing loading order and optional automatic batching (collation) and memory pinning. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------------------------- | -------------------------------------------------------------------------- | ---------- | | `dataset` | | Dataset to load data from. | *required* | | `batch_size` | [int](#int) | How many samples per batch to load. Defaults to 1. | *required* | | `shuffle` | [bool](#bool) | Set to True to have the data reshuffled at every epoch. Defaults to False. | *required* | | `sampler` | [Sampler](#Sampler) or [Iterable](#Iterable) | Defines the strategy to draw samples from the dataset. | *required* | | `drop_last` | [bool](#bool) | Set to True to drop the last incomplete batch. Defaults to False. | *required* | | `**kwargs` | | Additional keyword arguments for DataLoader. | | ### `BaseTimeSeriesDataset` ```python theme={null} BaseTimeSeriesDataset( temporal_cols, max_size, min_size, y_idx, static=None, static_cols=None ) ``` Bases: [Dataset](#torch.utils.data.Dataset) Base class for time series datasets. **Parameters:** | Name | Type | Description | Default | | --------------- | ----------------------------------------- | ----------------------------------- | ----------------- | | `temporal_cols` | | Column names for temporal features. | *required* | | `max_size` | [int](#int) | Maximum size of time series. | *required* | | `min_size` | [int](#int) | Minimum size of time series. | *required* | | `y_idx` | [int](#int) | Index of target variable. | *required* | | `static` | [Optional](#typing.Optional) | Static features array. | None | | `static_cols` | [Optional](#typing.Optional) | Column names for static features. | None | ### `LocalFilesTimeSeriesDataset` ```python theme={null} LocalFilesTimeSeriesDataset( files_ds, temporal_cols, id_col, time_col, target_col, last_times, indices, max_size, min_size, y_idx, static=None, static_cols=None, ) ``` Bases: [BaseTimeSeriesDataset](#neuralforecast.tsdataset.BaseTimeSeriesDataset) Time series dataset that loads data from local files. **Parameters:** | Name | Type | Description | Default | | --------------- | ----------------------------------------------- | ----------------------------------- | ----------------- | | `files_ds` | [List](#typing.List)\[[str](#str)] | List of file paths. | *required* | | `temporal_cols` | | Column names for temporal features. | *required* | | `id_col` | [str](#str) | Name of ID column. | *required* | | `time_col` | [str](#str) | Name of time column. | *required* | | `target_col` | [str](#str) | Name of target column. | *required* | | `last_times` | | Last time for each time series. | *required* | | `indices` | | Series indices. | *required* | | `max_size` | [int](#int) | Maximum size of time series. | *required* | | `min_size` | [int](#int) | Minimum size of time series. | *required* | | `y_idx` | [int](#int) | Index of target variable. | *required* | | `static` | [Optional](#typing.Optional) | Static features array. | None | | `static_cols` | [Optional](#typing.Optional) | Column names for static features. | None | #### `LocalFilesTimeSeriesDataset.from_data_directories` ```python theme={null} from_data_directories( directories, static_df=None, exogs=[], id_col="unique_id", time_col="ds", target_col="y", ) ``` Create dataset from data directories. Expects directories to be a list of directories of the form \[unique\_id=id\_0, unique\_id=id\_1, ...]. Each directory should contain the timeseries corresponding to that unique\_id, represented as a pandas or polars DataFrame. The timeseries can be entirely contained in one parquet file or split between multiple, but within each parquet files the timeseries should be sorted by time. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------------------------- | -------------------------------------------------- | ------------------------- | | `directories` | | List of directory paths. | *required* | | `static_df` | [Optional](#typing.Optional) | Static features DataFrame. | None | | `exogs` | [List](#typing.List) | List of exogenous variable names. Defaults to \[]. | \[] | | `id_col` | [str](#str) | Name of ID column. Defaults to "unique\_id". | 'unique\_id' | | `time_col` | [str](#str) | Name of time column. Defaults to "ds". | 'ds' | | `target_col` | [str](#str) | Name of target column. Defaults to "y". | 'y' | **Returns:** | Name | Type | Description | | ----------------------------- | ---- | --------------------------------- | | `LocalFilesTimeSeriesDataset` | | Dataset created from directories. | ### `TimeSeriesDataset` ```python theme={null} TimeSeriesDataset( temporal, temporal_cols, indptr, y_idx, static=None, static_cols=None ) ``` Bases: [BaseTimeSeriesDataset](#neuralforecast.tsdataset.BaseTimeSeriesDataset) Time series dataset implementation. **Parameters:** | Name | Type | Description | Default | | --------------- | ----------------------------------------- | ---------------------------------------- | ----------------- | | `temporal` | | Temporal data array. | *required* | | `temporal_cols` | | Column names for temporal features. | *required* | | `indptr` | | Index pointers for time series grouping. | *required* | | `y_idx` | [int](#int) | Index of target variable. | *required* | | `static` | [Optional](#typing.Optional) | Static features array. | None | | `static_cols` | [Optional](#typing.Optional) | Column names for static features. | None | #### `TimeSeriesDataset.append` ```python theme={null} append(futr_dataset) ``` Add future observations to the dataset. **Parameters:** | Name | Type | Description | Default | | -------------- | ----------------------------------------------------------------------------- | ------------------------- | ---------- | | `futr_dataset` | [TimeSeriesDataset](#neuralforecast.tsdataset.TimeSeriesDataset) | Future dataset to append. | *required* | **Returns:** | Name | Type | Description | | ------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------- | | `TimeSeriesDataset` | [TimeSeriesDataset](#neuralforecast.tsdataset.TimeSeriesDataset) | Copy of dataset with future observations appended. | **Raises:** | Type | Description | | -------------------------------------- | -------------------------------------------- | | [ValueError](#ValueError) | If datasets have different number of groups. | #### `TimeSeriesDataset.trim_dataset` ```python theme={null} trim_dataset(dataset, left_trim=0, right_trim=0) ``` Trim temporal information from a dataset. Returns temporal indexes \[t+left:t-right] for all series. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------ | ------------------------------------------------------------- | -------------- | | `dataset` | | Dataset to trim. | *required* | | `left_trim` | [int](#int) | Number of observations to trim from the left. Defaults to 0. | 0 | | `right_trim` | [int](#int) | Number of observations to trim from the right. Defaults to 0. | 0 | **Returns:** | Name | Type | Description | | ------------------- | ---- | ---------------- | | `TimeSeriesDataset` | | Trimmed dataset. | **Raises:** | Type | Description | | ------------------------------------ | ------------------------------------------- | | [Exception](#Exception) | If trim size exceeds minimum series length. | ### `TimeSeriesDataModule` ```python theme={null} TimeSeriesDataModule( dataset, batch_size=32, valid_batch_size=1024, drop_last=False, shuffle_train=True, **dataloaders_kwargs ) ``` Bases: [LightningDataModule](#pytorch_lightning.LightningDataModule) PyTorch Lightning data module for time series datasets. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------ | | `dataset` | [BaseTimeSeriesDataset](#neuralforecast.tsdataset.BaseTimeSeriesDataset) | Time series dataset. | *required* | | `batch_size` | [int](#int) | Batch size for training. Defaults to 32. | 32 | | `valid_batch_size` | [int](#int) | Batch size for validation. Defaults to 1024. | 1024 | | `drop_last` | [bool](#bool) | Whether to drop the last incomplete batch. Defaults to False. | False | | `shuffle_train` | [bool](#bool) | Whether to shuffle training data. Defaults to True. | True | | `**dataloaders_kwargs` | | Additional keyword arguments for data loaders. | | ### Example ```python theme={null} import lightning.pytorch as L import torch.utils.data as data from pytorch_lightning.demos.boring_classes import RandomDataset class MyDataModule(L.LightningDataModule): def prepare_data(self): # download, IO, etc. Useful with shared filesystems # only called on 1 GPU/TPU in distributed ... def setup(self, stage): # make assignments here (val/train/test split) # called on every process in DDP dataset = RandomDataset(1, 100) self.train, self.val, self.test = data.random_split( dataset, [80, 10, 10], generator=torch.Generator().manual_seed(42) ) def train_dataloader(self): return data.DataLoader(self.train) def val_dataloader(self): return data.DataLoader(self.val) def test_dataloader(self): return data.DataLoader(self.test) def on_exception(self, exception): # clean up state after the trainer faced an exception ... def teardown(self): # clean up state after the trainer stops, delete files... # called on every process in DDP ...* ``` ```python theme={null} # To test correct future_df wrangling of the `update_df` method # We are checking that we are able to recover the AirPassengers dataset # using the dataframe or splitting it into parts and initializing. ``` # Example Data Source: https://nixtlaverse.nixtla.io/neuralforecast/utils.html NeuralForecast utility functions and datasets. Includes AirPassengers data, time feature generation, prediction intervals, and synthetic panel data generators. The `core.NeuralForecast` class allows you to efficiently fit multiple `NeuralForecast` models for large sets of time series. It operates with pandas DataFrame `df` that identifies individual series and datestamps with the `unique_id` and `ds` columns, and the `y` column denotes the target time series variable. To assist development, we declare useful datasets that we use throughout all `NeuralForecast`'s unit tests. ## 1. Synthetic Panel Data ### `generate_series` ```python theme={null} generate_series( n_series, freq="D", min_length=50, max_length=500, n_temporal_features=0, n_static_features=0, equal_ends=False, seed=0, ) ``` Generate Synthetic Panel Series. Generates `n_series` of frequency `freq` of different lengths in the interval \[`min_length`, `max_length`]. If `n_temporal_features > 0`, then each serie gets temporal features with random values. If `n_static_features > 0`, then a static dataframe is returned along the temporal dataframe. If `equal_ends == True` then all series end at the same date. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------- | ----------------------------------------------------------------------------------- | ------------------ | | `n_series` | [int](#int) | Number of series for synthetic panel. | *required* | | `freq` | [str](#str) | Frequency of the data, panda's available frequencies. Defaults to "D". | 'D' | | `min_length` | [int](#int) | Minimal length of synthetic panel's series. Defaults to 50. | 50 | | `max_length` | [int](#int) | Maximal length of synthetic panel's series. Defaults to 500. | 500 | | `n_temporal_features` | [int](#int) | Number of temporal exogenous variables for synthetic panel's series. Defaults to 0. | 0 | | `n_static_features` | [int](#int) | Number of static exogenous variables for synthetic panel's series. Defaults to 0. | 0 | | `equal_ends` | [bool](#bool) | If True, series finish in the same date stamp `ds`. Defaults to False. | False | | `seed` | [int](#int) | Random seed for reproducibility. Defaults to 0. | 0 | **Returns:** | Type | Description | | ------------------------------------------- | ----------------------------------------------------------------------------------- | | [DataFrame](#pandas.DataFrame) | pd.DataFrame: Synthetic panel with columns \[`unique_id`, `ds`, `y`] and exogenous. | ```python theme={null} synthetic_panel = generate_series(n_series=2) synthetic_panel.groupby('unique_id').head(4) ``` ```python theme={null} temporal_df, static_df = generate_series(n_series=1000, n_static_features=2, n_temporal_features=4, equal_ends=False) static_df.head(2) ``` ## 2. AirPassengers Data The classic Box & Jenkins airline data. Monthly totals of international airline passengers, 1949 to 1960. It has been used as a reference on several forecasting libraries, since it is a series that shows clear trends and seasonalities it offers a nice opportunity to quickly showcase a model’s predictions performance. ```python theme={null} AirPassengersDF.head(12) ``` ```python theme={null} #We are going to plot the ARIMA predictions, and the prediction intervals. fig, ax = plt.subplots(1, 1, figsize = (20, 7)) plot_df = AirPassengersDF.set_index('ds') plot_df[['y']].plot(ax=ax, linewidth=2) ax.set_title('AirPassengers Forecast', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Timestamp [t]', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} n_static_features = 3 n_series = 5 static_features = np.random.uniform(low=0.0, high=1.0, size=(n_series, n_static_features)) static_df = pd.DataFrame.from_records(static_features, columns = [f'static_{i}'for i in range(n_static_features)]) static_df['unique_id'] = np.arange(n_series) ``` ```python theme={null} static_df ``` ## 3. Panel AirPassengers Data Extension to classic Box & Jenkins airline data. Monthly totals of international airline passengers, 1949 to 1960. It includes two series with static, temporal and future exogenous variables, that can help to explore the performance of models like [`NBEATSx`](https://nixtlaverse.nixtla.io/neuralforecast/models.nbeatsx.html#nbeatsx) and [`TFT`](https://nixtlaverse.nixtla.io/neuralforecast/models.tft.html#tft). ```python theme={null} fig, ax = plt.subplots(1, 1, figsize = (20, 7)) plot_df = AirPassengersPanel.set_index('ds') plot_df.groupby('unique_id')['y'].plot(legend=True) ax.set_title('AirPassengers Panel Data', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Timestamp [t]', fontsize=20) ax.legend(title='unique_id', prop={'size': 15}) ax.grid() ``` ```python theme={null} fig, ax = plt.subplots(1, 1, figsize = (20, 7)) plot_df = AirPassengersPanel[AirPassengersPanel.unique_id=='Airline1'].set_index('ds') plot_df[['y', 'trend', 'y_[lag12]']].plot(ax=ax, linewidth=2) ax.set_title('Box-Cox AirPassengers Data', fontsize=22) ax.set_ylabel('Monthly Passengers', fontsize=20) ax.set_xlabel('Timestamp [t]', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ## 4. Time Features We have developed a utility that generates normalized calendar features for use as absolute positional embeddings in Transformer-based models. These embeddings capture seasonal patterns in time series data and can be easily incorporated into the model architecture. Additionally, the features can be used as exogenous variables in other models to inform them of calendar patterns in the data. ### References * [Haoyi Zhou, Shanghang Zhang, Jieqi Peng, Shuai Zhang, Jianxin Li, Hui Xiong, Wancai Zhang. “Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting”](https://arxiv.org/abs/2012.07436) *** ### `augment_calendar_df` ```python theme={null} augment_calendar_df(df, freq='H') ``` Augment a dataframe with calendar features based on frequency. Frequency mappings: * Q - \[month] * M - \[month] * W - \[Day of month, week of year] * D - \[Day of week, day of month, day of year] * B - \[Day of week, day of month, day of year] * H - \[Hour of day, day of week, day of month, day of year] * T - \[Minute of hour\*, hour of day, day of week, day of month, day of year] * S - \[Second of minute, minute of hour, hour of day, day of week, day of month, day of year] \*minute returns a number from 0-3 corresponding to the 15 minute period it falls into. **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------------- | ------------------------------------------------------------------------ | ---------------- | | `df` | [DataFrame](#pandas.DataFrame) | DataFrame to augment with calendar features. | *required* | | `freq` | [str](#str) | Frequency string for determining which features to add. Defaults to "H". | 'H' | **Returns:** | Type | Description | | ----------------------------------------------------------------------------------------------- | ----------- | | Tuple\[pd.DataFrame, List\[str]]: Tuple of (augmented DataFrame, list of feature column names). | | ### `time_features_from_frequency_str` ```python theme={null} time_features_from_frequency_str(freq_str) ``` Returns a list of time features that will be appropriate for the given frequency string. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | --------------------------------------------------------------------------------------- | ---------- | | `freq_str` | [str](#str) | Frequency string of the form \[multiple]\[granularity] such as "12H", "5min", "1D" etc. | *required* | **Returns:** | Type | Description | | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | | [List](#typing.List)\[[TimeFeature](#neuralforecast.utils.TimeFeature)] | List\[TimeFeature]: List of time features appropriate for the frequency. | ### `WeekOfYear` Bases: [TimeFeature](#neuralforecast.utils.TimeFeature) Week of year encoded as value between \[-0.5, 0.5]. ### `MonthOfYear` Bases: [TimeFeature](#neuralforecast.utils.TimeFeature) Month of year encoded as value between \[-0.5, 0.5]. ### `DayOfYear` Bases: [TimeFeature](#neuralforecast.utils.TimeFeature) Day of year encoded as value between \[-0.5, 0.5]. ### `DayOfMonth` Bases: [TimeFeature](#neuralforecast.utils.TimeFeature) Day of month encoded as value between \[-0.5, 0.5]. ### `DayOfWeek` Bases: [TimeFeature](#neuralforecast.utils.TimeFeature) Day of week encoded as value between \[-0.5, 0.5]. ### `HourOfDay` Bases: [TimeFeature](#neuralforecast.utils.TimeFeature) Hour of day encoded as value between \[-0.5, 0.5]. ### `MinuteOfHour` Bases: [TimeFeature](#neuralforecast.utils.TimeFeature) Minute of hour encoded as value between \[-0.5, 0.5]. ### `SecondOfMinute` Bases: [TimeFeature](#neuralforecast.utils.TimeFeature) Second of minute encoded as value between \[-0.5, 0.5]. ### `TimeFeature` ```python theme={null} TimeFeature() ``` ```python theme={null} AirPassengerPanelCalendar, calendar_cols = augment_calendar_df(df=AirPassengersPanel, freq='M') AirPassengerPanelCalendar.head() ``` ```python theme={null} plot_df = AirPassengerPanelCalendar[AirPassengerPanelCalendar.unique_id=='Airline1'].set_index('ds') plt.plot(plot_df['month']) plt.grid() plt.xlabel('Datestamp') plt.ylabel('Normalized Month') plt.show() ``` ### `get_indexer_raise_missing` ```python theme={null} get_indexer_raise_missing(idx, vals) ``` Get index positions for values, raising error if any are missing. **Parameters:** | Name | Type | Description | Default | | ------ | ----------------------------------------------- | --------------------------- | ---------- | | `idx` | [Index](#pandas.Index) | Index to search in. | *required* | | `vals` | [List](#typing.List)\[[str](#str)] | Values to find indices for. | *required* | **Returns:** | Type | Description | | ----------------------------------------------- | ------------------------------------ | | [List](#typing.List)\[[int](#int)] | List\[int]: List of index positions. | **Raises:** | Type | Description | | -------------------------------------- | ----------------------------------------- | | [ValueError](#ValueError) | If any values are missing from the index. | ## 5. Prediction Intervals ### `PredictionIntervals` ```python theme={null} PredictionIntervals(n_windows=2, method='conformal_distribution', step_size=1) ``` Class for storing prediction intervals metadata information. Initialize PredictionIntervals. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | | `n_windows` | [int](#int) | Number of windows to evaluate. Defaults to 2. | 2 | | `method` | [str](#str) | One of the supported methods for the computation of prediction intervals: conformal\_error or conformal\_distribution. Defaults to "conformal\_distribution". | 'conformal\_distribution' | | `step_size` | [int](#int) | Step size between each cross-validation window. Defaults to 1. | 1 | #### `PredictionIntervals.method` ```python theme={null} method = method ``` #### `PredictionIntervals.n_windows` ```python theme={null} n_windows = n_windows ``` #### `PredictionIntervals.step_size` ```python theme={null} step_size = step_size ``` ### `add_conformal_distribution_intervals` ```python theme={null} add_conformal_distribution_intervals( model_fcsts, cs_df, model, cs_n_windows, n_series, horizon, level=None, quantiles=None, ) ``` Add conformal intervals based on conformal scores using distribution strategy. This strategy creates forecast paths based on errors and calculates quantiles using those paths. **Parameters:** | Name | Type | Description | Default | | -------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | ----------------- | | `model_fcsts` | [array](#numpy.array) | Model forecasts array. | *required* | | `cs_df` | [DFType](#utilsforecast.compat.DFType) | DataFrame containing conformal scores. | *required* | | `model` | [str](#str) | Model name. | *required* | | `cs_n_windows` | [int](#int) | Number of conformal score windows. | *required* | | `n_series` | [int](#int) | Number of series. | *required* | | `horizon` | [int](#int) | Forecast horizon. | *required* | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[Union](#typing.Union)\[[int](#int), [float](#float)]]] | Confidence levels for prediction intervals. Defaults to None. | None | | `quantiles` | [Optional](#typing.Optional)\[[List](#typing.List)\[[float](#float)]] | Quantiles for prediction intervals. Defaults to None. | None | **Returns:** | Type | Description | | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[array](#numpy.array), [List](#typing.List)\[[str](#str)]] | Tuple\[np.array, List\[str]]: Tuple of (forecasts with intervals, column names). | ### `add_conformal_error_intervals` ```python theme={null} add_conformal_error_intervals( model_fcsts, cs_df, model, cs_n_windows, n_series, horizon, level=None, quantiles=None, ) ``` Add conformal intervals based on conformal scores using error strategy. This strategy creates prediction intervals based on absolute errors. **Parameters:** | Name | Type | Description | Default | | -------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | ----------------- | | `model_fcsts` | [array](#numpy.array) | Model forecasts array. | *required* | | `cs_df` | [DFType](#utilsforecast.compat.DFType) | DataFrame containing conformal scores. | *required* | | `model` | [str](#str) | Model name. | *required* | | `cs_n_windows` | [int](#int) | Number of conformal score windows. | *required* | | `n_series` | [int](#int) | Number of series. | *required* | | `horizon` | [int](#int) | Forecast horizon. | *required* | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[Union](#typing.Union)\[[int](#int), [float](#float)]]] | Confidence levels for prediction intervals. Defaults to None. | None | | `quantiles` | [Optional](#typing.Optional)\[[List](#typing.List)\[[float](#float)]] | Quantiles for prediction intervals. Defaults to None. | None | **Returns:** | Type | Description | | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[array](#numpy.array), [List](#typing.List)\[[str](#str)]] | Tuple\[np.array, List\[str]]: Tuple of (forecasts with intervals, column names). | ### `get_prediction_interval_method` ```python theme={null} get_prediction_interval_method(method) ``` Get the prediction interval method function by name. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | --------------------------------------- | ---------- | | `method` | [str](#str) | Name of the prediction interval method. | *required* | **Returns:** | Name | Type | Description | | ---------- | ---- | ---------------------------------- | | `Callable` | | The corresponding method function. | **Raises:** | Type | Description | | -------------------------------------- | ------------------------------- | | [ValueError](#ValueError) | If the method is not supported. | ### `quantiles_to_level` ```python theme={null} quantiles_to_level(quantiles) ``` Convert a list of quantiles to confidence levels. **Parameters:** | Name | Type | Description | Default | | ----------- | --------------------------------------------------- | ------------------------------------------- | ---------- | | `quantiles` | [List](#typing.List)\[[float](#float)] | List of quantiles (e.g., \[0.1, 0.5, 0.9]). | *required* | **Returns:** | Type | Description | | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | [List](#typing.List)\[[Union](#typing.Union)\[[int](#int), [float](#float)]] | List\[Union\[int, float]]: List of corresponding confidence levels. | ### `level_to_quantiles` ```python theme={null} level_to_quantiles(level) ``` Convert a list of confidence levels to quantiles. **Parameters:** | Name | Type | Description | Default | | ------- | ----------------------------------------------------------------------------------------- | -------------------------------------------- | ---------- | | `level` | [List](#typing.List)\[[Union](#typing.Union)\[[int](#int), [float](#float)]] | List of confidence levels (e.g., \[80, 90]). | *required* | **Returns:** | Type | Description | | --------------------------------------------------- | ---------------------------------------------- | | [List](#typing.List)\[[float](#float)] | List\[float]: List of corresponding quantiles. | # Contribute to Nixtla Source: https://nixtlaverse.nixtla.io/statsforecast/docs/contribute/contribute.html Thank you for your interest in contributing to Nixtla. Nixtla is free, open-source software and welcomes all types of contributions, including documentation changes, bug reports, bug fixes, or new source code changes. ## Contribution issues 🔧 Most of the issues that are open for contributions will be tagged with `good first issue` or `help wanted`. A great place to start looking will be our GitHub projects for: * Community writers [dashboard](https://github.com/orgs/Nixtla/projects/9). * Community code contributors [dashboard](https://github.com/orgs/Nixtla/projects/6). Also, we are always open to suggestions so feel free to open new issues with your ideas and we can give you guidance! After you find the issue that you want to contribute to, follow the `fork-and-pull` workflow: 1. Fork the Nixtla repository you want to work on (e.g. StatsForecast or NeuralForecast) 2. Clone the repository locally (`git clone`) and create a new branch (`git checkout -b my-new-branch`) 3. Make changes and commit them 4. Push your local branch to your fork 5. Submit a Pull Request so that we can review your changes 6. Write a commit message 7. Make sure that the CI tests are GREEN (CI tests refer to automated tests that are run on code changes to ensure that new additions or modifications do not introduce new errors or break existing functionality.) Be sure to merge the latest from “upstream” before making a Pull Request! You can find a complete step-by-step guide on this `fork-and-pull` workflow [here](https://github.com/Nixtla/how-to-contribute-nixtlaverse). Pull Request reviews are done on a regular basis. Please make sure you respond to our feedback/questions and sign our CLA. ## Documentation 📖 We are committed to continuously improving our documentation. As such, we warmly welcome any Pull Requests that focus on improving our grammar, documentation structure, or fixing any typos. * Check the `documentation` tagged issues and help us. ## Write for us 📝 Do you find Nixtla useful and want to share your story or create some content? Make a PR to this repo with your writing in a markdown file, or just post it on Medium, Dev or your own blog post. We would love to hear from you 💚 This document is based on the documentation from [MindsDB](https://github.com/mindsdb/mindsdb) # Nixtla Documentation Source: https://nixtlaverse.nixtla.io/statsforecast/docs/contribute/docs.html TBD # Understanding Issue Labels Source: https://nixtlaverse.nixtla.io/statsforecast/docs/contribute/issue-labels.html This segment delves into the variety of issue labels used within the [Nixtla GitHub repository](https://github.com/nixtla/nixtla). ## Labels Relevant to Contributors Should you be a contributor now or in the future, it’s important to take note of issues flagged with these labels. ### The `first-timers-only` Label For those who have not yet contributed to Nixtla, start by looking for issues tagged as `first-timers-only`. Please note that before we can accept your contribution to Nixtla, you’ll need to sign our [Contributor License Agreement](https://github.com/nixtla/nixtla_native/blob/stable/assets/contributions-agreement/individual-contributor.md). You can browse all `first-timers-only` issues [here](https://github.com/nixtla/nixtla/labels/first-timers-only). ### The `good first issue` Label Issues labeled as `good first issue` are ideal for newcomers. You can browse all `good first issue` issues [here](https://github.com/nixtla/nixtla/labels/good%20first%20issue). ### The `help wanted` Label Issues tagged as `help wanted` are open to anyone who wishes to contribute to Nixtla. You can browse all `help wanted` issues [here](https://github.com/nixtla/nixtla/labels/help%20wanted). ### The `bug` Label The `bug` label flags issues that outline something that’s currently not functioning correctly. You can report a bug by following the instructions [here](./issues.html#report-a-bug). ### The `discussion` Label If an issue is labeled as `discussion`, it signifies that more conversation is needed before it can be resolved. ### The `documentation` Label The `documentation` label identifies issues pertaining to our documentation. You can contribute to improving our documentation by creating issues following the guidelines [here](./issues.html#improve-our-docs). ### The `enhancement` Label As Nixtla continues to evolve, there are always areas that can be enhanced. All issues suggesting improvements to Nixtla are tagged with the `enhancement` label. You can propose a feature by following the instructions [here](./issues.html#request-a-feature). ### The `discussion` Label If an issue is labeled as `discussion`, it needs more information before it can be resolved. ### The `requested` Label Our users are welcomed to propose improvements, report bugs, request feature, etc. Any issue originating from them is flagged as `requested`. # Submit an Issue 📢 Source: https://nixtlaverse.nixtla.io/statsforecast/docs/contribute/issues.html To report a bug, request a feature, propose a new integration, or suggest documentation improvements, please visit the [Nixtla GitHub issues page](https://github.com/nixtla/nixtla/issues). Before submitting a new issue, kindly check if it has already been reported. ## Steps to Submit an Issue Here’s a step-by-step guide on submitting an issue to the Nixtla repository. Visit [our GitHub issues page](https://github.com/nixtla/nixtla/issues) and click on the *New issue* button. A list of available issue types will be displayed. ### Reporting a Bug 🐞 Select `Report a bug` and click on the *Get started* button. The form to report the bug will appear. 1. Begin by adding a concise, informative title. 2. Describe the bug you’ve observed. This information is required. You can also attach relevant videos or screenshots. 3. If you’re aware of what the correct behavior should be, note it down here. 4. Documenting the steps leading to the bug will be of immense help to us. 5. You can also add links, references, logs, screenshots, and so on. Please ensure that your contributions abide by the [contributing guidelines](https://github.com/nixtla/nixtla/blob/staging/CONTRIBUTING.md) and [code of conduct](https://github.com/nixtla/nixtla/blob/staging/CODE_OF_CONDUCT.md). Thank you for your contribution! Your report aids in refining Nixtla for current and future users. ### Feature Request 🚀 Select `Request a feature` and click the *Get started* button. The feature request form will appear. 1. Start with a significant, clear title. 2. Provide a detailed description of the feature you want to request, along with the reasoning behind the request. This field is mandatory. Feel free to attach related videos or screenshots. 3. If you have an idea of how the feature should work, include it. 4. Additional references, links, logs, and screenshots are welcome! Please ensure that your contributions abide by the [contributing guidelines](https://github.com/nixtla/nixtla/blob/staging/CONTRIBUTING.md) and [code of conduct](https://github.com/nixtla/nixtla/blob/staging/CODE_OF_CONDUCT.md). Thank you for your feature request! It will help us enhance Nixtla for all users. ### Suggest Documentation Improvements ✍️ Select `Improve our docs` and click the *Get started* button. A form for suggesting improvements will appear. 1. A clear, concise title is important. 2. Describe the improvements you believe are needed. This field is mandatory. Attach any related videos or screenshots, if necessary. 3. Any additional references, links, logs, screenshots are appreciated! Please ensure that your contributions abide by the [contributing guidelines](https://github.com/nixtla/nixtla/blob/staging/CONTRIBUTING.md) and [code of conduct](https://github.com/nixtla/nixtla/blob/staging/CODE_OF_CONDUCT.md). Thank you for your valuable suggestions! Your input helps us refine Nixtla’s documentation. ### Proposing a New Integration 🧑‍🔧 If you have a proposal for a new database integration or a new machine learning framework, here’s how to get started: Select \` Propose a new integration\` and click the *Get started* button. A form for your proposal will appear. 1. Start with a clear, concise title. 2. Describe your proposal and why it is needed. This field is mandatory. Feel free to attach any related videos or screenshots. 3. If you have an idea of how this integration should work, include it. 4. Any additional references, links, logs, screenshots, and so on, are welcome! Please ensure that your contributions abide by the [contributing guidelines](https://github.com/nixtla/nixtla/blob/staging/CONTRIBUTING.md) and [code of conduct](https://github.com/nixtla/nixtla/blob/staging/CODE_OF_CONDUCT.md). Thank you for your proposal! Your suggestion helps us extend the capabilities of Nixtla. ## Reviewing Issues * Issues are reviewed on a regular basis, usually every day. * Issues will be labeled as `Bug` or `enhancement` based on their type. * Please be ready to respond to our feedback or questions regarding your issue. This document is based on the documentation from [MindsDB](https://github.com/mindsdb/mindsdb) # Step-by-step Contribution Guide Source: https://nixtlaverse.nixtla.io/statsforecast/docs/contribute/step-by-step.html This document contains instructions for collaborating on the different libraries of Nixtla. Sometimes, diving into a new technology can be challenging and overwhelming. We’ve been there too, and we’re more than ready to assist you with any issues you may encounter while following these steps. Don’t hesitate to reach out to us on [Slack](https://join.slack.com/t/nixtlacommunity/shared_invite/zt-1pmhan9j5-F54XR20edHk0UtYAPcW4KQ). Just give fede a ping, and she’ll be glad to assist you. ## Table of Contents 📚 1. [Prerequisites](#prerequisites) 2. [Git `fork-and-pull` worklow](#git-fork-and-pull-worklow) 3. [Set Up a Conda Environment](#set-up-a-conda-environment) 4. [Install required libraries for development](#install-required-libraries-for-development) 5. [Start editable mode](#start-editable-mode) 6. [Set Up your Notebook based development environment](#set-up-your-notebook-based-development-environment) 7. [Start Coding](#start-coding) 8. [Example with Screen-shots](#example-with-screen-shots) ## Prerequisites * *GitHub*: You should already have a GitHub account and a basic understanding of its functionalities. Alternatively check [this guide](https://docs.github.com/en/get-started). * *Python*: Python should be installed on your system. Alternatively check [this guide](https://www.python.org/downloads/). * *conda*: You need to have conda installed, along with a good grasp of fundamental operations such as creating environments, and activating and deactivating them. Alternatively check [this guide](https://conda.io/projects/conda/en/latest/user-guide/install/index.html). ## Git `fork-and-pull` worklow **1. Fork the Project:** Start by forking the Nixtla repository to your own GitHub account. This creates a personal copy of the project where you can make changes without affecting the main repository. **2. Clone the Forked Repository** Clone the forked repository to your local machine using `git clone https://github.com//nixtla.git`. This allows you to work with the code directly on your system. **3. Create a Branch:** Branching in GitHub is a key strategy for effectively managing and isolating changes to your project. It allows you to segregate work on different features, fixes, and issues without interfering with the main, production-ready codebase. 1. *Main Branch*: The default branch with production-ready code. 2. *Feature Branches*: For new features, create branches prefixed with ‘feature/’, like `git checkout -b feature/new-model`. 3. *Fix Branches*: For bug fixes, use ‘fix/’ prefix, like `git checkout -b fix/forecasting-bug`. 4. *Issue Branches*: For specific issues, use `git checkout -b issue/issue-number` or `git checkout -b issue/issue-description`. After testing, branches are merged back into the main branch via a pull request, and then typically deleted to maintain a clean repository. You can read more about github and branching [here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-and-deleting-branches-within-your-repository). ## Set Up a Conda Environment > If you want to use Docker or Codespaces, let us know opening an issue > and we will set you up. Next, you’ll need to set up a [Conda](https://docs.conda.io/en/latest/) environment. Conda is an open-source package management and environment management system that runs on Windows, macOS, and Linux. It allows you to create separate environments containing files, packages, and dependencies that will not interact with each other. First, ensure you have Anaconda or Miniconda installed on your system. Alternatively checkout these guides: [Anaconda](https://www.anaconda.com/), [Miniconda](https://docs.conda.io/en/latest/miniconda.html), and [Mamba](https://mamba.readthedocs.io/en/latest/). Then, you can create a new environment using `conda create -n nixtla-env python=3.10`. You can also use mamba for creating the environment (mamba is faster than Conda) using `mamba create -n nixtla-env python=3.10`. You can replace `nixtla-env` for something more meaningful to you. Eg. `statsforecast-env` or `mlforecast-env`. You can always check the list of environments in your system using `conda env list`. Activate your new environment with `conda activate nixtla-env`. ## Install required libraries for development The `environment.yml` file contains all the dependencies required for the project. To install these dependencies, use the `mamba` package manager, which offers faster package installation and environment resolution than Conda. If you haven’t installed `mamba` yet, you can do so using `conda install mamba -c conda-forge`. Run the following command to install the dependencies: ```text theme={null} mamba env update -f environment.yml ``` Sometimes (e.g. StatsForecast) the `enviorment.yml` is sometimes inside a folder called `dev`. In that case, you should run `mamba env update -f dev/environment.yml`. ## Start editable mode Install the library in editable mode using `pip install -e ".[dev]"`. This means the package is linked directly to the source code, allowing any changes made to the source code to be immediately reflected in your Python environment without the need to reinstall the package. This is useful for testing changes during package development. ## Set Up your Notebook based development environment Notebook-based development refers to using interactive notebooks, such as Jupyter Notebooks, for coding, data analysis, and visualization. Here’s a brief description of its characteristics: 1. **Interactivity**: Code in notebooks is written in cells which can be run independently. This allows for iterative development and testing of small code snippets. 2. **Visualization**: Notebooks can render charts, tables, images, and other graphical outputs within the same interface, making it great for data exploration and analysis. 3. **Documentation**: Notebooks support Markdown and HTML, allowing for detailed inline documentation. Code, outputs, and documentation are in one place, which is ideal for tutorials, reports, or sharing work. For notebook based development you’ll need `nbdev` and a notebook editor (such as VS Code, Jupyter Notebook or Jupyter Lab). `nbdev` and jupyter have been installed in the previous step. If you use VS Code follow [this tutorial](https://code.visualstudio.com/docs/datascience/jupyter-notebooks). [nbdev](https://github.com/fastai/nbdev) makes debugging and refactoring your code much easier than in traditional programming environments since you always have live objects at your fingertips. `nbdev` also promotes software engineering best practices because tests and documentation are first class. All your changes must be written in the notebooks contained in the library (under the `nbs` directory). Once a specific notebook is open (more details to come), you can write your Python code in cells within the notebook, as you would do in a traditional Python development workflow. You can break down complex problems into smaller parts, visualizing data, and documenting your thought process. Along with your code, you can include markdown cells to add documentation directly in the notebook. This includes explanations of your logic, usage examples, and more. Also, `nbdev` allows you to write [tests inline](https://nbdev.fast.ai/tutorials/best_practices.html#document-error-cases-as-tests) with your code in your notebook. After writing a function, you can immediately write tests for it in the following cells. Once your code is ready, `nbdev` can automatically convert your notebook into Python scripts. Code cells are converted into Python code, and markdown cells into comments and docstrings. ## Start Coding Open a jupyter notebook using `jupyter lab` (or VS Code). 1. **Make Your Changes:** Make changes to the codebase, ensuring your changes are self-contained and cohesive. 2. **Commit Your Changes:** Add the changed files using `git add [your_modified_file_0.ipynb] [your_modified_file_1.ipynb]`, then commit these changes using `git commit -m ": "`. Please use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 3. **Push Your Changes:** Push your changes to the remote repository on GitHub with `git push origin feature/your-feature-name`. 4. **Open a Pull Request:** Open a pull request from your new branch on the Nixtla repository on GitHub. Provide a thorough description of your changes when creating the pull request. 5. **Wait for Review:** The maintainers of the Nixtla project will review your changes. Be ready to iterate on your contributions based on their feedback. Remember, contributing to open-source projects is a collaborative effort. Respect the work of others, welcome feedback, and always strive to improve. Happy coding! > Nixtla offers the possibility of assisting with stipends for computing > infrastructure for our contributors. If you are interested, please > join our > [slack](https://nixtlacommunity.slack.com/join/shared_invite/zt-1pmhan9j5-F54XR20edHk0UtYAPcW4KQ#/shared-invite/email) > and write to fede or Max. You can find a detailed step by step buide with screen-shots below. ## Example with Screen-shots ### 1. Create a fork of the mlforecast repo The first thing you need to do is create a fork of the GitHub repository to your own account: image Your fork on your account will look like this: image In that repository, you can make your changes and then request to have them added to the main repo. ### 2. Clone the repository In this tutorial, we are using Mac (also compatible with other Linux distributions). If you are a collaborator of Nixtla, you can request an AWS instance to collaborate from there. If this is the case, please reach out to Max or Fede on [Slack](https://join.slack.com/t/nixtlacommunity/shared_invite/zt-1pmhan9j5-F54XR20edHk0UtYAPcW4KQ) to receive the appropriate access. We also use Visual Studio Code, which you can download from [here](https://code.visualstudio.com/download). Once the repository is created, you need to clone it to your own computer. Simply copy the repository URL from GitHub as shown below: image Then open Visual Studio Code, click on “Clone Git Repository,” and paste the line you just copied into the top part of the window, as shown below: image Select the folder where you want to copy the repository: image And choose to open the cloned repository: image You will end up with something like this: image ### 3. Create the Conda environment Open a terminal within Visual Studio Code, as shown in the image: image You can use conda but we highly recommend using Mamba to speed up the creation of the Conda environment. To install it, simply use `conda install mamba -c conda-forge` in the terminal you just opened: image Create an empty environment named `mlforecast` with the following command: `mamba create -n mlforecast python=3.10`: image Activate the newly created environment using `conda activate mlforecast`: image Install the libraries within the environment file `environment.yml` using `mamba env update -f environment.yml`: image Now install the library to make interactive changes and other additional dependencies using `pip install -e ".[dev]"`: image ### 4. Make the changes you want. In this section, we assume that we want to increase the default number of windows used to create prediction intervals from 2 to 3. The first thing we need to do is create a specific branch for that change using `git checkout -b [new_branch]` like this: image Once created, open the notebook you want to modify. In this case, it’s `nbs/utils.ipynb`, which contains the metadata for the prediction intervals. After opening it, click on the environment you want to use (top right) and select the `mlforecast` environment: image Next, execute the notebook and make the necessary changes. In this case, we want to modify the `PredictionIntervals` class: image We will change the default value of `n_window` from 2 to 3: image Once you have made the change and performed any necessary validations, it’s time to convert the notebook to Python modules. To do this, simply use `nbdev_export` in the terminal. You will see that the `mlforecast/utils.py` file has been modified (the changes from `nbs/utils.ipynb` are reflected in that module). Before committing the changes, we need to clean the notebooks using the command `./action_files/clean_nbs` and verify that the linters pass using `./action_files/lint`: image Once you have done the above, simply add the changes using `git add nbs/utils.ipynb mlforecast/utils.py`: image Create a descriptive commit message for the changes using `git commit -m "[description of changes]"`: image Finally, push your changes using `git push`: image ### 5. Create a pull request. In GitHub, open your repository that contains your fork of the original repo. Once inside, you will see the changes you just pushed. Click on “Compare and pull request”: image Include an appropriate title for your pull request and fill in the necessary information. Once you’re done, click on “Create pull request”. image Finally, you will see something like this: image ## Notes * This file was generated using [this file](https://github.com/Nixtla/nixtla-commons/blob/main/docs/contribute/step-by-step.md). Please change that file if you want to enhance the document. # Contributing Code to Nixtla Development Source: https://nixtlaverse.nixtla.io/statsforecast/docs/contribute/techstack.html A guide on the technical skills and tools needed to contribute code to the Nixtla project. Curious about the skills required to contribute to the Nixtla project? ## Required Skills for Contribution ### Coding If you’re interested in making code contributions, possessing any of the following skills can assist you in getting started: * [GitHub](https://github.com/) * [Python 3](https://www.python.org/) * [conda](https://docs.conda.io/en/latest/) ### Time Series Theory * [Forecasting: Principles and Practice](https://otexts.com/fpp3/) * [Python Adaptation](https://github.com/Nixtla/fpp3-python) of Forecasting: Principles and Practice Happy forecasting! # Dask Source: https://nixtlaverse.nixtla.io/statsforecast/docs/distributed/dask.html > Run StatsForecast distributedly on top of Dask. StatsForecast works on top of Spark, Dask, and Ray through [Fugue](https://github.com/fugue-project/fugue/). StatsForecast will read the input DataFrame and use the corresponding engine. For example, if the input is a Spark DataFrame, StatsForecast will use the existing Spark session to run the forecast. ## Installation As long as Dask is installed and configured, StatsForecast will be able to use it. If executing on a distributed Dask cluster, make use the `statsforecast` library is installed across all the workers. ## StatsForecast on Pandas Before running on Dask, it’s recommended to test on a smaller Pandas dataset to make sure everything is working. This example also helps show the small differences when using Dask. ```python theme={null} from statsforecast.core import StatsForecast from statsforecast.models import ( AutoARIMA, AutoETS, ) from statsforecast.utils import generate_series ``` ```python theme={null} n_series = 4 horizon = 7 series = generate_series(n_series) sf = StatsForecast( models=[AutoETS(season_length=7)], freq='D', ) sf.forecast(df=series, h=horizon).head() ``` | | unique\_id | ds | AutoETS | | - | ---------- | ---------- | -------- | | 0 | 0 | 2000-08-10 | 5.261609 | | 1 | 0 | 2000-08-11 | 6.196357 | | 2 | 0 | 2000-08-12 | 0.282309 | | 3 | 0 | 2000-08-13 | 1.264195 | | 4 | 0 | 2000-08-14 | 2.262453 | ## Executing on Dask To run the forecasts distributed on Dask, just pass in a Dask DataFrame instead. ```python theme={null} import dask.dataframe as dd ``` ```python theme={null} series['unique_id'] = series['unique_id'].astype(str) ddf = dd.from_pandas(series, npartitions=4) sf.forecast(df=ddf, h=horizon).compute().head() ``` | | unique\_id | ds | AutoETS | | - | ---------- | ------------------- | -------- | | 0 | 0 | 2000-08-10 00:00:00 | 5.261609 | | 1 | 0 | 2000-08-11 00:00:00 | 6.196357 | | 2 | 0 | 2000-08-12 00:00:00 | 0.282309 | | 3 | 0 | 2000-08-13 00:00:00 | 1.264195 | | 4 | 0 | 2000-08-14 00:00:00 | 2.262453 | # Ray Source: https://nixtlaverse.nixtla.io/statsforecast/docs/distributed/ray.html > Run StatsForecast distributedly on top of Ray. StatsForecast works on top of Spark, Dask, and Ray through [Fugue](https://github.com/fugue-project/fugue/). StatsForecast will read the input DataFrame and use the corresponding engine. For example, if the input is a Ray Dataset, StatsForecast will use the existing Ray instance to run the forecast. A benchmark (with older syntax) can be found [here](https://www.anyscale.com/blog/how-nixtla-uses-ray-to-accurately-predict-more-than-a-million-time-series) where we forecasted one million timeseries in under half an hour. ## Installation As long as Ray is installed and configured, StatsForecast will be able to use it. If executing on a distributed Ray cluster, make use the `statsforecast` library is installed across all the workers. ## StatsForecast on Pandas Before running on Ray, it’s recommended to test on a smaller Pandas dataset to make sure everything is working. This example also helps show the small differences when using Ray. ```python theme={null} from statsforecast.core import StatsForecast from statsforecast.models import AutoARIMA, AutoETS from statsforecast.utils import generate_series ``` ```python theme={null} n_series = 4 horizon = 7 series = generate_series(n_series) sf = StatsForecast( models=[AutoETS(season_length=7)], freq='D', ) sf.forecast(df=series, h=horizon).head() ``` | | unique\_id | ds | AutoETS | | - | ---------- | ---------- | -------- | | 0 | 0 | 2000-08-10 | 5.261609 | | 1 | 0 | 2000-08-11 | 6.196357 | | 2 | 0 | 2000-08-12 | 0.282309 | | 3 | 0 | 2000-08-13 | 1.264195 | | 4 | 0 | 2000-08-14 | 2.262453 | ## Executing on Ray To run the forecasts distributed on Ray, just pass in a Ray Dataset instead. ```python theme={null} import ray import logging ``` ```python theme={null} ray.init(logging_level=logging.ERROR) series['unique_id'] = series['unique_id'].astype(str) ctx = ray.data.context.DatasetContext.get_current() ctx.use_streaming_executor = False ray_series = ray.data.from_pandas(series).repartition(4) ``` ```python theme={null} sf.forecast(df=ray_series, h=horizon).take(5) ``` # Spark Source: https://nixtlaverse.nixtla.io/statsforecast/docs/distributed/spark.html > Run StatsForecast distributedly on top of Spark. StatsForecast works on top of Spark, Dask, and Ray through [Fugue](https://github.com/fugue-project/fugue/). StatsForecast will read the input DataFrame and use the corresponding engine. For example, if the input is a Spark DataFrame, StatsForecast will use the existing Spark session to run the forecast. A benchmark (with older syntax) can be found [here](https://towardsdatascience.com/distributed-forecast-of-1m-time-series-in-under-15-minutes-with-spark-nixtla-and-fugue-e9892da6fd5c) where we forecasted one million timeseries in under 15 minutes. ## Installation As long as Spark is installed and configured, StatsForecast will be able to use it. If executing on a distributed Spark cluster, make use the `statsforecast` library is installed across all the workers. ## StatsForecast on Pandas Before running on Spark, it’s recommended to test on a smaller Pandas dataset to make sure everything is working. This example also helps show the small differences when using Spark. ```python theme={null} from statsforecast.core import StatsForecast from statsforecast.models import AutoARIMA, AutoETS from statsforecast.utils import generate_series ``` ```python theme={null} n_series = 4 horizon = 7 series = generate_series(n_series) sf = StatsForecast( models=[AutoETS(season_length=7)], freq='D', ) sf.forecast(df=series, h=horizon).head() ``` | | unique\_id | ds | AutoETS | | - | ---------- | ---------- | -------- | | 0 | 0 | 2000-08-10 | 5.261609 | | 1 | 0 | 2000-08-11 | 6.196357 | | 2 | 0 | 2000-08-12 | 0.282309 | | 3 | 0 | 2000-08-13 | 1.264195 | | 4 | 0 | 2000-08-14 | 2.262453 | ## Executing on Spark To run the forecasts distributed on Spark, just pass in a Spark DataFrame instead. ```python theme={null} from pyspark.sql import SparkSession ``` ```python theme={null} spark = SparkSession.builder.getOrCreate() series['unique_id'] = series['unique_id'].astype(str) # Convert to Spark sdf = spark.createDataFrame(series) # Returns a Spark DataFrame sf.forecast(df=sdf, h=horizon, level=[90]).show(5) ``` ```text theme={null} +---------+-------------------+----------+-------------+-------------+ |unique_id| ds| AutoETS|AutoETS-lo-90|AutoETS-hi-90| +---------+-------------------+----------+-------------+-------------+ | 0|2000-08-10 00:00:00| 5.261609| 5.0255513| 5.4976664| | 0|2000-08-11 00:00:00| 6.1963573| 5.9603| 6.432415| | 0|2000-08-12 00:00:00|0.28230855| 0.04625102| 0.5183661| | 0|2000-08-13 00:00:00| 1.2641948| 1.0281373| 1.5002524| | 0|2000-08-14 00:00:00| 2.2624528| 2.0263953| 2.4985104| +---------+-------------------+----------+-------------+-------------+ only showing top 5 rows ``` # Amazon Forecast vs StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/docs/experiments/amazonstatsforecast.html > Amazon’s AutoML vs open source statistical methods ## Data We will make use of the [M5 competition](https://mofc.unic.ac.cy/m5-competition/) dataset provided by Walmart. This dataset is interesting for its scale but also the fact that it features many timeseries with infrequent occurances. Such timeseries are common in retail scenarios and are difficult for traditional timeseries forecasting techniques to address. The data are ready for download at the following URLs: * Train set: `https://m5-benchmarks.s3.amazonaws.com/data/train/target.parquet` * Temporal exogenous variables (used by AmazonForecast): `https://m5-benchmarks.s3.amazonaws.com/data/train/temporal.parquet` * Static exogenous variables (used by AmazonForecast): `https://m5-benchmarks.s3.amazonaws.com/data/train/static.parquet` A more detailed description of the data can be found [here](../../../datasetsforecast/m5.html). > **Warning** > > The M5 competition is hierarchical. That is, forecasts are required > for different levels of aggregation: national, state, store, etc. In > this experiment, we only generate forecasts using the bottom-level > data. The evaluation is performed using the bottom-up reconciliation > method to obtain the forecasts for the higher hierarchies. ## Amazon Forecast Amazon Forecast is a fully automated solution for time series forecasting. The solution can take the time series to forecast and exogenous variables (temporal and static). For this experiment, we used the AutoPredict functionality of Amazon Forecast following the steps of [this tutorial](https://docs.aws.amazon.com/forecast/latest/dg/gs-console.html). A detailed description of the particular steps for this dataset can be found [here](./amazonstatsforecast.html). Amazon Forecast creates predictors with AutoPredictor, which involves applying the optimal combination of algorithms to each time series in your datasets. The predictor is an Amazon Forecast model that is trained using your target time series, related time series, item metadata, and any additional datasets you include. Included algorithms range from commonly used statistical algorithms like Autoregressive Integrated Moving Average (ARIMA), to complex neural network algorithms like CNN-QR and DeepAR+.: CNN-QR, DeepAR+, Prophet, NPTS, ARIMA, and ETS. To leverage the probabilistic features of Amazon Forecast and enable confidence intervals for further analysis we forecasted the following quantiles: 0.1 | 0.5 | 0.9. The full pipeline of Amazon Forecast took 4.1 hours and the results can be found here: `s3://m5-benchmarks/forecasts/amazonforecast-m5.parquet` ## Nixtla’s StatsForecast ### Install necessary libraries We assume you have StatsForecast already installed. Check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html). Additionally, we will install `s3fs` to read from the S3 Filesystem of AWS. (If you don’t want to use a cloud storage provider, you can read your files locally using pandas) ```python theme={null} %%capture !pip install statsforecast s3fs ``` ### Input format We will use pandas to read the data set stored in a parquet file for efficiency. You can use ordinary pandas operations to read your data in other formats likes `.csv`. The input to StatsForecast is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. We will rename the So we will rename the original columns to make it compatible with StatsForecast. Depending on your internet connection, this step should take around 20 seconds. > **Warning** > > We are reading a file from S3, so you need to install the s3fs > library. To install it, run `! pip install s3fs` ### Read data ```python theme={null} import pandas as pd Y_df_m5 = pd.read_parquet('https://m5-benchmarks.s3.amazonaws.com/data/train/target.parquet') Y_df_m5 = Y_df_m5.rename(columns={ 'item_id': 'unique_id', 'timestamp': 'ds', 'demand': 'y' }) Y_df_m5.head() ``` | | unique\_id | ds | y | | - | -------------------- | ---------- | --- | | 0 | FOODS\_1\_001\_CA\_1 | 2011-01-29 | 3.0 | | 1 | FOODS\_1\_001\_CA\_1 | 2011-01-30 | 0.0 | | 2 | FOODS\_1\_001\_CA\_1 | 2011-01-31 | 0.0 | | 3 | FOODS\_1\_001\_CA\_1 | 2011-02-01 | 1.0 | | 4 | FOODS\_1\_001\_CA\_1 | 2011-02-02 | 4.0 | ### Train statistical models We fit the model by instantiating a new `StatsForecast` object with the following parameters: * `models`: a list of models. Select the models you want from [models](../../src/core/models.html) and import them. For this example, we will use `AutoETS` and `DynamicOptimizedTheta`. We set `season_length` to 7 because we expect seasonal effects every week. (See: [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/)) * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs`: n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model`: a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. * `AutoETS`: Exponential Smoothing model. Automatically selects the best ETS (Error, Trend, Seasonality) model using an information criterion. Ref: `AutoETS`. * `SeasonalNaive`: Memory Efficient Seasonal Naive predictions. Ref: `SeasonalNaive`. * `DynamicOptimizedTheta`: fit two theta lines to a deseasonalized time series, using different techniques to obtain and combine the two theta lines to produce the final forecasts. Ref: `DynamicOptimizedTheta`. ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import ( AutoETS, DynamicOptimizedTheta, SeasonalNaive ) # Create list of models models = [ AutoETS(season_length=7), DynamicOptimizedTheta(season_length=7), ] # Instantiate StatsForecast class sf = StatsForecast( models=models, freq='D', n_jobs=-1, fallback_model=SeasonalNaive(season_length=7) ) ``` ```text theme={null} /home/ubuntu/fede/statsforecast/statsforecast/core.py:21: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console) from tqdm.autonotebook import tqdm ``` The `forecast` method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h` (int): represents the forecast h steps into the future. In this case, 12 months ahead. * `level` (list of floats): this optional parameter is used for probabilistic forecasting. Set the `level` (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. > **Note** > > The `forecast` is inteded to be compatible with distributed clusters, > so it does not store any model parameters. If you want to store > parameter for everymodel you can use the `fit` and `predict` methods. > However, those methods are not defined for distrubed engines like > Spark, Ray or Dask. ```python theme={null} from time import time ``` ```python theme={null} init = time() forecasts_df = sf.forecast(df=Y_df_m5, h=28) end = time() print(f'Statsforecast time M5 {(end - init) / 60}') ``` ```text theme={null} Statsforecast time M5 14.274124479293823 ``` Store the results for further evaluation. ```python theme={null} forecasts_df['ThETS'] = forecasts_df[['DynamicOptimizedTheta', 'AutoETS']].clip(0).median(axis=1, numeric_only=True) forecasts_df.to_parquet('s3://m5-benchmarks/forecasts/statsforecast-m5.parquet') ``` ## Evaluation This section evaluates the performance of `StatsForecast` and `AmazonForecast`. To do this, we first need to install [datasetsforecast](https://github.com/Nixtla/datasetsforecast), a Python library developed by Nixtla that includes a large battery of benchmark datasets and evaluation utilities. The library will allow us to calculate the performance of the models using the original evaluation used in the competition. ```python theme={null} %%capture !pip install datasetsforecast ``` ```python theme={null} from datasetsforecast.m5 import M5, M5Evaluation ``` The following function will allow us to evaluate a specific model included in the input dataframe. The function is useful for evaluating different models. ```python theme={null} from datasetsforecast.m5 import M5, M5Evaluation from statsforecast import StatsForecast ### Evaluator def evaluate_forecasts(df, model, model_name): Y_hat = df.set_index('ds', append=True)[model].unstack() *_, S_df = M5.load('data') Y_hat = S_df.merge(Y_hat, how='left', on=['unique_id']) eval_ = M5Evaluation.evaluate(y_hat=Y_hat, directory='./data') eval_ = eval_.rename(columns={'wrmsse': f'{model_name}_{model}_wrmsse'}) return eval_ ``` Now let’s read the forecasts generated for each solution. ```python theme={null} ### Read Forecasts statsforecasts_df = pd.read_parquet('s3://m5-benchmarks/forecasts/statsforecast-m5.parquet') amazonforecasts_df = pd.read_parquet('s3://m5-benchmarks/forecasts/amazonforecast-m5.parquet') ### Amazon Forecast wrangling amazonforecasts_df = amazonforecasts_df.rename(columns={'item_id': 'unique_id', 'date': 'ds'}) # amazon forecast returns the unique_id column in lower case # we need to transform it to upper case to ensure proper merging amazonforecasts_df['unique_id'] = amazonforecasts_df['unique_id'].str.upper() amazonforecasts_df = amazonforecasts_df.set_index('unique_id') # parse datestamp amazonforecasts_df['ds'] = pd.to_datetime(amazonforecasts_df['ds']).dt.tz_localize(None) ``` Finally, let’s use our predefined function to compute the performance of each model. ```python theme={null} ### Evaluate performances m5_eval_df = pd.concat([ evaluate_forecasts(statsforecasts_df, 'ThETS', 'StatsForecast'), evaluate_forecasts(statsforecasts_df, 'AutoETS', 'StatsForecast'), evaluate_forecasts(statsforecasts_df, 'DynamicOptimizedTheta', 'StatsForecast'), evaluate_forecasts(amazonforecasts_df, 'p50', 'AmazonForecast'), ], axis=1) m5_eval_df.T ``` | | Total | Level1 | Level2 | Level3 | Level4 | Level5 | Level6 | Level7 | Level8 | Level9 | Level10 | Level11 | Level12 | | -------------------------------------------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | | StatsForecast\_ThETS\_wrmsse | 0.669606 | 0.424331 | 0.515777 | 0.580670 | 0.474098 | 0.552459 | 0.578092 | 0.651079 | 0.642446 | 0.725324 | 1.009390 | 0.967537 | 0.914068 | | StatsForecast\_AutoETS\_wrmsse | 0.672404 | 0.430474 | 0.516340 | 0.580736 | 0.482090 | 0.559721 | 0.579939 | 0.655362 | 0.643638 | 0.727967 | 1.010596 | 0.968168 | 0.913820 | | StatsForecast\_DynamicOptimizedTheta\_wrmsse | 0.675333 | 0.429670 | 0.521640 | 0.589278 | 0.478730 | 0.557520 | 0.584278 | 0.656283 | 0.650613 | 0.731735 | 1.013910 | 0.971758 | 0.918576 | | AmazonForecast\_p50\_wrmsse | 1.617815 | 1.912144 | 1.786991 | 1.736382 | 1.972658 | 2.010498 | 1.805926 | 1.819329 | 1.667225 | 1.619216 | 1.156432 | 1.012942 | 0.914040 | The results (including processing time and costs) can be summarized in the following table. image # AutoARIMA Comparison (Prophet and pmdarima) Source: https://nixtlaverse.nixtla.io/statsforecast/docs/experiments/autoarima_vs_prophet.html Open In Colab ## Motivation The `AutoARIMA` model is widely used to forecast time series in production and as a benchmark. However, the python implementation (`pmdarima`) is so slow that prevent data scientist practioners from quickly iterating and deploying `AutoARIMA` in production for a large number of time series. In this notebook we present Nixtla’s `AutoARIMA` based on the R implementation (developed by Rob Hyndman) and optimized using `numba`. ## Example ### Libraries ```python theme={null} %%capture # !pip install statsforecast prophet statsmodels sklearn matplotlib pmdarima ``` ```python theme={null} import logging import os import random import time import warnings warnings.filterwarnings("ignore") from itertools import product from multiprocessing import cpu_count, Pool # for prophet import matplotlib.pyplot as plt import numpy as np import pandas as pd from pmdarima import auto_arima as auto_arima_p from prophet import Prophet from statsforecast import StatsForecast from statsforecast.models import AutoARIMA, _TS from statsmodels.graphics.tsaplots import plot_acf from sklearn.model_selection import ParameterGrid from utilsforecast.plotting import plot_series ``` #### Useful functions ```python theme={null} def plot_autocorrelation_grid(df_train): fig, axes = plt.subplots(4, 2, figsize = (24, 14)) unique_ids = df_train['unique_id'].unique() assert len(unique_ids) >= 8, "Must provide at least 8 ts" unique_ids = random.sample(list(unique_ids), k=8) for uid, (idx, idy) in zip(unique_ids, product(range(4), range(2))): train_uid = df_train.query('unique_id == @uid') plot_acf(train_uid['y'].values, ax=axes[idx, idy], title=f'ACF M4 Hourly {uid}') axes[idx, idy].set_xlabel('Timestamp [t]') axes[idx, idy].set_ylabel('Autocorrelation') fig.subplots_adjust(hspace=0.5) plt.show() ``` ### Data For testing purposes, we will use the Hourly dataset from the M4 competition. ```python theme={null} train = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly.csv') test = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly-test.csv').rename(columns={'y': 'y_test'}) ``` In this example we will use a subset of the data to avoid waiting too long. You can modify the number of series if you want. ```python theme={null} n_series = 16 uids = train['unique_id'].unique()[:n_series] train = train.query('unique_id in @uids') test = test.query('unique_id in @uids') ``` ```python theme={null} plot_series(train, test, max_ids=n_series) ``` Would an autorregresive model be the right choice for our data? There is no doubt that we observe seasonal periods. The autocorrelation function (`acf`) can help us to answer the question. Intuitively, we have to observe a decreasing correlation to opt for an AR model. ```python theme={null} plot_autocorrelation_grid(train) ``` Thus, we observe a high autocorrelation for previous lags and also for the seasonal lags. Therefore, we will let `auto_arima` to handle our data. ### Training and forecasting `StatsForecast` receives a list of models to fit each time series. Since we are dealing with Hourly data, it would be benefitial to use 24 as seasonality. ```python theme={null} ?AutoARIMA ``` ```text theme={null} Init signature: AutoARIMA( d: Optional[int] = None, D: Optional[int] = None, max_p: int = 5, max_q: int = 5, max_P: int = 2, max_Q: int = 2, max_order: int = 5, max_d: int = 2, max_D: int = 1, start_p: int = 2, start_q: int = 2, start_P: int = 1, start_Q: int = 1, stationary: bool = False, seasonal: bool = True, ic: str = 'aicc', stepwise: bool = True, nmodels: int = 94, trace: bool = False, approximation: Optional[bool] = False, method: Optional[str] = None, truncate: Optional[bool] = None, test: str = 'kpss', test_kwargs: Optional[str] = None, seasonal_test: str = 'seas', seasonal_test_kwargs: Optional[Dict] = None, allowdrift: bool = False, allowmean: bool = False, blambda: Optional[float] = None, biasadj: bool = False, season_length: int = 1, alias: str = 'AutoARIMA', prediction_intervals: Optional[statsforecast.utils.ConformalIntervals] = None, ) Docstring: AutoARIMA model. Automatically selects the best ARIMA (AutoRegressive Integrated Moving Average) model using an information criterion. Default is Akaike Information Criterion (AICc). **Note:**
This implementation is a mirror of Hyndman's [forecast::auto.arima](https://github.com/robjhyndman/forecast). **References:**
[Rob J. Hyndman, Yeasmin Khandakar (2008). "Automatic Time Series Forecasting: The forecast package for R"](https://www.jstatsoft.org/article/view/v027i03). Parameters ---------- d : Optional[int] Order of first-differencing. D : Optional[int] Order of seasonal-differencing. max_p : int Max autorregresives p. max_q : int Max moving averages q. max_P : int Max seasonal autorregresives P. max_Q : int Max seasonal moving averages Q. max_order : int Max p+q+P+Q value if not stepwise selection. max_d : int Max non-seasonal differences. max_D : int Max seasonal differences. start_p : int Starting value of p in stepwise procedure. start_q : int Starting value of q in stepwise procedure. start_P : int Starting value of P in stepwise procedure. start_Q : int Starting value of Q in stepwise procedure. stationary : bool If True, restricts search to stationary models. seasonal : bool If False, restricts search to non-seasonal models. ic : str Information criterion to be used in model selection. stepwise : bool If True, will do stepwise selection (faster). nmodels : int Number of models considered in stepwise search. trace : bool If True, the searched ARIMA models is reported. approximation : Optional[bool] If True, conditional sums-of-squares estimation, final MLE. method : Optional[str] Fitting method between maximum likelihood or sums-of-squares. truncate : Optional[int] Observations truncated series used in model selection. test : str Unit root test to use. See `ndiffs` for details. test_kwargs : Optional[str] Unit root test additional arguments. seasonal_test : str Selection method for seasonal differences. seasonal_test_kwargs : Optional[dict] Seasonal unit root test arguments. allowdrift : bool (default True) If True, drift models terms considered. allowmean : bool (default True) If True, non-zero mean models considered. blambda : Optional[float] Box-Cox transformation parameter. biasadj : bool Use adjusted back-transformed mean Box-Cox. season_length : int Number of observations per unit of time. Ex: 24 Hourly data. alias : str Custom name of the model. prediction_intervals : Optional[ConformalIntervals] Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. File: /hdd/github/statsforecast/statsforecast/models.py Type: type Subclasses: ``` As we see, we can pass `season_length` to `AutoARIMA`, so the definition of our models would be, ```python theme={null} models = [AutoARIMA(season_length=24, approximation=True)] ``` ```python theme={null} fcst = StatsForecast(df=train, models=models, freq='H', n_jobs=-1) ``` ```python theme={null} init = time.time() forecasts = fcst.forecast(48) end = time.time() time_nixtla = end - init time_nixtla ``` ```text theme={null} 40.38660216331482 ``` ```python theme={null} forecasts.head() ``` | | ds | AutoARIMA | | ---------- | --- | ---------- | | unique\_id | | | | H1 | 701 | 616.084167 | | H1 | 702 | 544.432129 | | H1 | 703 | 510.414490 | | H1 | 704 | 481.046539 | | H1 | 705 | 460.893066 | ```python theme={null} forecasts = forecasts.reset_index() ``` ```python theme={null} test = test.merge(forecasts, how='left', on=['unique_id', 'ds']) ``` ```python theme={null} plot_series(train, test) ``` ## Alternatives ### pmdarima You can use the `StatsForecast` class to parallelize your own models. In this section we will use it to run the `auto_arima` model from `pmdarima`. ```python theme={null} class PMDAutoARIMA(_TS): def __init__(self, season_length: int): self.season_length = season_length def forecast(self, y, h, X=None, X_future=None, fitted=False): mod = auto_arima_p( y, m=self.season_length, with_intercept=False #ensure comparability with Nixtla's implementation ) return {'mean': mod.predict(h)} def __repr__(self): return 'pmdarima' ``` ```python theme={null} n_series_pmdarima = 2 ``` ```python theme={null} fcst = StatsForecast( df = train.query('unique_id in ["H1", "H10"]'), models=[PMDAutoARIMA(season_length=24)], freq='H', n_jobs=-1 ) ``` ```python theme={null} init = time.time() forecast_pmdarima = fcst.forecast(48) end = time.time() time_pmdarima = end - init time_pmdarima ``` ```text theme={null} 886.2768685817719 ``` ```python theme={null} forecast_pmdarima.head() ``` | | ds | pmdarima | | ---------- | --- | ---------- | | unique\_id | | | | H1 | 701 | 628.310547 | | H1 | 702 | 571.659851 | | H1 | 703 | 543.504700 | | H1 | 704 | 517.539062 | | H1 | 705 | 502.829559 | ```python theme={null} forecast_pmdarima = forecast_pmdarima.reset_index() ``` ```python theme={null} test = test.merge(forecast_pmdarima, how='left', on=['unique_id', 'ds']) ``` ```python theme={null} plot_series(train, test, plot_random=False) ``` ### Prophet `Prophet` is designed to receive a pandas dataframe, so we cannot use `StatForecast`. Therefore, we need to parallize from scratch. ```python theme={null} params_grid = {'seasonality_mode': ['multiplicative','additive'], 'growth': ['linear', 'flat'], 'changepoint_prior_scale': [0.1, 0.2, 0.3, 0.4, 0.5], 'n_changepoints': [5, 10, 15, 20]} grid = ParameterGrid(params_grid) ``` ```python theme={null} def fit_and_predict(index, ts): df = ts.drop(columns='unique_id', axis=1) max_ds = df['ds'].max() df['ds'] = pd.date_range(start='1970-01-01', periods=df.shape[0], freq='H') df_val = df.tail(48) df_train = df.drop(df_val.index) y_val = df_val['y'].values if len(df_train) >= 48: val_results = {'losses': [], 'params': []} for params in grid: model = Prophet(seasonality_mode=params['seasonality_mode'], growth=params['growth'], weekly_seasonality=True, daily_seasonality=True, yearly_seasonality=True, n_changepoints=params['n_changepoints'], changepoint_prior_scale=params['changepoint_prior_scale']) model = model.fit(df_train) forecast = model.make_future_dataframe(periods=48, include_history=False, freq='H') forecast = model.predict(forecast) forecast['unique_id'] = index forecast = forecast.filter(items=['unique_id', 'ds', 'yhat']) loss = np.mean(abs(y_val - forecast['yhat'].values)) val_results['losses'].append(loss) val_results['params'].append(params) idx_params = np.argmin(val_results['losses']) params = val_results['params'][idx_params] else: params = {'seasonality_mode': 'multiplicative', 'growth': 'flat', 'n_changepoints': 150, 'changepoint_prior_scale': 0.5} model = Prophet(seasonality_mode=params['seasonality_mode'], growth=params['growth'], weekly_seasonality=True, daily_seasonality=True, yearly_seasonality=True, n_changepoints=params['n_changepoints'], changepoint_prior_scale=params['changepoint_prior_scale']) model = model.fit(df) forecast = model.make_future_dataframe(periods=48, include_history=False, freq='H') forecast = model.predict(forecast) forecast.insert(0, 'unique_id', index) forecast['ds'] = np.arange(max_ds + 1, max_ds + 48 + 1) forecast = forecast.filter(items=['unique_id', 'ds', 'yhat']) return forecast ``` ```python theme={null} init = time.time() with Pool(cpu_count()) as pool: forecast_prophet = pool.starmap(fit_and_predict, train.groupby('unique_id')) end = time.time() forecast_prophet = pd.concat(forecast_prophet).rename(columns={'yhat': 'prophet'}) time_prophet = end - init time_prophet ``` ```text theme={null} 120.7272641658783 ``` ```python theme={null} forecast_prophet ``` | | unique\_id | ds | prophet | | --- | ---------- | --- | ----------- | | 0 | H1 | 701 | 635.914254 | | 1 | H1 | 702 | 565.976464 | | 2 | H1 | 703 | 505.095507 | | 3 | H1 | 704 | 462.559539 | | 4 | H1 | 705 | 438.766801 | | ... | ... | ... | ... | | 43 | H112 | 744 | 6184.686240 | | 44 | H112 | 745 | 6188.851888 | | 45 | H112 | 746 | 6129.306256 | | 46 | H112 | 747 | 6058.040672 | | 47 | H112 | 748 | 5991.982370 | ```python theme={null} test = test.merge(forecast_prophet, how='left', on=['unique_id', 'ds']) ``` ```python theme={null} plot_series(train, test) ``` ### Evaluation ### Time Since `AutoARIMA` works with numba is useful to calculate the time for just one time series. ```python theme={null} fcst = StatsForecast(df=train.query('unique_id == "H1"'), models=models, freq='H', n_jobs=1) ``` ```python theme={null} init = time.time() forecasts = fcst.forecast(48) end = time.time() time_nixtla_1 = end - init time_nixtla_1 ``` ```text theme={null} 18.752424716949463 ``` ```python theme={null} times = pd.DataFrame({'n_series': np.arange(1, 414 + 1)}) times['pmdarima'] = time_pmdarima * times['n_series'] / n_series_pmdarima times['prophet'] = time_prophet * times['n_series'] / n_series times['AutoARIMA_nixtla'] = time_nixtla_1 + times['n_series'] * (time_nixtla - time_nixtla_1) / n_series times = times.set_index('n_series') ``` ```python theme={null} times.tail(5) ``` | | pmdarima | prophet | AutoARIMA\_nixtla | | --------- | ------------- | ----------- | ----------------- | | n\_series | | | | | 410 | 181686.758059 | 3093.636144 | 573.128222 | | 411 | 182129.896494 | 3101.181598 | 574.480358 | | 412 | 182573.034928 | 3108.727052 | 575.832494 | | 413 | 183016.173362 | 3116.272506 | 577.184630 | | 414 | 183459.311796 | 3123.817960 | 578.536766 | ```python theme={null} fig, axes = plt.subplots(1, 2, figsize = (24, 7)) (times/3600).plot(ax=axes[0], linewidth=4) np.log10(times).plot(ax=axes[1], linewidth=4) axes[0].set_title('Time across models [Hours]', fontsize=22) axes[1].set_title('Time across models [Log10 Scale]', fontsize=22) axes[0].set_ylabel('Time [Hours]', fontsize=20) axes[1].set_ylabel('Time Seconds [Log10 Scale]', fontsize=20) fig.suptitle('Time comparison using M4-Hourly data', fontsize=27) for ax in axes: ax.set_xlabel('Number of Time Series [N]', fontsize=20) ax.legend(prop={'size': 20}) ax.grid() for label in (ax.get_xticklabels() + ax.get_yticklabels()): label.set_fontsize(20) ``` ```python theme={null} fig.savefig('computational-efficiency.png', dpi=300) ``` ### Performance #### pmdarima (only two time series) ```python theme={null} name_models = test.drop(columns=['unique_id', 'ds', 'y_test']).columns.tolist() ``` ```python theme={null} test_pmdarima = test.query('unique_id in ["H1", "H10"]') eval_pmdarima = [] for model in name_models: mae = np.mean(abs(test_pmdarima[model] - test_pmdarima['y_test'])) eval_pmdarima.append({'model': model, 'mae': mae}) pd.DataFrame(eval_pmdarima).sort_values('mae') ``` | | model | mae | | - | --------- | --------- | | 0 | AutoARIMA | 20.289669 | | 1 | pmdarima | 24.676279 | | 2 | prophet | 39.201933 | #### Prophet ```python theme={null} eval_prophet = [] for model in name_models: if 'pmdarima' in model: continue mae = np.mean(abs(test[model] - test['y_test'])) eval_prophet.append({'model': model, 'mae': mae}) pd.DataFrame(eval_prophet).sort_values('mae') ``` | | model | mae | | - | --------- | ----------- | | 0 | AutoARIMA | 680.202965 | | 1 | prophet | 1058.578963 | For a complete comparison check the [complete experiment](https://github.com/Nixtla/statsforecast/tree/v0.6.0/experiments/arima). Open In Colab # AutoARIMAProphet Adapter Source: https://nixtlaverse.nixtla.io/statsforecast/docs/experiments/autoarimaprophet_adapter.html > AutoArima is faster and more accurate than FB-Prophet in most cases. > Replace it with two lines of code. We benchmarked on more than 100K series and show that you can improve *MAPE* and *sMAPE* forecast accuracy by *17%* and *15%* with *37x* less computational time. Now you can replace Prophet with two lines of code and verify it for yourself. ### Install StatsForecast ```bash theme={null} pip install statsforecast ``` ### Replace FB-Prophet to make your forecast a lot faster We respected the original Prophet syntax so your pipeline won’t get affected. ## Results on M3, M4, Tourism and Peyton Manning ## Background [FB-Prophet](https://github.com/facebook/prophet) is one of the world’s most used time series forecasting models. Its GitHub repository has more than 14 thousand stars, and more than a hundred repositories depend on it. However, in many scenarios, [FB-Prophet does not offer good performance in terms of time and accuracy.](https://analyticsindiamag.com/why-are-people-bashing-facebook-prophet/) This lacking performance suggests that the FB-Prophet’s success can be explained mainly by its usability. For example, [adding exogenous and calendar variables is almost trivial.](https://facebook.github.io/prophet/docs/seasonality,_holiday_effects,_and_regressors.html) To contribute to the forecasting community, we created a *FB-Prophet API adapter* that lets you use Prophet’s useful methods without accuracy and computational downsides. Just import this AutoARIMAProphet adapter and replace the Prophet class to start using AutoARIMA in any of your existing pipelines. ## Empirical validation To validate the AutoARIMAProphet adapter, we designed a pipeline considering the M3, M4, and Tourism datasets (standard benchmarks in the forecasting practice) and the Peyton Manning data set originally used by FB. The pipeline automatically selects ARIMA’s parameters with the AIC criterion and selects Prophet’s parameters with time-series cross-validation. ## Results The following table shows the MAPE, sMAPE and Time (in minutes) AutoARIMA improvements on Prophet for each dataset. ## Reproducibility 1. Create a conda environment `arima_prophet` using the `environment.yml` file. ```shell theme={null} conda env create -f environment.yml ``` 2. Activate the conda environment using ```shell theme={null} conda activate arima_prophet ``` 3. Run the experiments for each dataset and each model using ```shell theme={null} python -m src.experiment --dataset [dataset] --group [group] --model_name [model_name] ``` For `M4`, the groups are `Yearly`, `Monthly`, `Quarterly`, `Weekly`, `Daily`, and `Hourly`. For `M3`, the groups are `Yearly`, `Monthly`, `Quarterly`, and `Other`. For `Tourism`, the groups are `Yearly`, `Monthly`, and `Quarterly`. Finally, for `PeytonManning` the group is `Daily`. 4. Evaluate the results using ```shell theme={null} python -m src.evaluation ``` ## Conclusion * Be mindful on what you read on Towards Data Science. * Always use strong baselines when forecasting. * Quick and easy results are sometimes [misleading](https://en.wikipedia.org/wiki/Streetlight_effect). * Simpler models are sometimes [better](https://en.wikipedia.org/wiki/Occam%27s_razor). * **Facebook’s Prophet might be many things, but it’s definitely not a model for forecasting time series at scale.** ## Misc. * [`StatsForecast`](https://github.com/nixtla/statsforecast) also includes a variety of lightning fast baseline models. * If you really need to do forecast at scale, [here](https://github.com/nixtla/statsforecast/tree/main/experiments/ray) we show how to forecast 1 million time series under 30 minutes using [Ray](https://github.com/ray-project/ray). * If you are interested in SOTA Deep Learning models, check [`NeuralForecast`](https://github.com/nixtla/neuralforecast). # Forecasting at Scale using ETS and ray (M5) Source: https://nixtlaverse.nixtla.io/statsforecast/docs/experiments/ets_ray_m5.html > Forecast the M5 dataset In this notebook we show how to use `StatsForecast` and `ray` to forecast thounsands of time series in less than 6 minutes (M5 dataset). Also, we show that `StatsForecast` has better performance in time and accuracy compared to [`Prophet` running on a Spark cluster](./prophet_spark_m5.html) using DataBricks. In this example, we used a ray cluster (AWS) of 11 instances of type m5.2xlarge (8 cores, 32 GB RAM). ## Installing StatsForecast Library ```python theme={null} %%capture !pip install "statsforecast[ray]" neuralforecast s3fs pyarrow ``` ```python theme={null} %%capture from time import time import pandas as pd from neuralforecast.data.datasets.m5 import M5, M5Evaluation from statsforecast import StatsForecast from statsforecast.models import ETS ``` ## Download data The example uses the [M5 dataset](https://github.com/Mcompetitions/M5-methods/blob/master/M5-Competitors-Guide.pdf). It consists of `30,490` bottom time series. ```python theme={null} Y_df = pd.read_parquet('s3://m5-benchmarks/data/train/target.parquet') Y_df = Y_df.rename(columns={ 'item_id': 'unique_id', 'timestamp': 'ds', 'demand': 'y' }) Y_df['ds'] = pd.to_datetime(Y_df['ds']) ``` ```python theme={null} Y_df.head() ``` | | unique\_id | ds | y | | - | -------------------- | ---------- | --- | | 0 | FOODS\_1\_001\_CA\_1 | 2011-01-29 | 3.0 | | 1 | FOODS\_1\_001\_CA\_1 | 2011-01-30 | 0.0 | | 2 | FOODS\_1\_001\_CA\_1 | 2011-01-31 | 0.0 | | 3 | FOODS\_1\_001\_CA\_1 | 2011-02-01 | 1.0 | | 4 | FOODS\_1\_001\_CA\_1 | 2011-02-02 | 4.0 | Since the M5 dataset contains intermittent time series, we add a constant to avoid problems during the training phase. Later, we will substract the constant from the forecasts. ```python theme={null} constant = 10 Y_df['y'] += constant ``` ## Train the model `StatsForecast` receives a list of models to fit each time series. Since we are dealing with Daily data, it would be benefitial to use 7 as seasonality. Observe that we need to pass the ray address to the `ray_address` argument. ```python theme={null} fcst = StatsForecast( df=Y_df, models=[ETS(season_length=7, model='ZNA')], freq='D', #n_jobs=-1 ray_address='ray://ADDRESS:10001' ) ``` ```python theme={null} init = time() Y_hat = fcst.forecast(28) end = time() print(f'Minutes taken by StatsForecast using: {(end - init) / 60}') ``` ```text theme={null} /home/ubuntu/miniconda/envs/ray/lib/python3.7/site-packages/ray/util/client/worker.py:618: UserWarning: More than 10MB of messages have been created to schedule tasks on the server. This can be slow on Ray Client due to communication overhead over the network. If you're running many fine-grained tasks, consider running them inside a single remote function. See the section on "Too fine-grained tasks" in the Ray Design Patterns document for more details: https://docs.google.com/document/d/167rnnDFIVRhHhK4mznEIemOtj63IOhtIPvSYaPgI4Fg/edit#heading=h.f7ins22n6nyl. If your functions frequently use large objects, consider storing the objects remotely with ray.put. An example of this is shown in the "Closure capture of large / unserializable object" section of the Ray Design Patterns document, available here: https://docs.google.com/document/d/167rnnDFIVRhHhK4mznEIemOtj63IOhtIPvSYaPgI4Fg/edit#heading=h.1afmymq455wu UserWarning, ``` ```text theme={null} Minutes taken by StatsForecast using: 5.4817593971888225 ``` `StatsForecast` and `ray` took only 5.48 minutes to train `30,490` time series, compared to 18.23 minutes for Prophet and Spark. We remove the constant. ```python theme={null} Y_hat['ETS'] -= constant ``` ### Evaluating performance The M5 competition used the weighted root mean squared scaled error. You can find details of the metric [here](https://github.com/Mcompetitions/M5-methods/blob/master/M5-Competitors-Guide.pdf). ```python theme={null} Y_hat = Y_hat.reset_index().set_index(['unique_id', 'ds']).unstack() Y_hat = Y_hat.droplevel(0, 1).reset_index() ``` ```python theme={null} *_, S_df = M5.load('./data') Y_hat = S_df.merge(Y_hat, how='left', on=['unique_id']) ``` ```text theme={null} 100%|███████████████████████████████████████████████████████████| 50.2M/50.2M [00:00<00:00, 77.1MiB/s] ``` ```python theme={null} M5Evaluation.evaluate(y_hat=Y_hat, directory='./data') ``` | | wrmsse | | ------- | -------- | | Total | 0.677233 | | Level1 | 0.435558 | | Level2 | 0.522863 | | Level3 | 0.582109 | | Level4 | 0.488484 | | Level5 | 0.567825 | | Level6 | 0.587605 | | Level7 | 0.662774 | | Level8 | 0.647712 | | Level9 | 0.732107 | | Level10 | 1.013124 | | Level11 | 0.970465 | | Level12 | 0.916175 | Also, `StatsForecast` is more accurate than Prophet, since the overall WMRSSE is `0.68`, against `0.77` obtained by prophet. # StatsForecast ETS and Facebook Prophet on Spark (M5) Source: https://nixtlaverse.nixtla.io/statsforecast/docs/experiments/prophet_spark_m5.html > This notebook was originally executed using DataBricks The purpose of this notebook is to create a scalability benchmark (time and performance). To that end, Nixtla’s [StatsForecast](https://github.com/Nixtla/statsforecast) (using the ETS model) is trained on the M5 dataset using spark to distribute the training. As a comparison, Facebook’s [Prophet](https://github.com/facebook/prophet) model is used. An AWS cluster (mounted on databricks) of 11 instances of type m5.2xlarge (8 cores, 32 GB RAM) with runtime 10.4 LTS was used. [This](https://d1r5llqwmkrl74.cloudfront.net/notebooks/RCG/Fine_Grained_Demand_Forecasting/index.html#Fine_Grained_Demand_Forecasting_1.html) notebook was used as base case. The example uses the [M5 dataset](https://github.com/Mcompetitions/M5-methods/blob/master/M5-Competitors-Guide.pdf). It consists of `30,490` bottom time series. ## Main results | Method | Time (mins) | Performance (wRMSSE) | | ------------- | ----------: | -------------------: | | StatsForecast | 7.5 | 0.68 | | Prophet | 18.23 | 0.77 | ## Installing libraries ```python theme={null} pip install prophet "neuralforecast<1.0.0" "statsforecast[fugue]" ``` ## StatsForecast pipeline ```python theme={null} from time import time from neuralforecast.data.datasets.m5 import M5, M5Evaluation from statsforecast.distributed.utils import forecast from statsforecast.distributed.fugue import FugueBackend from statsforecast.models import ETS, SeasonalNaive from statsforecast.core import StatsForecast from pyspark.sql import SparkSession ``` ```python theme={null} spark = SparkSession.builder.getOrCreate() backend = FugueBackend(spark, {"fugue.spark.use_pandas_udf":True}) ``` ### Forecast With statsforecast you don’t have to download your data. The distributed backend can handle a file with your data. ```python theme={null} init = time() ets_forecasts = backend.forecast( "s3://m5-benchmarks/data/train/m5-target.parquet", [ETS(season_length=7, model='ZAA')], freq="D", h=28, ).toPandas() end = time() print(f'Minutes taken by StatsForecast on a Spark cluster: {(end - init) / 60}') ``` ### Evaluating performance The M5 competition used the weighted root mean squared scaled error. You can find details of the metric [here](https://github.com/Mcompetitions/M5-methods/blob/master/M5-Competitors-Guide.pdf). ```python theme={null} Y_hat = ets_forecasts.set_index(['unique_id', 'ds']).unstack() Y_hat = Y_hat.droplevel(0, 1).reset_index() ``` ```python theme={null} *_, S_df = M5.load('./data') Y_hat = S_df.merge(Y_hat, how='left', on=['unique_id'])#.drop(columns=['unique_id']) ``` ```python theme={null} wrmsse_ets = M5Evaluation.evaluate(y_hat=Y_hat, directory='./data') ``` ```python theme={null} wrmsse_ets ``` | | wrmsse | | ------- | -------- | | Total | 0.682358 | | Level1 | 0.449115 | | Level2 | 0.533754 | | Level3 | 0.592317 | | Level4 | 0.497086 | | Level5 | 0.572189 | | Level6 | 0.593880 | | Level7 | 0.665358 | | Level8 | 0.652183 | | Level9 | 0.734492 | | Level10 | 1.012633 | | Level11 | 0.969902 | | Level12 | 0.915380 | ## Prophet pipeline ```python theme={null} import logging from time import time import pandas as pd from neuralforecast.data.datasets.m5 import M5, M5Evaluation from prophet import Prophet from pyspark.sql.types import * # disable informational messages from prophet logging.getLogger('py4j').setLevel(logging.ERROR) ``` ### Download data ```python theme={null} # structure of the training data set train_schema = StructType([ StructField('unique_id', StringType()), StructField('ds', DateType()), StructField('y', DoubleType()) ]) # read the training file into a dataframe train = spark.read.parquet( 's3://m5-benchmarks/data/train/m5-target.parquet', header=True, schema=train_schema ) # make the dataframe queriable as a temporary view train.createOrReplaceTempView('train') ``` ```python theme={null} sql_statement = ''' SELECT unique_id AS unique_id, CAST(ds as date) as ds, y as y FROM train ''' m5_history = ( spark .sql( sql_statement ) .repartition(sc.defaultParallelism, ['unique_id']) ).cache() ``` ### Forecast function using Prophet ```python theme={null} def forecast( history_pd: pd.DataFrame ) -> pd.DataFrame: # TRAIN MODEL AS BEFORE # -------------------------------------- # remove missing values (more likely at day-store-item level) history_pd = history_pd.dropna() # configure the model model = Prophet( growth='linear', daily_seasonality=False, weekly_seasonality=True, yearly_seasonality=True, seasonality_mode='multiplicative' ) # train the model model.fit( history_pd ) # -------------------------------------- # BUILD FORECAST AS BEFORE # -------------------------------------- # make predictions future_pd = model.make_future_dataframe( periods=28, freq='d', include_history=False ) forecast_pd = model.predict( future_pd ) # -------------------------------------- # ASSEMBLE EXPECTED RESULT SET # -------------------------------------- # get relevant fields from forecast forecast_pd['unique_id'] = history_pd['unique_id'].unique()[0] f_pd = forecast_pd[['unique_id', 'ds','yhat']] # -------------------------------------- # return expected dataset return f_pd ``` ```python theme={null} result_schema = StructType([ StructField('unique_id', StringType()), StructField('ds',DateType()), StructField('yhat',FloatType()), ]) ``` #### Training Prophet on the M5 dataset ```python theme={null} init = time() results = ( m5_history .groupBy('unique_id') .applyInPandas(forecast, schema=result_schema) ).toPandas() end = time() print(f'Minutes taken by Prophet on a Spark cluster: {(end - init) / 60}') ``` ### Evaluating performance The M5 competition used the weighted root mean squared scaled error. You can find details of the metric [here](https://github.com/Mcompetitions/M5-methods/blob/master/M5-Competitors-Guide.pdf). ```python theme={null} Y_hat = results.set_index(['unique_id', 'ds']).unstack() Y_hat = Y_hat.droplevel(0, 1).reset_index() ``` ```python theme={null} *_, S_df = M5.load('./data') Y_hat = S_df.merge(Y_hat, how='left', on=['unique_id'])#.drop(columns=['unique_id']) ``` ```python theme={null} wrmsse = M5Evaluation.evaluate(y_hat=Y_hat, directory='./data') ``` ```python theme={null} wrmsse ``` | | wrmsse | | ------- | -------- | | Total | 0.771800 | | Level1 | 0.507905 | | Level2 | 0.586328 | | Level3 | 0.666686 | | Level4 | 0.549358 | | Level5 | 0.655003 | | Level6 | 0.647176 | | Level7 | 0.747047 | | Level8 | 0.743422 | | Level9 | 0.824667 | | Level10 | 1.207069 | | Level11 | 1.108780 | | Level12 | 1.018163 | # End to End Walkthrough | StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/docs/getting-started/getting_started_complete.html > Model training, evaluation and selection for multiple time series > **Prerequisites** > > This Guide assumes basic familiarity with StatsForecast. For a minimal > example visit the [Quick Start](./getting_started_short.html). Follow this article for a step-by-step guide on building a production-ready forecasting pipeline for multiple time series. During this guide you will gain familiarity with the core `StatsForecast`class and some relevant methods like `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation.` We will use a classical benchmarking dataset from the M4 competition. The dataset includes time series from different domains like finance, economy and sales. In this example, we will use a subset of the Hourly dataset. We will model each time series individually. Forecasting at this level is also known as local forecasting. Therefore, you will train a series of models for every unique series and then select the best one. StatsForecast focuses on speed, simplicity, and scalability, which makes it ideal for this task. **Outline:** 1. Install packages. 2. Read the data. 3. Explore the data. 4. Train many models for every unique combination of time series. 5. Evaluate the model’s performance using cross-validation. 6. Select the best model for every unique time series. > **Not Covered in this guide** > > * Forecasting at scale using clusters on the cloud. > * [Forecast the M5 Dataset in > 5min](../experiments/ets_ray_m5.html) using Ray clusters. > * [Forecast the M5 Dataset in > 5min](../experiments/prophet_spark_m5.html) using Spark > clusters. > * Learn how to predict [1M series in less than > 30min](https://www.anyscale.com/blog/how-nixtla-uses-ray-to-accurately-predict-more-than-a-million-time-series). > * Training models on Multiple Seasonalities. > * Learn to use multiple seasonality in this [Electricity Load > forecasting](../tutorials/electricityloadforecasting.html) > tutorial. > * Using external regressors or exogenous variables > * Follow this tutorial to [include exogenous > variables](../how-to-guides/exogenous.html) like weather or > holidays or static variables like category or family. > * Comparing StatsForecast with other popular libraries. > * You can reproduce our benchmarks > [here](https://github.com/Nixtla/statsforecast/tree/main/experiments). ## Install libraries We assume you have StatsForecast already installed. Check this guide for instructions on [how to install StatsForecast](./installation.html). ## Read the data We will use pandas to read the M4 Hourly data set stored in a parquet file for efficiency. You can use ordinary pandas operations to read your data in other formats likes `.csv`. The input to StatsForecast is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestamp ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. The target column needs to be renamed to `y` if it has a different column name. This data set already satisfies the requirements. Depending on your internet connection, this step should take around 10 seconds. ```python theme={null} import pandas as pd ``` ```python theme={null} Y_df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | This dataset contains 414 unique series with 900 observations on average. For this example and reproducibility’s sake, we will select only 10 unique IDs and keep only the last week. Depending on your processing infrastructure feel free to select more or less series. > **Note** > > Processing time is dependent on the available computing resources. > Running this example with the complete dataset takes around 10 minutes > in a c5d.24xlarge (96 cores) instance from AWS. ```python theme={null} uids = Y_df['unique_id'].unique()[:10] # Select 10 ids to make the example faster Y_df = Y_df.query('unique_id in @uids') Y_df = Y_df.groupby('unique_id').tail(7 * 24) #Select last 7 days of data to make example faster ``` ## Explore Data with the plot method Plot some series using the `plot` method from the `StatsForecast` class. This method prints 8 random series from the dataset and is useful for basic EDA. > **Note** > > The `StatsForecast.plot` method uses Plotly as a default engine. You > can change to MatPlotLib by setting `engine="matplotlib"`. ```python theme={null} from statsforecast import StatsForecast ``` ```python theme={null} StatsForecast.plot(Y_df) ``` ## Train multiple models for many series StatsForecast can train many models on many time series efficiently. Start by importing and instantiating the desired models. StatsForecast offers a wide variety of models grouped in the following categories: * **Auto Forecast:** Automatic forecasting tools search for the best parameters and select the best possible model for a series of time series. These tools are useful for large collections of univariate time series. Includes automatic versions of: Arima, ETS, Theta, CES. * **Exponential Smoothing:** Uses a weighted average of all past observations where the weights decrease exponentially into the past. Suitable for data with no clear trend or seasonality. Examples: SES, Holt’s Winters, SSO. * **Benchmark models:** classical models for establishing baselines. Examples: Mean, Naive, Random Walk * **Intermittent or Sparse models:** suited for series with very few non-zero observations. Examples: CROSTON, ADIDA, IMAPA * **Multiple Seasonalities:** suited for signals with more than one clear seasonality. Useful for low-frequency data like electricity and logs. Examples: MSTL. * **Theta Models:** fit two theta lines to a deseasonalized time series, using different techniques to obtain and combine the two theta lines to produce the final forecasts. Examples: Theta, DynamicTheta Here you can check the complete list of [models](../../src/core/models_intro.html) . For this example we will use: * `AutoARIMA`: Automatically selects the best ARIMA (AutoRegressive Integrated Moving Average) model using an information criterion. Ref: `AutoARIMA`. * `HoltWinters`: triple exponential smoothing, Holt-Winters’ method is an extension of exponential smoothing for series that contain both trend and seasonality. Ref: `HoltWinters` * `SeasonalNaive`: Memory Efficient Seasonal Naive predictions. Ref: `SeasonalNaive` * `HistoricAverage`: arithmetic mean. Ref: `HistoricAverage`. * `DynamicOptimizedTheta`: The theta family of models has been shown to perform well in various datasets such as M3. Models the deseasonalized time series. Ref: `DynamicOptimizedTheta`. Import and instantiate the models. Setting the `season_length` argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/)) by the master, Rob Hyndmann, can be useful. ```python theme={null} from statsforecast.models import ( HoltWinters, CrostonClassic as Croston, HistoricAverage, DynamicOptimizedTheta as DOT, SeasonalNaive ) ``` ```python theme={null} # Create a list of models and instantiation parameters models = [ HoltWinters(), Croston(), SeasonalNaive(season_length=24), HistoricAverage(), DOT(season_length=24) ] ``` We fit the models by instantiating a new `StatsForecast` object with the following parameters: * `models`: a list of models. Select the models you want from [models](../../src/core/models_intro.html) and import them. * `freq`: a string indicating the frequency of the data. (See [pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs`: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model`: a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} # Instantiate StatsForecast class as sf sf = StatsForecast( models=models, freq=1, fallback_model = SeasonalNaive(season_length=7), n_jobs=-1, ) ``` `<<<<<<< HEAD` `=======` > **Note** > > StatsForecast achieves its blazing speed using JIT compiling through > Numba. The first time you call the statsforecast class, the fit method > should take around 5 seconds. The second time -once Numba compiled > your settings- it should take less than 0.2s. `>>>>>>> f262b71470cd5bd4105e3701c8088b848f98a7af` The `forecast` method takes two arguments: forecasts next `h` (horizon) and `level`. * `h` (int): represents the forecast h steps into the future. In this case, 12 months ahead. * `level` (list of floats): this optional parameter is used for probabilistic forecasting. Set the `level` (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like ARIMA and Theta) > **Note** > > The `forecast` method is compatible with distributed clusters, so it > does not store any model parameters. If you want to store parameters > for every model you can use the `fit` and `predict` methods. However, > those methods are not defined for distributed engines like Spark, Ray > or Dask. ```python theme={null} forecasts_df = sf.forecast(df=Y_df, h=48, level=[90]) forecasts_df.head() ``` | | unique\_id | ds | HoltWinters | HoltWinters-lo-90 | HoltWinters-hi-90 | CrostonClassic | CrostonClassic-lo-90 | CrostonClassic-hi-90 | SeasonalNaive | SeasonalNaive-lo-90 | SeasonalNaive-hi-90 | HistoricAverage | HistoricAverage-lo-90 | HistoricAverage-hi-90 | DynamicOptimizedTheta | DynamicOptimizedTheta-lo-90 | DynamicOptimizedTheta-hi-90 | | - | ---------- | --- | ----------- | ----------------- | ----------------- | -------------- | -------------------- | -------------------- | ------------- | ------------------- | ------------------- | --------------- | --------------------- | --------------------- | --------------------- | --------------------------- | --------------------------- | | 0 | H1 | 749 | 829.0 | 422.549268 | 1235.450732 | 829.0 | 422.549268 | 1235.450732 | 635.0 | 566.036734 | 703.963266 | 660.982143 | 398.037761 | 923.926524 | 592.701851 | 577.677280 | 611.652639 | | 1 | H1 | 750 | 807.0 | 400.549268 | 1213.450732 | 807.0 | 400.549268 | 1213.450732 | 572.0 | 503.036734 | 640.963266 | 660.982143 | 398.037761 | 923.926524 | 525.589117 | 505.449755 | 546.621805 | | 2 | H1 | 751 | 785.0 | 378.549268 | 1191.450732 | 785.0 | 378.549268 | 1191.450732 | 532.0 | 463.036734 | 600.963266 | 660.982143 | 398.037761 | 923.926524 | 489.251814 | 462.072871 | 512.424116 | | 3 | H1 | 752 | 756.0 | 349.549268 | 1162.450732 | 756.0 | 349.549268 | 1162.450732 | 493.0 | 424.036734 | 561.963266 | 660.982143 | 398.037761 | 923.926524 | 456.195032 | 430.554302 | 478.260963 | | 4 | H1 | 753 | 719.0 | 312.549268 | 1125.450732 | 719.0 | 312.549268 | 1125.450732 | 477.0 | 408.036734 | 545.963266 | 660.982143 | 398.037761 | 923.926524 | 436.290514 | 411.051232 | 461.815932 | Plot the results of 8 random series using the `StatsForecast.plot` method. ```python theme={null} sf.plot(Y_df,forecasts_df) ``` The `StatsForecast.plot` allows for further customization. For example, plot the results of the different models and unique ids. ```python theme={null} # Plot to unique_ids and some selected models sf.plot(Y_df, forecasts_df, models=["HoltWinters","DynamicOptimizedTheta"], unique_ids=["H10", "H105"], level=[90]) ``` ```python theme={null} # Explore other models sf.plot(Y_df, forecasts_df, models=["SeasonalNaive"], unique_ids=["H10", "H105"], level=[90]) ``` ## Evaluate the model’s performance In previous steps, we’ve taken our historical data to predict the future. However, to assess its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, **Cross Validation** is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 2 days (n\_windows=2), forecasting every second day (step\_size=48). Depending on your computer, this step should take around 1 min. > **Tip** > > Setting `n_windows=1` mirrors a traditional train-test split with our > historical data serving as the training set and the last 48 hours > serving as the testing set. The `cross_validation` method from the `StatsForecast` class takes the following arguments. * `df`: training data frame * `h` (int): represents h steps into the future that are being forecasted. In this case, 24 hours ahead. * `step_size` (int): step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows`(int): number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} cv_df = sf.cross_validation( df=Y_df, h=24, step_size=24, n_windows=2 ) ``` The `cv_df` object is a new data frame that includes the following columns: * `unique_id`: series identifier * `ds`: datestamp or temporal index * `cutoff`: the last datestamp or temporal index for the `n_windows.` If `n_windows=1`, then one unique cutoff value, if `n_windows=2` then two unique cutoff values. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_df.head() ``` | | unique\_id | ds | cutoff | y | HoltWinters | CrostonClassic | SeasonalNaive | HistoricAverage | DynamicOptimizedTheta | | - | ---------- | --- | ------ | ----- | ----------- | -------------- | ------------- | --------------- | --------------------- | | 0 | H1 | 701 | 700 | 619.0 | 847.0 | 742.668748 | 691.0 | 661.675 | 612.767525 | | 1 | H1 | 702 | 700 | 565.0 | 820.0 | 742.668748 | 618.0 | 661.675 | 536.846296 | | 2 | H1 | 703 | 700 | 532.0 | 790.0 | 742.668748 | 563.0 | 661.675 | 497.824302 | | 3 | H1 | 704 | 700 | 495.0 | 784.0 | 742.668748 | 529.0 | 661.675 | 464.723235 | | 4 | H1 | 705 | 700 | 481.0 | 752.0 | 742.668748 | 504.0 | 661.675 | 440.972351 | Next, we will evaluate the performance of every model for every series using common error metrics like Mean Absolute Error (MAE) or Mean Square Error (MSE) Define a utility function to evaluate different error metrics for the cross validation data frame. First import the desired error metrics from `utilsforecast.losses`. Then define a utility function that takes a cross-validation data frame as a metric and returns an evaluation data frame with the average of the error metric for every unique id and fitted model and all cutoffs. ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import mse ``` ```python theme={null} def evaluate_cv(df, metric): models = df.columns.drop(['unique_id', 'ds', 'y', 'cutoff']).tolist() evals = evaluate(df, metrics=[metric], models=models) evals = evals.drop(columns=['metric']) evals['best_model'] = evals[models].idxmin(axis=1) return evals ``` > **Warning** > > You can also use Mean Average Percentage Error (MAPE), however for > granular forecasts, MAPE values are extremely [hard to > judge](https://blog.blueyonder.com/mean-absolute-percentage-error-mape-has-served-its-duty-and-should-now-retire/) > and not useful to assess forecasting quality. Create the data frame with the results of the evaluation of your cross-validation data frame using a Mean Squared Error metric. ```python theme={null} evaluation_df = evaluate_cv(cv_df, mse) evaluation_df.head() ``` | | unique\_id | cutoff | HoltWinters | CrostonClassic | SeasonalNaive | HistoricAverage | DynamicOptimizedTheta | best\_model | | - | ---------- | ------ | ------------- | -------------- | ------------- | --------------- | --------------------- | -------------- | | 0 | H1 | 700 | 38009.958333 | 28751.156365 | 1517.500000 | 23823.193125 | 1595.491947 | SeasonalNaive | | 1 | H10 | 700 | 2617.458333 | 1429.094499 | 89.375000 | 1833.382222 | 397.616596 | SeasonalNaive | | 2 | H100 | 700 | 104198.500000 | 80556.697053 | 8313.250000 | 71869.350278 | 28936.963781 | SeasonalNaive | | 3 | H101 | 700 | 19922.958333 | 6907.766752 | 13607.708333 | 9870.140347 | 107823.332195 | CrostonClassic | | 4 | H102 | 700 | 264394.791667 | 144611.993865 | 9206.166667 | 350475.255208 | 32221.379750 | SeasonalNaive | Create a summary table with a model column and the number of series where that model performs best. In this case, the Arima and Seasonal Naive are the best models for 10 series and the Theta model should be used for two. ```python theme={null} evaluation_df['best_model'].value_counts().to_frame().reset_index() ``` | | best\_model | count | | - | --------------------- | ----- | | 0 | SeasonalNaive | 11 | | 1 | DynamicOptimizedTheta | 8 | | 2 | CrostonClassic | 1 | You can further explore your results by plotting the unique\_ids where a specific model wins. ```python theme={null} seasonal_ids = evaluation_df.query('best_model == "SeasonalNaive"')['unique_id'] sf.plot(Y_df,forecasts_df, unique_ids=seasonal_ids, models=["SeasonalNaive","DynamicOptimizedTheta"]) ``` ## Select the best model for every unique series Define a utility function that takes your forecast’s data frame with the predictions and the evaluation data frame and returns a data frame with the best possible forecast for every unique\_id. ```python theme={null} def get_best_model_forecast(forecasts_df, evaluation_df): with_best = forecasts_df.merge(evaluation_df[['unique_id', 'best_model']]) res = with_best[['unique_id', 'ds']].copy() for suffix in ('', '-lo-90', '-hi-90'): res[f'best_model{suffix}'] = with_best.apply(lambda row: row[row['best_model'] + suffix], axis=1) return res ``` Create your production-ready data frame with the best forecast for every unique\_id. ```python theme={null} prod_forecasts_df = get_best_model_forecast(forecasts_df, evaluation_df) prod_forecasts_df.head() ``` | | unique\_id | ds | best\_model | best\_model-lo-90 | best\_model-hi-90 | | - | ---------- | --- | ----------- | ----------------- | ----------------- | | 0 | H1 | 749 | 635.000000 | 566.036734 | 703.963266 | | 1 | H1 | 749 | 592.701851 | 577.677280 | 611.652639 | | 2 | H1 | 750 | 572.000000 | 503.036734 | 640.963266 | | 3 | H1 | 750 | 525.589117 | 505.449755 | 546.621805 | | 4 | H1 | 751 | 532.000000 | 463.036734 | 600.963266 | Plot the results. ```python theme={null} sf.plot(Y_df, prod_forecasts_df, level=[90]) ``` # End to End Walkthrough with Polars Source: https://nixtlaverse.nixtla.io/statsforecast/docs/getting-started/getting_started_complete_polars.html > Model training, evaluation and selection for multiple time series ## Introducing Polars: A High-Performance DataFrame Library This document aims to highlight the recent integration of Polars, a robust and high-speed DataFrame library developed in Rust, into the functionality of StatsForecast. Polars, with its nimble and potent capabilities, has rapidly established a strong reputation within the Data Science community, further solidifying its position as a reliable tool for managing and manipulating substantial data sets. Available in languages including Rust, Python, Node.js, and R, Polars demonstrates a remarkable ability to handle sizable data sets with efficiency and speed that surpasses many other DataFrame libraries, such as Pandas. Polars’ open-source nature invites ongoing enhancements and contributions, augmenting its appeal within the data science arena. The most significant features of Polars that contribute to its rapid adoption are: 1. **Performance Efficiency**: Constructed using Rust, Polars exhibits an exemplary ability to manage substantial datasets with remarkable speed and minimal memory usage. 2. **Lazy Evaluation**: Polars operates on the principle of ‘lazy evaluation’, creating an optimized logical plan of operations for efficient execution, a feature that mirrors the functionality of Apache Spark. 3. **Parallel Execution**: Demonstrating the capability to exploit multi-core CPUs, Polars facilitates parallel execution of operations, substantially accelerating data processing tasks. > **Prerequisites** > > This Guide assumes basic familiarity with StatsForecast. For a minimal > example visit the [Quick Start](./getting_started_short.html) Follow this article for a step-by-step guide on building a production-ready forecasting pipeline for multiple time series. During this guide you will gain familiarity with the core `StatsForecast`class and some relevant methods like `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation.` We will use a classical benchmarking dataset from the M4 competition. The dataset includes time series from different domains like finance, economy and sales. In this example, we will use a subset of the Hourly dataset. We will model each time series individually. Forecasting at this level is also known as local forecasting. Therefore, you will train a series of models for every unique series and then select the best one. StatsForecast focuses on speed, simplicity, and scalability, which makes it ideal for this task. **Outline:** 1. Install packages. 2. Read the data. 3. Explore the data. 4. Train many models for every unique combination of time series. 5. Evaluate the model’s performance using cross-validation. 6. Select the best model for every unique time series. > **Not Covered in this guide** > > * Forecasting at scale using clusters on the cloud. > * [Forecast the M5 Dataset in > 5min](../experiments/ets_ray_m5.html) using Ray clusters. > * [Forecast the M5 Dataset in > 5min](../experiments/prophet_spark_m5.html) using Spark > clusters. > * Learn how to predict [1M series in less than > 30min](https://www.anyscale.com/blog/how-nixtla-uses-ray-to-accurately-predict-more-than-a-million-time-series). > * Training models on Multiple Seasonalities. > * Learn to use multiple seasonality in this [Electricity Load > forecasting](../tutorials/electricityloadforecasting.html) > tutorial. > * Using external regressors or exogenous variables > * Follow this tutorial to [include exogenous > variables](../how-to-guides/exogenous.html) like weather or > holidays or static variables like category or family. > * Comparing StatsForecast with other popular libraries. > * You can reproduce our benchmarks > [here](https://github.com/Nixtla/statsforecast/tree/main/experiments). ## Install libraries We assume you have StatsForecast already installed. Check this guide for instructions on [how to install StatsForecast](./installation.html). ## Read the data We will use polars to read the M4 Hourly data set stored in a parquet file for efficiency. You can use ordinary polars operations to read your data in other formats likes `.csv`. The input to StatsForecast is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestamp ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. This data set already satisfies the requirement. Depending on your internet connection, this step should take around 10 seconds. ```python theme={null} import polars as pl ``` ```python theme={null} Y_df = pl.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') Y_df.head() ``` | unique\_id | ds | y | | ---------- | --- | ----- | | str | i64 | f64 | | "H1" | 1 | 605.0 | | "H1" | 2 | 586.0 | | "H1" | 3 | 586.0 | | "H1" | 4 | 559.0 | | "H1" | 5 | 511.0 | This dataset contains 414 unique series with 900 observations on average. For this example and reproducibility’s sake, we will select only 10 unique IDs and keep only the last week. Depending on your processing infrastructure feel free to select more or less series. > **Note** > > Processing time is dependent on the available computing resources. > Running this example with the complete dataset takes around 10 minutes > in a c5d.24xlarge (96 cores) instance from AWS. ```python theme={null} uids = Y_df['unique_id'].unique(maintain_order=True)[:10] # Select 10 ids to make the example faster Y_df = Y_df.filter(pl.col('unique_id').is_in(uids)) Y_df = Y_df.group_by('unique_id').tail(7 * 24) #Select last 7 days of data to make example faster ``` ```text theme={null} /var/folders/cc/cylsfhls0hb_9wg0wh8tvpyh0000gn/T/ipykernel_32103/3020671980.py:2: DeprecationWarning: `is_in` with a collection of the same datatype is ambiguous and deprecated. Please use `implode` to return to previous behavior. See https://github.com/pola-rs/polars/issues/22149 for more information. Y_df = Y_df.filter(pl.col('unique_id').is_in(uids)) ``` ## Explore Data with the plot method Plot some series using the `plot` method from the `StatsForecast` class. This method prints 8 random series from the dataset and is useful for basic EDA. > **Note** > > The `StatsForecast.plot` method uses matplotlib as a default engine. > You can change to plotly by setting `engine="plotly"`. ```python theme={null} from statsforecast import StatsForecast ``` ```text theme={null} /Users/nasaul/nixtla/statsforecast/.venv/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm ``` ```python theme={null} StatsForecast.plot(Y_df) ``` ## Train multiple models for many series StatsForecast can train many models on many time series efficiently. Start by importing and instantiating the desired models. StatsForecast offers a wide variety of models grouped in the following categories: * **Auto Forecast:** Automatic forecasting tools search for the best parameters and select the best possible model for a series of time series. These tools are useful for large collections of univariate time series. Includes automatic versions of: Arima, ETS, Theta, CES. * **Exponential Smoothing:** Uses a weighted average of all past observations where the weights decrease exponentially into the past. Suitable for data with no clear trend or seasonality. Examples: SES, Holt’s Winters, SSO. * **Benchmark models:** classical models for establishing baselines. Examples: Mean, Naive, Random Walk * **Intermittent or Sparse models:** suited for series with very few non-zero observations. Examples: CROSTON, ADIDA, IMAPA * **Multiple Seasonalities:** suited for signals with more than one clear seasonality. Useful for low-frequency data like electricity and logs. Examples: MSTL. * **Theta Models:** fit two theta lines to a deseasonalized time series, using different techniques to obtain and combine the two theta lines to produce the final forecasts. Examples: Theta, DynamicTheta Here you can check the complete list of [models](../../src/core/models_intro.html). For this example we will use: * `AutoARIMA`: Automatically selects the best ARIMA (AutoRegressive Integrated Moving Average) model using an information criterion. Ref: `AutoARIMA`. * `HoltWinters`: triple exponential smoothing, Holt-Winters’ method is an extension of exponential smoothing for series that contain both trend and seasonality. Ref: `HoltWinters` * `SeasonalNaive`: Memory Efficient Seasonal Naive predictions. Ref: `SeasonalNaive` * `HistoricAverage`: arithmetic mean. Ref: `HistoricAverage`. * `DynamicOptimizedTheta`: The theta family of models has been shown to perform well in various datasets such as M3. Models the deseasonalized time series. Ref: `DynamicOptimizedTheta`. Import and instantiate the models. Setting the `season_length` argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/)) by the master, Rob Hyndmann, can be useful. ```python theme={null} from statsforecast.models import ( HoltWinters, CrostonClassic as Croston, HistoricAverage, DynamicOptimizedTheta as DOT, SeasonalNaive ) ``` ```python theme={null} # Create a list of models and instantiation parameters models = [ HoltWinters(), Croston(), SeasonalNaive(season_length=24), HistoricAverage(), DOT(season_length=24) ] ``` We fit the models by instantiating a new `StatsForecast` object with the following parameters: * `models`: a list of models. Select the models you want from [models](../../src/core/models_intro.html) and import them. * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) This is also available with Polars. * `n_jobs`: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model`: a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} # Instantiate StatsForecast class as sf sf = StatsForecast( models=models, freq=1, n_jobs=-1, fallback_model=SeasonalNaive(season_length=7), verbose=True, ) ``` The `forecast` method takes two arguments: forecasts next `h` (horizon) and `level`. * `h` (int): represents the forecast h steps into the future. In this case, 12 months ahead. * `level` (list of floats): this optional parameter is used for probabilistic forecasting. Set the `level` (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like ARIMA and Theta) > **Note** > > The `forecast` method is compatible with distributed clusters, so it > does not store any model parameters. If you want to store parameters > for every model you can use the `fit` and `predict` methods. However, > those methods are not defined for distributed engines like Spark, Ray > or Dask. ```python theme={null} forecasts_df = sf.forecast(df=Y_df, h=48, level=[90]) forecasts_df.head() ``` ```text theme={null} Forecast: 100%|██████████| 10/10 [Elapsed: 00:03] ``` | unique\_id | ds | HoltWinters | HoltWinters-lo-90 | HoltWinters-hi-90 | CrostonClassic | CrostonClassic-lo-90 | CrostonClassic-hi-90 | SeasonalNaive | SeasonalNaive-lo-90 | SeasonalNaive-hi-90 | HistoricAverage | HistoricAverage-lo-90 | HistoricAverage-hi-90 | DynamicOptimizedTheta | DynamicOptimizedTheta-lo-90 | DynamicOptimizedTheta-hi-90 | | ---------- | --- | ----------- | ----------------- | ----------------- | -------------- | -------------------- | -------------------- | ------------- | ------------------- | ------------------- | --------------- | --------------------- | --------------------- | --------------------- | --------------------------- | --------------------------- | | str | i64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | | "H1" | 749 | 829.0 | 422.549268 | 1235.450732 | 829.0 | 422.549268 | 1235.450732 | 635.0 | 566.036734 | 703.963266 | 660.982143 | 398.037761 | 923.926524 | 592.701851 | 577.67728 | 611.652639 | | "H1" | 750 | 807.0 | 400.549268 | 1213.450732 | 807.0 | 400.549268 | 1213.450732 | 572.0 | 503.036734 | 640.963266 | 660.982143 | 398.037761 | 923.926524 | 525.589117 | 505.449755 | 546.621805 | | "H1" | 751 | 785.0 | 378.549268 | 1191.450732 | 785.0 | 378.549268 | 1191.450732 | 532.0 | 463.036734 | 600.963266 | 660.982143 | 398.037761 | 923.926524 | 489.251814 | 462.072871 | 512.424116 | | "H1" | 752 | 756.0 | 349.549268 | 1162.450732 | 756.0 | 349.549268 | 1162.450732 | 493.0 | 424.036734 | 561.963266 | 660.982143 | 398.037761 | 923.926524 | 456.195032 | 430.554302 | 478.260963 | | "H1" | 753 | 719.0 | 312.549268 | 1125.450732 | 719.0 | 312.549268 | 1125.450732 | 477.0 | 408.036734 | 545.963266 | 660.982143 | 398.037761 | 923.926524 | 436.290514 | 411.051232 | 461.815932 | Plot the results of 8 random series using the `StatsForecast.plot` method. ```python theme={null} sf.plot(Y_df,forecasts_df) ``` The `StatsForecast.plot` allows for further customization. For example, plot the results of the different models and unique ids. ```python theme={null} # Plot to unique_ids and some selected models sf.plot(Y_df, forecasts_df, models=["HoltWinters","DynamicOptimizedTheta"], unique_ids=["H10", "H105"], level=[90]) ``` ```python theme={null} # Explore other models sf.plot(Y_df, forecasts_df, models=["SeasonalNaive"], unique_ids=["H10", "H105"], level=[90]) ``` ## Evaluate the model’s performance In previous steps, we’ve taken our historical data to predict the future. However, to assess its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, **Cross Validation** is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 2 days (n\_windows=2), forecasting every second day (step\_size=48). Depending on your computer, this step should take around 1 min. > **Tip** > > Setting `n_windows=1` mirrors a traditional train-test split with our > historical data serving as the training set and the last 48 hours > serving as the testing set. The `cross_validation` method from the `StatsForecast` class takes the following arguments. * `df`: training data frame * `h` (int): represents h steps into the future that are being forecasted. In this case, 24 hours ahead. * `step_size` (int): step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows`(int): number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} cv_df = sf.cross_validation( df=Y_df, h=24, step_size=24, n_windows=2 ) ``` ```text theme={null} Cross Validation Time Series 1: 100%|██████████| 2/2 [00:00<00:00, 2.02it/s] Cross Validation Time Series 1: 100%|██████████| 2/2 [00:00<00:00, 2.02it/s] Cross Validation Time Series 1: 100%|██████████| 2/2 [00:00<00:00, 2.26it/s] Cross Validation Time Series 1: 100%|██████████| 2/2 [00:00<00:00, 2.76it/s] Cross Validation Time Series 1: 100%|██████████| 2/2 [00:00<00:00, 2.25it/s] Cross Validation Time Series 1: 100%|██████████| 2/2 [00:00<00:00, 2.78it/s] Cross Validation Time Series 1: 100%|██████████| 2/2 [00:00<00:00, 3.20it/s] Cross Validation Time Series 1: 100%|██████████| 2/2 [00:00<00:00, 4.14it/s] Cross Validation Time Series 1: 100%|██████████| 2/2 [00:00<00:00, 4.11it/s] Cross Validation Time Series 1: 100%|██████████| 2/2 [00:00<00:00, 4.75it/s] ``` The `cv_df` object is a new data frame that includes the following columns: * `unique_id`: series identifier * `ds`: datestamp or temporal index * `cutoff`: the last datestamp or temporal index for the `n_windows.` If `n_windows=1`, then one unique cutoff value, if `n_windows=2` then two unique cutoff values. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_df.head() ``` | unique\_id | ds | cutoff | y | HoltWinters | CrostonClassic | SeasonalNaive | HistoricAverage | DynamicOptimizedTheta | | ---------- | --- | ------ | ----- | ----------- | -------------- | ------------- | --------------- | --------------------- | | str | i64 | i64 | f64 | f64 | f64 | f64 | f64 | f64 | | "H1" | 701 | 700 | 619.0 | 847.0 | 742.668748 | 691.0 | 661.675 | 612.767525 | | "H1" | 702 | 700 | 565.0 | 820.0 | 742.668748 | 618.0 | 661.675 | 536.846296 | | "H1" | 703 | 700 | 532.0 | 790.0 | 742.668748 | 563.0 | 661.675 | 497.824302 | | "H1" | 704 | 700 | 495.0 | 784.0 | 742.668748 | 529.0 | 661.675 | 464.723235 | | "H1" | 705 | 700 | 481.0 | 752.0 | 742.668748 | 504.0 | 661.675 | 440.972351 | Next, we will evaluate the performance of every model for every series using common error metrics like Mean Absolute Error (MAE) or Mean Square Error (MSE) Define a utility function to evaluate different error metrics for the cross validation data frame. First import the desired error metrics from `utilsforecast.losses`. Then define a utility function that takes a cross-validation data frame as a metric and returns an evaluation data frame with the average of the error metric for every unique id and fitted model and all cutoffs. ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import mse ``` ```python theme={null} def evaluate_cv(df, metric): models = [c for c in df.columns if c not in ('unique_id', 'ds', 'cutoff', 'y')] evals = evaluate(df, metrics=[metric], models=models) evals = evals.drop('metric') pos2model = dict(enumerate(models)) return evals.with_columns( best_model=pl.concat_list(models).list.arg_min().replace_strict(pos2model) ) ``` > **Warning** > > You can also use Mean Average Percentage Error (MAPE), however for > granular forecasts, MAPE values are extremely [hard to > judge](https://blog.blueyonder.com/mean-absolute-percentage-error-mape-has-served-its-duty-and-should-now-retire/) > and not useful to assess forecasting quality. Create the data frame with the results of the evaluation of your cross-validation data frame using a Mean Squared Error metric. ```python theme={null} evaluation_df = evaluate_cv(cv_df, mse) evaluation_df.head() ``` | unique\_id | cutoff | HoltWinters | CrostonClassic | SeasonalNaive | HistoricAverage | DynamicOptimizedTheta | best\_model | | ---------- | ------ | ------------- | -------------- | ------------- | --------------- | --------------------- | ---------------- | | str | i64 | f64 | f64 | f64 | f64 | f64 | str | | "H1" | 700 | 38009.958333 | 28751.156365 | 1517.5 | 23823.193125 | 1595.491947 | "SeasonalNaive" | | "H10" | 700 | 2617.458333 | 1429.094499 | 89.375 | 1833.382222 | 397.616596 | "SeasonalNaive" | | "H100" | 700 | 104198.5 | 80556.697053 | 8313.25 | 71869.350278 | 28936.963781 | "SeasonalNaive" | | "H101" | 700 | 19922.958333 | 6907.766752 | 13607.708333 | 9870.140347 | 107823.332195 | "CrostonClassic" | | "H102" | 700 | 264394.791667 | 144611.993865 | 9206.166667 | 350475.255208 | 32221.37975 | "SeasonalNaive" | Create a summary table with a model column and the number of series where that model performs best. In this case, the Arima and Seasonal Naive are the best models for 10 series and the Theta model should be used for two. ```python theme={null} evaluation_df['best_model'].value_counts() ``` | best\_model | count | | ----------------------- | ----- | | str | u32 | | "SeasonalNaive" | 11 | | "CrostonClassic" | 1 | | "DynamicOptimizedTheta" | 8 | You can further explore your results by plotting the unique\_ids where a specific model wins. ```python theme={null} seasonal_ids = evaluation_df.filter(pl.col('best_model') == 'SeasonalNaive')['unique_id'] sf.plot(Y_df,forecasts_df, unique_ids=seasonal_ids, models=["SeasonalNaive","DynamicOptimizedTheta"]) ``` ## Select the best model for every unique series Define a utility function that takes your forecast’s data frame with the predictions and the evaluation data frame and returns a data frame with the best possible forecast for every unique\_id. ```python theme={null} def get_best_model_forecast(forecasts_df, evaluation_df): models = { c.replace('-lo-90', '').replace('-hi-90', '') for c in forecasts_df.columns if c not in ('unique_id', 'ds') } model2pos = {m: i for i, m in enumerate(models)} with_best = forecasts_df.join(evaluation_df[['unique_id', 'best_model']], on='unique_id') return with_best.select( 'unique_id', 'ds', *[ ( pl.concat_list([f'{m}{suffix}' for m in models]) .list.get(pl.col('best_model').replace_strict(model2pos)) .alias(f'best_model{suffix}') ) for suffix in ('', '-lo-90', '-hi-90') ] ) ``` Create your production-ready data frame with the best forecast for every unique\_id. ```python theme={null} prod_forecasts_df = get_best_model_forecast(forecasts_df, evaluation_df) prod_forecasts_df.head() ``` | unique\_id | ds | best\_model | best\_model-lo-90 | best\_model-hi-90 | | ---------- | --- | ----------- | ----------------- | ----------------- | | str | i64 | f64 | f64 | f64 | | "H1" | 749 | 635.0 | 566.036734 | 703.963266 | | "H1" | 749 | 592.701851 | 577.67728 | 611.652639 | | "H1" | 750 | 572.0 | 503.036734 | 640.963266 | | "H1" | 750 | 525.589117 | 505.449755 | 546.621805 | | "H1" | 751 | 532.0 | 463.036734 | 600.963266 | Plot the results. ```python theme={null} sf.plot(Y_df, prod_forecasts_df, level=[90]) ``` # Quick Start | StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/docs/getting-started/getting_started_short.html > Minimal Example of StatsForecast `StatsForecast` follows the sklearn model API. For this minimal example, you will create an instance of the StatsForecast class and then call its `fit` and `predict` methods. We recommend this option if speed is not paramount and you want to explore the fitted values and parameters. > **Tip** > > If you want to forecast many series, we recommend using the `forecast` > method. Check this [Getting Started with multiple time > series](./getting_started_complete.html) guide. The input to StatsForecast is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. As an example, let’s look at the US Air Passengers dataset. This time series consists of monthly totals of a US airline passengers from 1949 to 1960. The CSV is available [here](https://www.kaggle.com/datasets/chirag19/air-passengers). We assume you have StatsForecast already installed. Check this guide for instructions on [how to install StatsForecast](./installation.html). First, we’ll import the data: ```python theme={null} # uncomment the following line to install the library # %pip install statsforecast ``` ```python theme={null} import pandas as pd ``` ```python theme={null} df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/air-passengers.csv', parse_dates=['ds']) df.head() ``` | | unique\_id | ds | y | | - | ------------- | ---------- | --- | | 0 | AirPassengers | 1949-01-01 | 112 | | 1 | AirPassengers | 1949-02-01 | 118 | | 2 | AirPassengers | 1949-03-01 | 132 | | 3 | AirPassengers | 1949-04-01 | 129 | | 4 | AirPassengers | 1949-05-01 | 121 | We fit the model by instantiating a new `StatsForecast` object with its [two required parameters](../../src/core/models.html): \* `models`: a list of models. Select the models you want from [models](../../src/core/models.html) and import them. For this example, we will use a `AutoARIMA` model. We set `season_length` to 12 because we expect seasonal effects every 12 months. (See: [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/)) * `freq`: a string indicating the frequency of the data. (See [pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoARIMA ``` ```python theme={null} sf = StatsForecast( models=[AutoARIMA(season_length = 12)], freq='MS', ) sf.fit(df) ``` ```text theme={null} StatsForecast(models=[AutoARIMA]) ``` The `predict` method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h` (int): represents the forecast h steps into the future. In this case, 12 months ahead. * `level` (list of floats): this optional parameter is used for probabilistic forecasting. Set the `level` (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. ```python theme={null} forecast_df = sf.predict(h=12, level=[90]) forecast_df.tail() ``` | | unique\_id | ds | AutoARIMA | AutoARIMA-lo-90 | AutoARIMA-hi-90 | | -- | ------------- | ---------- | ---------- | --------------- | --------------- | | 7 | AirPassengers | 1961-08-01 | 633.236389 | 590.009033 | 676.463745 | | 8 | AirPassengers | 1961-09-01 | 535.236389 | 489.558899 | 580.913940 | | 9 | AirPassengers | 1961-10-01 | 488.236389 | 440.233795 | 536.239014 | | 10 | AirPassengers | 1961-11-01 | 417.236389 | 367.016205 | 467.456604 | | 11 | AirPassengers | 1961-12-01 | 459.236389 | 406.892456 | 511.580322 | You can plot the forecast by calling the `StatsForecast.plot` method and passing in your forecast dataframe. ```python theme={null} sf.plot(df, forecast_df, level=[90]) ``` > **Next Steps** > > * Build and end-to-end forecasting pipeline following best practices > in [End to End Walkthrough](./getting_started_complete.html) > * [Forecast millions of > series](../experiments/prophet_spark_m5.html) in a scalable > cluster in the cloud using Spark and Nixtla > * [Detect anomalies](../tutorials/anomalydetection.html) in your > past observations # Install | StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/docs/getting-started/installation.html > Install StatsForecast with pip or conda You can install the *released version* of `StatsForecast` from the [Python package index](https://pypi.org) with: ```shell theme={null} pip install statsforecast ``` or ```shell theme={null} conda install -c conda-forge statsforecast ``` > **Warning** > > We are constantly updating StatsForecast, so we suggest fixing the > version to avoid issues. `pip install statsforecast=="1.0.0"` > **Tip** > > We recommend installing your libraries inside a python virtual or > [conda > environment](https://docs.conda.io/projects/conda/en/latest/user-guide/install/macos.html). #### Extras The following features can also be installed by specifying the extra inside the install command, e.g. `pip install 'statsforecast[extra1,extra2]'` * **polars**: provide polars dataframes to StatsForecast. * **plotly**: use `StatsForecast.plot` with the plotly backend. * **dask**: perform distributed forecasting with dask. * **spark**: perform distributed forecasting with spark. * **ray**: perform distributed forecasting with ray. #### Development version If you want to try out a new feature that hasn’t made it into a release yet you have the following options: * Install from our nightly wheels: `pip install --extra-index-url=http://nixtla-packages.s3-website.us-east-2.amazonaws.com --trusted-host nixtla-packages.s3-website.us-east-2.amazonaws.com statsforecast` * Install from github: `pip install git+https://github.com/Nixtla/statsforecast`. This requires that you have a C++ compiler installed, so we encourage you to try the previous option first. # Automatic Time Series Forecasting Source: https://nixtlaverse.nixtla.io/statsforecast/docs/how-to-guides/automatic_forecasting.html > How to do automatic forecasting using `AutoARIMA`, `AutoETS`, > `AutoCES` and `AutoTheta`. > **Tip** > > Automatic forecasts of large numbers of univariate time series are > often needed. It is common to have multiple product lines or skus that > need forecasting. In these circumstances, an automatic forecasting > algorithm is an essential tool. Automatic forecasting algorithms must > determine an appropriate time series model, estimate the parameters > and compute the forecasts. They must be robust to unusual time series > patterns, and applicable to large numbers of series without user > intervention. ## 1. Install statsforecast and load data Use pip to install statsforecast and load Air Passengers dataset as an example ```python theme={null} # uncomment the following line to install the library # %pip install statsforecast ``` ```python theme={null} from statsforecast.utils import AirPassengersDF ``` ```python theme={null} Y_df = AirPassengersDF ``` ## 2. Import StatsForecast and models Import the core StatsForecast class and the models you want to use ```python theme={null} import pandas as pd from statsforecast import StatsForecast from statsforecast.models import AutoARIMA, AutoETS, AutoTheta, AutoCES ``` ## 3. Instantiate the class Instantiate the StatsForecast class with the appropriate parameters ```python theme={null} season_length = 12 # Define season length as 12 months for monthly data horizon = 1 # Forecast horizon is set to 1 month # Define a list of models for forecasting models = [ AutoARIMA(season_length=season_length), # ARIMA model with automatic order selection and seasonal component AutoETS(season_length=season_length), # ETS model with automatic error, trend, and seasonal component AutoTheta(season_length=season_length), # Theta model with automatic seasonality detection AutoCES(season_length=season_length), # CES model with automatic seasonality detection ] # Instantiate StatsForecast class with models, data frequency ('M' for monthly), # and parallel computation on all CPU cores (n_jobs=-1) sf = StatsForecast( models=models, # models for forecasting freq=pd.offsets.MonthEnd(), # frequency of the timestamps n_jobs=1 # number of jobs to run in parallel, -1 means using all processors ) ``` ## 4. a) Forecast with forecast method The `.forecast` method is faster for distributed computing and does not save the fittted models ```python theme={null} # Generate forecasts for the specified horizon using the sf object Y_hat_df = sf.forecast(df=Y_df, h=horizon) # forecast data # Display the first few rows of the forecast DataFrame Y_hat_df.head() # preview of forecasted data ``` | | unique\_id | ds | AutoARIMA | AutoETS | AutoTheta | CES | | - | ---------- | ---------- | ---------- | ---------- | ---------- | --------- | | 0 | 1.0 | 1961-01-31 | 444.309575 | 442.357169 | 442.940797 | 453.03418 | ## 4. b) Forecast with fit and predict The `.fit` method saves the fitted models ```python theme={null} sf.fit(df=Y_df) # Fit the models to the data using the fit method of the StatsForecast object sf.fitted_ # Access fitted models from the StatsForecast object Y_hat_df = sf.predict(h=horizon) # Predict or forecast 'horizon' steps ahead using the predict method Y_hat_df.head() # Preview the first few rows of the forecasted data ``` | | unique\_id | ds | AutoARIMA | AutoETS | AutoTheta | CES | | - | ---------- | ---------- | ---------- | ---------- | ---------- | --------- | | 0 | 1.0 | 1961-01-31 | 444.309575 | 442.357169 | 442.940797 | 453.03418 | ## References [Hyndman, RJ and Khandakar, Y (2008) “Automatic time series forecasting: The forecast package for R”, Journal of Statistical Software, 26(3).](https://www.jstatsoft.org/article/view/v027i03) # Exogenous Regressors Source: https://nixtlaverse.nixtla.io/statsforecast/docs/how-to-guides/exogenous.html > In this notebook, we’ll incorporate exogenous regressors to a > StatsForecast model. > **Prerequisites** > > This tutorial assumes basic familiarity with StatsForecast. For a > minimal example visit the [Quick > Start](../getting-started/getting_started_short.html) ## Introduction **Exogenous regressors** are variables that can affect the values of a time series. They may not be directly related to the variable that is being forecasted, but they can still have an impact on it. Examples of exogenous regressors are weather data, economic indicators, or promotional sales. They are typically collected from external sources and by incorporating them into a forecasting model, they can improve the accuracy of our predictions. By the end of this tutorial, you’ll have a good understanding of how to incorporate exogenous regressors into [StatsForecast](../../src/core/models.html)’s models. Furthermore, you’ll see how to evaluate their performance and decide whether or not they can help enhance the forecast. **Outline** 1. Install libraries 2. Load and explore the data 3. Split train/test set 4. Add exogenous regressors 5. Create future exogenous regressors 6. Train model 7. Evaluate results > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Install libraries We assume that you have StatsForecast already installed. If not, check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html). ```python theme={null} # uncomment the following line to install the library # %pip install statsforecast ``` ```python theme={null} import pandas as pd ``` ## Load and explore the data In this example, we’ll use a single time series from the [M5 Competition](https://www.sciencedirect.com/science/article/pii/S0169207021001187#:~:text=The%20objective%20of%20the%20M5,the%20uncertainty%20around%20these%20forecasts.) dataset. This series represents the daily sales of a product in a Walmart store. The product-store combination that we’ll use in this notebook has `unique_id = FOODS_3_586_CA_3`. This time series was chosen because it is not intermittent and has exogenous regressors that will be useful for forecasting. We’ll load the following dataframes: * `Y_ts`: (pandas DataFrame) The target time series with columns \[`unique_id`, `ds`, `y`]. * `X_ts`: (pandas DataFrame) Exogenous time series with columns \[`unique_id`, `ds`, exogenous regressors]. ```python theme={null} base_url = 'https://datasets-nixtla.s3.amazonaws.com' filters = [('unique_id', '=', 'FOODS_3_586_CA_3')] Y_ts = pd.read_parquet(f'{base_url}/m5_y.parquet', filters=filters) X_ts = pd.read_parquet(f'{base_url}/m5_x.parquet', filters=filters) ``` We can plot the sales of this product-store combination with the `statsforecast.plot` method from the [StatsForecast](../../src/core/core.html#statsforecast) class. This method has multiple parameters, and the required ones to generate the plots in this notebook are explained below. * `df`: A pandas dataframe with columns \[`unique_id`, `ds`, `y`]. * `forecasts_df`: A pandas dataframe with columns \[`unique_id`, `ds`] and models. * `engine`: str = `matplotlib`. It can also be `plotly`. `plotly` generates interactive plots, while `matplotlib` generates static plots. ```python theme={null} from statsforecast import StatsForecast ``` ```python theme={null} StatsForecast.plot(Y_ts) ``` The M5 Competition included several exogenous regressors. Here we’ll use the following two. * `sell_price`: The price of the product for the given store. The price is provided per week. * `snap_CA`: A binary variable indicating whether the store allows SNAP purchases (1 if yes, 0 otherwise). SNAP stands for Supplement Nutrition Assitance Program, and it gives individuals and families money to help them purchase food products. ```python theme={null} X_ts = X_ts[['unique_id', 'ds', 'sell_price', 'snap_CA']] X_ts.head() ``` | | unique\_id | ds | sell\_price | snap\_CA | | - | -------------------- | ---------- | ----------- | -------- | | 0 | FOODS\_3\_586\_CA\_3 | 2011-01-29 | 1.48 | 0 | | 1 | FOODS\_3\_586\_CA\_3 | 2011-01-30 | 1.48 | 0 | | 2 | FOODS\_3\_586\_CA\_3 | 2011-01-31 | 1.48 | 0 | | 3 | FOODS\_3\_586\_CA\_3 | 2011-02-01 | 1.48 | 1 | | 4 | FOODS\_3\_586\_CA\_3 | 2011-02-02 | 1.48 | 1 | Here the `unique_id` is a category, but for the exogenous regressors it needs to be a string. ```python theme={null} X_ts['unique_id'] = X_ts.unique_id.astype(str) ``` We can plot the exogenous regressors using `plotly`. We could use `statsforecast.plot`, but then one of the regressors must be renamed `y`, and the name must be changed back to the original before generating the forecast. ```python theme={null} StatsForecast.plot(Y_ts, X_ts, max_insample_length=0) ``` From this plot, we can conclude that price has increased twice and that SNAP occurs at regular intervals. ## Split train/test set In the M5 Competition, participants had to forecast sales for the last 28 days in the dataset. We’ll use the same forecast horizon and create the train and test sets accordingly. ```python theme={null} # Extract dates for train and test set dates = Y_ts['ds'].unique() dtrain = dates[:-28] dtest = dates[-28:] Y_train = Y_ts.query('ds in @dtrain') Y_test = Y_ts.query('ds in @dtest') X_train = X_ts.query('ds in @dtrain') X_test = X_ts.query('ds in @dtest') ``` ## Add exogenous regressors The exogenous regressors need to be place after the target variable `y`. ```python theme={null} train = Y_train.merge(X_ts, how = 'left', on = ['unique_id', 'ds']) train.head() ``` | | unique\_id | ds | y | sell\_price | snap\_CA | | - | -------------------- | ---------- | ---- | ----------- | -------- | | 0 | FOODS\_3\_586\_CA\_3 | 2011-01-29 | 56.0 | 1.48 | 0 | | 1 | FOODS\_3\_586\_CA\_3 | 2011-01-30 | 55.0 | 1.48 | 0 | | 2 | FOODS\_3\_586\_CA\_3 | 2011-01-31 | 45.0 | 1.48 | 0 | | 3 | FOODS\_3\_586\_CA\_3 | 2011-02-01 | 57.0 | 1.48 | 1 | | 4 | FOODS\_3\_586\_CA\_3 | 2011-02-02 | 54.0 | 1.48 | 1 | ## Create future exogenous regressors We need to include the future values of the exogenous regressors so that we can produce the forecasts. Notice that we already have this information in `X_test`. ```python theme={null} X_test.head() ``` | | unique\_id | ds | sell\_price | snap\_CA | | ---- | -------------------- | ---------- | ----------- | -------- | | 1941 | FOODS\_3\_586\_CA\_3 | 2016-05-23 | 1.68 | 0 | | 1942 | FOODS\_3\_586\_CA\_3 | 2016-05-24 | 1.68 | 0 | | 1943 | FOODS\_3\_586\_CA\_3 | 2016-05-25 | 1.68 | 0 | | 1944 | FOODS\_3\_586\_CA\_3 | 2016-05-26 | 1.68 | 0 | | 1945 | FOODS\_3\_586\_CA\_3 | 2016-05-27 | 1.68 | 0 | > **Important** > > If the future values of the exogenous regressors are not available, > then they must be forecasted or the regressors need to be eliminated > from the model. Without them, it is not possible to generate the > forecast. ## Train model To generate the forecast, we’ll use [AutoARIMA](https://nixtlaverse.nixtla.io/statsforecast/docs/models/AutoARIMA), which is one of the models available in StatsForecast that allows exogenous regressors. To use this model, we first need to import it from `statsforecast.models` and then we need to instantiate it. Given that we’re working with daily data, we need to set `season_length = 7`. ```python theme={null} from statsforecast.models import AutoARIMA ``` ```python theme={null} # Create a list with the model and its instantiation parameters models = [AutoARIMA(season_length=7)] ``` Next, we need to instantiate a new StatsForecast object, which has the following parameters. * `df`: The dataframe with the training data. * `models`: The list of models defined in the previous step. * `freq`: A string indicating the frequency of the data. See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). * `n_jobs`: An integer that indicates the number of jobs used in parallel processing. Use -1 to select all cores. ```python theme={null} sf = StatsForecast( models=models, freq='D', n_jobs=1, ) ``` Now we’re ready to generate the forecast. To do this, we’ll use the `forecast` method, which takes the following arguments. * `h`: An integer that represents the forecast horizon. In this case, we’ll forecast the next 28 days. * `X_df`: A pandas dataframe with the future values of the exogenous regressors. * `level`: A list of floats with the confidence levels of the prediction intervals. For example, `level=[95]` means that the range of values should include the actual future value with probability 95%. ```python theme={null} horizon = 28 level = [95] fcst = sf.forecast(df=train, h=horizon, X_df=X_test, level=level) fcst.head() ``` | | unique\_id | ds | AutoARIMA | AutoARIMA-lo-95 | AutoARIMA-hi-95 | | - | -------------------- | ---------- | --------- | --------------- | --------------- | | 0 | FOODS\_3\_586\_CA\_3 | 2016-05-23 | 72.956276 | 44.109070 | 101.803482 | | 1 | FOODS\_3\_586\_CA\_3 | 2016-05-24 | 71.138611 | 40.761467 | 101.515747 | | 2 | FOODS\_3\_586\_CA\_3 | 2016-05-25 | 68.140945 | 37.550083 | 98.731804 | | 3 | FOODS\_3\_586\_CA\_3 | 2016-05-26 | 65.485588 | 34.841637 | 96.129539 | | 4 | FOODS\_3\_586\_CA\_3 | 2016-05-27 | 64.961441 | 34.291973 | 95.630905 | We can plot the forecasts with the `statsforecast.plot` method described above. ```python theme={null} StatsForecast.plot(Y_ts, fcst, max_insample_length=28*2) ``` ## Evaluate results We’ll merge the test set and the forecast to evaluate the accuracy using the [mean absolute error](https://en.wikipedia.org/wiki/Mean_absolute_error) (MAE). ```python theme={null} res = Y_test.merge(fcst, how='left', on=['unique_id', 'ds']) res.head() ``` | | unique\_id | ds | y | AutoARIMA | AutoARIMA-lo-95 | AutoARIMA-hi-95 | | - | -------------------- | ---------- | ---- | --------- | --------------- | --------------- | | 0 | FOODS\_3\_586\_CA\_3 | 2016-05-23 | 66.0 | 72.956276 | 44.109070 | 101.803482 | | 1 | FOODS\_3\_586\_CA\_3 | 2016-05-24 | 62.0 | 71.138611 | 40.761467 | 101.515747 | | 2 | FOODS\_3\_586\_CA\_3 | 2016-05-25 | 40.0 | 68.140945 | 37.550083 | 98.731804 | | 3 | FOODS\_3\_586\_CA\_3 | 2016-05-26 | 72.0 | 65.485588 | 34.841637 | 96.129539 | | 4 | FOODS\_3\_586\_CA\_3 | 2016-05-27 | 69.0 | 64.961441 | 34.291973 | 95.630905 | ```python theme={null} mae = abs(res['y']-res['AutoARIMA']).mean() print('The MAE with exogenous regressors is '+str(round(mae,2))) ``` ```text theme={null} The MAE with exogenous regressors is 11.42 ``` To check whether the exogenous regressors were useful or not, we need to generate the forecast again, now without them. To do this, we simply pass the dataframe without exogenous variables to the `forecast` method. Notice that the data only includes `unique_id`, `ds`, and `y`. The `forecast` method no longer requires the future values of the exogenous regressors `X_df`. ```python theme={null} # univariate model fcst_u = sf.forecast(df=train[['unique_id', 'ds', 'y']], h=28) res_u = Y_test.merge(fcst_u, how='left', on=['unique_id', 'ds']) mae_u = abs(res_u['y']-res_u['AutoARIMA']).mean() ``` ```python theme={null} print('The MAE without exogenous regressors is '+str(round(mae_u,2))) ``` ```text theme={null} The MAE without exogenous regressors is 12.18 ``` Hence, we can conclude that using `sell_price` and `snap_CA` as external regressors helped improve the forecast. # Generating features Source: https://nixtlaverse.nixtla.io/statsforecast/docs/how-to-guides/generating_features.html > Leverage StatsForecast models to create features Some models create internal representations of the series that can be useful for other models to use as inputs. One example is the `MSTL` model, which decomposes the series into trend and seasonal components. This guide shows you how to use the `mstl_decomposition` function to extract those features for training and then use their future values for inference. ```python theme={null} from functools import partial import pandas as pd import statsforecast from statsforecast import StatsForecast from statsforecast.feature_engineering import mstl_decomposition from statsforecast.models import ARIMA, MSTL from utilsforecast.evaluation import evaluate from utilsforecast.losses import smape, mase ``` ```python theme={null} df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') uids = df['unique_id'].unique()[:10] df = df[df['unique_id'].isin(uids)] df.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | Suppose that you want to use an ARIMA model to forecast your series but you want to incorporate the trend and seasonal components from the MSTL model as external regressors. You can define the MSTL model to use and then provide it to the mstl\_decomposition function. ```python theme={null} freq = 1 season_length = 24 horizon = 2 * season_length valid = df.groupby('unique_id').tail(horizon) train = df.drop(valid.index) model = MSTL(season_length=24) transformed_df, X_df = mstl_decomposition(train, model=model, freq=freq, h=horizon) ``` This generates the dataframe that we should use for training (with the trend and seasonal columns added), as well as the dataframe we should use to forecast. ```python theme={null} transformed_df.head() ``` | | unique\_id | ds | y | trend | seasonal | | - | ---------- | -- | ----- | ---------- | ---------- | | 0 | H1 | 1 | 605.0 | 502.872910 | 131.419934 | | 1 | H1 | 2 | 586.0 | 507.873456 | 93.100015 | | 2 | H1 | 3 | 586.0 | 512.822533 | 82.155386 | | 3 | H1 | 4 | 559.0 | 517.717481 | 42.412749 | | 4 | H1 | 5 | 511.0 | 522.555849 | -11.401890 | ```python theme={null} X_df.head() ``` | | unique\_id | ds | trend | seasonal | | - | ---------- | --- | ---------- | ----------- | | 0 | H1 | 701 | 643.801348 | -29.189627 | | 1 | H1 | 702 | 644.328207 | -99.680432 | | 2 | H1 | 703 | 644.749693 | -141.169014 | | 3 | H1 | 704 | 645.086883 | -173.325625 | | 4 | H1 | 705 | 645.356634 | -195.862530 | We can now train our ARIMA models and compute our forecasts. ```python theme={null} sf = StatsForecast( models=[ARIMA(order=(1, 0, 1), season_length=season_length)], freq=freq ) preds = sf.forecast(h=horizon, df=transformed_df, X_df=X_df) preds.head() ``` | | unique\_id | ds | ARIMA | | - | ---------- | --- | ---------- | | 0 | H1 | 701 | 612.737668 | | 1 | H1 | 702 | 542.851796 | | 2 | H1 | 703 | 501.931839 | | 3 | H1 | 704 | 470.248289 | | 4 | H1 | 705 | 448.115839 | We can now evaluate the performance. ```python theme={null} def compute_evaluation(preds): full = preds.merge(valid, on=['unique_id', 'ds']) mase24 = partial(mase, seasonality=24) res = evaluate(full, metrics=[smape, mase24], train_df=train).groupby('metric')['ARIMA'].mean() res_smape = '{:.1%}'.format(res['smape']) res_mase = '{:.1f}'.format(res['mase']) return pd.Series({'mase': res_mase, 'smape': res_smape}) ``` ```python theme={null} compute_evaluation(preds) ``` ```text theme={null} mase 1.0 smape 3.9% dtype: object ``` And compare this with just using the series values. ```python theme={null} preds_noexog = sf.forecast(h=horizon, df=train) compute_evaluation(preds_noexog) ``` ```text theme={null} mase 2.3 smape 7.7% dtype: object ``` # Numba caching Source: https://nixtlaverse.nixtla.io/statsforecast/docs/how-to-guides/numba_cache.html > Enabling caching for numba functions to reduce cold-starts `statsforecast` makes heavy use of [numba](https://numba.pydata.org/) to speed up several critical functions that estimate model parameters. This comes at a cost though, which is that the functions have to be [JIT compiled](https://en.wikipedia.org/wiki/Just-in-time_compilation) the first time they’re run, which can be expensive. Once a function has been JIT compiled, subsequent calls are significantly faster. One problem is that this compilation is saved (by default) on a per-session basis. In order to mitigate the compilation overhead numba offers the option to cache the function compiled code to a file, which can be then reused across sessions, and even copied over to different machines that share the same CPU characteristics ([more info](https://numba.pydata.org/numba-doc/latest/developer/caching.html#cache-sharing)). To leverage caching, you can set the `NIXTLA_NUMBA_CACHE` environment variable (e.g. `NIXTLA_NUMBA_CACHE=1`), which will enable caching for all functions. By default the cache is saved to the `__pycache__` directory, but you can override this with the `NUMBA_CACHE_DIR` environment variable to save it to a different path (e.g. `NUMBA_CACHE_DIR=numba_cache`), you can find more information in the [docs](https://numba.pydata.org/numba-doc/latest/reference/envvars.html#envvar-NUMBA_CACHE_DIR). If you want to have this enabled for all your sessions, we suggest adding `export NIXTLA_NUMBA_CACHE=1` to your profile files, such as `.bashrc`, `.zshrc`, etc. # Sklearn models Source: https://nixtlaverse.nixtla.io/statsforecast/docs/how-to-guides/sklearn_models.html > Use any scikit-learn model for forecasting statsforecast supports providing scikit-learn models through the `statsforecast.models.SklearnModel` wrapper. This can help you leverage feature engineering and train one model per serie, which can sometimes be better than training a single global model (as in mlforecast). ## Data setup ```python theme={null} from functools import partial from datasetsforecast.m4 import M4, M4Info from sklearn.linear_model import Lasso, Ridge from utilsforecast.feature_engineering import pipeline, trend, fourier from utilsforecast.plotting import plot_series from statsforecast import StatsForecast from statsforecast.models import SklearnModel from statsforecast.utils import ConformalIntervals ``` ```python theme={null} group = 'Hourly' season_length = M4Info[group].seasonality horizon = M4Info[group].horizon data, *_ = M4.load('data', group) data['ds'] = data['ds'].astype('int64') valid = data.groupby('unique_id').tail(horizon).copy() train = data.drop(valid.index) train.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | ## Generating features The utilsforecast library [provides some utilies for feature engineering](../../../utilsforecast/feature_engineering.html). ```python theme={null} train_features, valid_features = pipeline( train, features=[ trend, partial(fourier, season_length=season_length, k=10), # 10 fourier terms ], freq=1, h=horizon, ) train_features.head() ``` | | unique\_id | ds | y | trend | sin1\_24 | sin2\_24 | sin3\_24 | sin4\_24 | sin5\_24 | sin6\_24 | sin7\_24 | sin8\_24 | sin9\_24 | sin10\_24 | cos1\_24 | cos2\_24 | cos3\_24 | cos4\_24 | cos5\_24 | cos6\_24 | cos7\_24 | cos8\_24 | cos9\_24 | cos10\_24 | | - | ---------- | -- | ----- | ----- | --------- | --------- | --------- | --------- | --------- | --------- | --------- | --------- | --------- | --------- | -------- | -------- | --------- | --------- | --------- | --------- | --------- | --------- | --------- | --------- | | 0 | H1 | 1 | 605.0 | 261.0 | -0.707105 | -1.000000 | -0.707108 | -0.000012 | 0.707112 | 1.000000 | 0.707095 | 0.000024 | -0.707125 | -1.000000 | 0.707109 | 0.000006 | -0.707106 | -1.000000 | -0.707101 | -0.000003 | 0.707119 | 1.000000 | 0.707088 | -0.000015 | | 1 | H1 | 2 | 586.0 | 262.0 | -0.500001 | -0.866027 | -1.000000 | -0.866023 | -0.499988 | -0.000007 | 0.500001 | 0.866031 | 1.000000 | 0.866011 | 0.866025 | 0.499998 | 0.000004 | -0.500005 | -0.866032 | -1.000000 | -0.866025 | -0.499991 | 0.000019 | 0.500025 | | 2 | H1 | 3 | 586.0 | 263.0 | -0.258817 | -0.499997 | -0.707103 | -0.866021 | -0.965931 | -1.000000 | -0.965922 | -0.866033 | -0.707098 | -0.499964 | 0.965926 | 0.866027 | 0.707111 | 0.500007 | 0.258799 | 0.000012 | -0.258835 | -0.499986 | -0.707116 | -0.866046 | | 3 | H1 | 4 | 559.0 | 264.0 | 0.000005 | 0.000011 | 0.000008 | 0.000021 | 0.000003 | 0.000016 | -0.000001 | 0.000042 | -0.000006 | 0.000007 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | | 4 | H1 | 5 | 511.0 | 265.0 | 0.258820 | 0.500002 | 0.707114 | 0.866027 | 0.965925 | 1.000000 | 0.965930 | 0.866022 | 0.707106 | 0.500005 | 0.965926 | 0.866024 | 0.707099 | 0.499997 | 0.258822 | -0.000021 | -0.258803 | -0.500006 | -0.707107 | -0.866022 | ## Forecasting ```python theme={null} sf = StatsForecast( models=[ SklearnModel(Lasso()), SklearnModel(Ridge()), ], freq=1, ) preds = sf.forecast( df=train_features, h=horizon, X_df=valid_features, prediction_intervals=ConformalIntervals(n_windows=4, h=horizon), level=[95], ) plot_series(train, preds, level=[95], palette='tab20b', max_ids=4) ``` # ADIDA Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/adida.html > Step-by-step guide on using the `ADIDA Model` with `Statsforecast`. In this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation`. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) ## Table of Contents * [Introduction](#introduction) * [ADIDA Model](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of ADIDA with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The Aggregate-Disaggregate Intermittent Demand Approach (ADIDA) is a forecasting method that is used to predict the demand for products that exhibit intermittent demand patterns. Intermittent demand patterns are characterized by a large number of zero observations, which can make forecasting challenging. The ADIDA method uses temporal aggregation to reduce the number of zero observations and mitigate the effect of the variance observed in the intervals. The method uses equally sized time buckets to perform non-overlapping temporal aggregation and predict the demand over a pre-specified lead time. The time bucket is set equal to the mean inter-demand interval, which is the average time between two consecutive non-zero observations. The method uses the Simple Exponential Smoothing (SES) technique to obtain the forecasts. SES is a popular time series forecasting technique that is commonly used for its simplicity and effectiveness in producing accurate forecasts. The ADIDA method has several advantages. It is easy to implement and can be used for a wide range of intermittent demand patterns. The method also provides accurate forecasts and can be used to predict the demand over a pre-specified lead time. However, the ADIDA method has some limitations. The method assumes that the time buckets are equally sized, which may not be the case for all intermittent demand patterns. Additionally, the method may not be suitable for time series data with complex patterns or trends. Overall, the ADIDA method is a useful forecasting technique for intermittent demand patterns that can help mitigate the effect of zero observations and produce accurate demand forecasts. ## ADIDA Model ### What is intermittent demand? Intermittent demand is a demand pattern characterized by the irregular and sporadic occurrence of events or sales. In other words, it refers to situations in which the demand for a product or service occurs intermittently, with periods of time in which there are no sales or significant events. Intermittent demand differs from constant or regular demand, where sales occur in a predictable and consistent manner over time. In contrast, in intermittent demand, periods without sales may be long and there may not be a regular sequence of events. This type of demand can occur in different industries and contexts, such as low consumption products, seasonal products, high variability products, products with short life cycles, or in situations where demand depends on specific events or external factors. Intermittent demand can pose challenges in forecasting and inventory management, as it is difficult to predict when sales will occur and in what quantity. Methods like the Croston model, which I mentioned earlier, are used to address intermittent demand and generate more accurate and appropriate forecasts for this type of demand pattern. ### Problem with intermittent demand Intermittent demand can present various challenges and issues in inventory management and demand forecasting. Some of the common problems associated with intermittent demand are as follows: 1. Unpredictable variability: Intermittent demand can have unpredictable variability, making planning and forecasting difficult. Demand patterns can be irregular and fluctuate dramatically between periods with sales and periods without sales. 2. Low frequency of sales: Intermittent demand is characterized by long periods without sales. This can lead to inventory management difficulties, as it is necessary to hold enough stock to meet demand when it occurs, while avoiding excess inventory during non-sales periods. 3. Forecast error: Forecasting intermittent demand can be more difficult to pin down than constant demand. Traditional forecast models may not be adequate to capture the variability and lack of patterns in intermittent demand, which can lead to significant errors in estimates of future demand. 4. Impact on the supply chain: Intermittent demand can affect the efficiency of the supply chain and create difficulties in production planning, supplier management and logistics. Lead times and inventory levels must be adjusted to meet unpredictable demand. 5. Operating costs: Managing inventory in situations of intermittent demand can increase operating costs. Maintaining adequate inventory during non-sales periods and managing stock levels may require additional investments in storage and logistics. To address these issues, specific approaches to intermittent demand management are used, such as specialized forecasting models, product classification techniques, and tailored inventory strategies. These solutions seek to minimize the impacts of variability and lack of patterns in intermittent demand, optimizing inventory management and improving supply chain efficiency. ### ADIDA Model The ADIDA model is based on the Simple Exponential Smoothing (SES) method and uses temporal aggregation to handle the problem of intermittent demand. The mathematical development of the model can be summarized as follows: Let St be the demand at time $t$, where $t = 1, 2, ..., T$. The mean inter-demand interval is denoted as MI, which is the average time between two consecutive non-zero demands. The time bucket size is set equal to MI. The demand data is then aggregated into non-overlapping time buckets of size MI. Let Bt be the demand in bucket $t$, where $t = 1, 2, ..., T/MI$. The aggregated demand data can be represented as: $B_t = \sum S_t, for (t-1)*MI + 1 ≤ j ≤ t*MI$ The SES method is then applied to the aggregated demand data to obtain the forecasts. The forecast for bucket $t$ is denoted as $F_t$. The SES method involves estimating the level $L_t$ at time t based on the actual demand $D_t$ at time t and the estimated level at the previous time period, $L_{t-1}$, using the following equation: $L_t = \alpha * D_t + (1 - α) * L_{t-1}$ where $\alpha$ is the smoothing parameter that controls the weight given to the current demand value. The forecast for bucket $t$ is then obtained by using the estimated level at the previous time period, $L_{t-1}$, as follows: $F_t = L_{t-1}$ The forecasts are then disaggregated to obtain the demand predictions for the original time period. Let $Y_t$ be the demand prediction at time $t$. The disaggregation can be performed using the following equation: $Y_t = F_t / MI, for (t-1)*MI + 1 ≤ j ≤ t*MI$ ### How can you determine if the ADIDA model is suitable for a specific data set? To determine if the ADIDA model is suitable for a specific data set, the following steps can be followed: 1. Analyze the demand pattern: Examine the demand pattern of the data to determine if it fits an intermittent pattern. Intermittent data is characterized by a high proportion of zeros and sporadic demands in specific periods. 2. Evaluate seasonality: Check if there is a clear seasonality in the data. The ADIDA model assumes that there is no seasonality or that it can be handled by temporal aggregation. If the data show complex seasonality or cannot be handled by temporal aggregation, the ADIDA model may not be suitable. 3. Data requirements: Consider the data requirements of the ADIDA model. The model requires historical demand data and the ability to calculate the mean interval between non-zero demands. Make sure you have enough data to estimate the parameters and that the data is available at a frequency suitable for temporal aggregation. 4. Performance evaluation: Perform a performance evaluation of the ADIDA model on the specific data set. Compare model-generated forecasts with actual demand values and use evaluation metrics such as mean absolute error (MAE) or mean square error (MSE). If the model performs well and produces accurate forecasts on the data set, this is an indication that it is suitable for that data set. 5. Comparison with other models: Compare the performance of the ADIDA model with other forecast models suitable for intermittent data. Consider models like Croston, Syntetos-Boylan Approximation (SBA), or models based on exponential smoothing techniques that have been developed specifically for intermittent data. If the ADIDA model shows similar or better performance than other models, it can be considered suitable. Remember that the adequacy of the ADIDA model may depend on the specific nature of the data and the context of the forecasting problem. It is advisable to carry out a thorough analysis and experiment with different models to determine the most appropriate approach for the data set in question. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf from statsmodels.graphics.tsaplots import plot_pacf import plotly.graph_objects as go plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ```python theme={null} import pandas as pd df = pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/tipos_malarias_choco_colombia.csv", sep=";", usecols=[0,4]) df = df.dropna() df.head() ``` | | semanas | malaria\_falciparum | | - | ---------- | ------------------- | | 0 | 2007-12-31 | 50.0 | | 1 | 2008-01-07 | 62.0 | | 2 | 2008-01-14 | 76.0 | | 3 | 2008-01-21 | 64.0 | | 4 | 2008-01-28 | 38.0 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ---------- | ---- | ---------- | | 0 | 2007-12-31 | 50.0 | 1 | | 1 | 2008-01-07 | 62.0 | 1 | | 2 | 2008-01-14 | 76.0 | 1 | | 3 | 2008-01-21 | 64.0 | 1 | | 4 | 2008-01-28 | 38.0 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y float64 unique_id object dtype: object ``` We need to convert the `object` types to datetime and numeric. ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) df["y"] = df["y"].astype(float).astype("int64") ``` ## Explore data with the plot method Plot a series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = Level + Trend + Seasonality + Noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose from plotly.subplots import make_subplots import plotly.graph_objects as go def plot_seasonal_decompose( x, model='additive', filt=None, period=None, two_sided=True, extrapolate_trend=0, title="Seasonal Decomposition"): result = seasonal_decompose( x, model=model, filt=filt, period=period, two_sided=two_sided, extrapolate_trend=extrapolate_trend) fig = make_subplots( rows=4, cols=1, subplot_titles=["Observed", "Trend", "Seasonal", "Residuals"]) for idx, col in enumerate(['observed', 'trend', 'seasonal', 'resid']): fig.add_trace( go.Scatter(x=result.observed.index, y=getattr(result, col), mode='lines'), row=idx+1, col=1, ) return fig ``` ```python theme={null} plot_seasonal_decompose( df["y"], model="additive", period=52, title="Seasonal Decomposition") ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `ADIDA Model`. 2. Data to test our model For the test data we will use the last 25 week to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2022-07-04'] test = df[df.ds>'2022-07-04'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((758, 3), (25, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train", linestyle="--",linewidth=2) sns.lineplot(test, x="ds", y="y", label="Test", linewidth=2, color="yellow") plt.title("Falciparum Malaria"); plt.show() ``` ## Implementation of `ADIDA Model` with StatsForecast To also know more about the parameters of the functions of the `ADIDA Model`, they are listed below. For more information, visit the [documentation](../../src/core/models.html#adida) ```text theme={null} alias : str Custom name of the model. prediction_intervals : Optional[ConformalIntervals] Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. ``` ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import ADIDA ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 52 # Hourly data horizon = len(test) # number of predictions # We call the model that we are going to use models = [ADIDA()] ``` We fit the models by instantiating a new `StatsForecast` object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='7d', n_jobs=-1) ``` ### Fit the Model Here, we call the `fit()` method to fit the model. ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[ADIDA]) ``` Let’s see the results of our `ADIDA Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'mean': array([336.74736919])} ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `forecast()` method does not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 25 week ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon) Y_hat ``` | | unique\_id | ds | ADIDA | | --- | ---------- | ---------- | ---------- | | 0 | 1 | 2022-07-11 | 336.747375 | | 1 | 1 | 2022-07-18 | 336.747375 | | 2 | 1 | 2022-07-25 | 336.747375 | | ... | ... | ... | ... | | 22 | 1 | 2022-12-12 | 336.747375 | | 23 | 1 | 2022-12-19 | 336.747375 | | 24 | 1 | 2022-12-26 | 336.747375 | ```python theme={null} sf.plot(train, Y_hat.merge(test)) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 25 week ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecast_df = sf.predict(h=horizon) forecast_df.head() ``` | | unique\_id | ds | ADIDA | | - | ---------- | ---------- | ---------- | | 0 | 1 | 2022-07-11 | 336.747375 | | 1 | 1 | 2022-07-18 | 336.747375 | | 2 | 1 | 2022-07-25 | 336.747375 | | 3 | 1 | 2022-08-01 | 336.747375 | | 4 | 1 | 2022-08-08 | 336.747375 | ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy:
img
img
### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=30, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | ADIDA | | --- | ---------- | ---------- | ---------- | ----- | ---------- | | 0 | 1 | 2020-03-23 | 2020-03-16 | 317.0 | 251.901505 | | 1 | 1 | 2020-03-30 | 2020-03-16 | 332.0 | 251.901505 | | 2 | 1 | 2020-04-06 | 2020-03-16 | 306.0 | 251.901505 | | ... | ... | ... | ... | ... | ... | | 122 | 1 | 2022-12-12 | 2022-07-04 | 151.0 | 336.747375 | | 123 | 1 | 2022-12-19 | 2022-07-04 | 97.0 | 336.747375 | | 124 | 1 | 2022-12-26 | 2022-07-04 | 42.0 | 336.747375 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | ADIDA | | - | ---------- | ------ | ---------- | | 0 | 1 | mae | 114.527585 | | 1 | 1 | mape | 0.820029 | | 2 | 1 | mase | 0.874115 | | 3 | 1 | rmse | 129.749320 | | 4 | 1 | smape | 0.221878 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla ADIDA API](../../src/core/models.html#adida) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # ARCH Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/arch.html > Step-by-step guide on using the `ARCH Model` with `Statsforecast`. In this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation`. The text in this article is largely taken from [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) ## Table of Contents * [Introduction](#introduction) * [ARCH Models](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of ARCH with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction Financial time series analysis has been one of the hottest research topics in the recent decades. In this guide, we illustrate the stylized facts of financial time series by real financial data. To characterize these facts, new models different from the Box- Jenkins ones are needed. And for this reason, ARCH models were firstly proposed by R. F. Engle in 1982 and have been extended by a great number of scholars since then. We also demonstrate how to use Python and its libraries to implement `ARCH`. As we have known, there are lot of time series that possess the ARCH effect, that is, although the (modeling residual) series is white noise, its squared series may be autocorrelated. What is more, in practice, a large number of financial time series are found having this property so that the ARCH effect has become one of the stylized facts from financial time series. ### Stylized Facts of Financial Time Series Now we briefly list and describe several important stylized facts (features) of financial return series: * **Fat (heavy) tails:** The distribution density function of returns often has fatter (heavier) tails than the tails of the corresponding normal distribution density. * **ARCH effect:** Although the return series can often be seen as a white noise, its squared (and absolute) series may usually be autocorrelated, and these autocorrelations are hardly negative. * **Volatility clustering:** Large changes in returns tend to cluster in time, and small changes tend to be followed by small changes. * **Asymmetry:** As we have know , the distribution of asset returns is slightly negatively skewed. One possible explanation could be that traders react more strongly to unfavorable information than favorable information. ## Definition of ARCH Models Specifically, we give the definition of the ARCH model as follows. **Definition 1.** An $\text{ARCH(p)}$ model with order $p≥1$ is of the form $$ \begin{equation} \left\{ \begin{array}{ll} X_t =\sigma_t \varepsilon_t \\ \sigma_{t}^2 =\omega+ \alpha_1 X_{t-1}^2 + \alpha_2 X_{t-2}^2 + \cdots+ \alpha_p X_{t-p}^2 \\ \end{array} \right. \end{equation} $$ where $\omega ≥ 0, \alpha_i ≥ 0$, and $\alpha_p > 0$ are constants, $\varepsilon_t \sim iid(0, 1)$, and $\varepsilon_t$ is independent of $\{X_k;k ≤ t − 1 \}$. A stochastic process $X_t$ is called an $ARCH(p)$ process if it satisfies Eq. (1). By Definition 1, $\sigma_{t}^2$ (and $\sigma_t$ ) is independent of $\varepsilon_t$ . Besides, usually it is further assumed that $\varepsilon_t \sim N(0, 1)$. Sometimes, however, we need to further suppose that $\varepsilon_t$ follows a standardized (skew) Student’s T distribution or a generalized error distribution in order to capture more features of a financial time series. Let $\mathscr{F}_s$ denote the information set generated by $\{X_k;k ≤ s \}$, namely, the sigma field $\sigma(X_k;k ≤ s)$. It is easy to see that $\mathscr{F}_s$ is independent of $\varepsilon_t$ for any $s 0$, in light of the properties of the conditional mathematical expectation and by (2), we have that $E(X_{t+h} X_t) = E(E(X_{t+h} X_t|\mathscr{F}_{t+h-1})) = E(X_t E(X_{t+h}|\mathscr{F}_{t+h-1})) = 0.$ In conclusion, if $0 < \alpha_1 < 1$, we have that: * Any $\text{ARCH}(1)$ process $\{X_t \}$ defined by Eqs.(3) follows a white noise $WN(0, \omega/(1 − \alpha_1))$ . * Since $X_{t}^2$ is an $\text{AR}(1)$ process defined by (4), $\text{Corr}(X_{t}^2,X_{t+h}^2) = \alpha_{1}^{|h|} > 0$, which reveals the ARCH effect. * It is clear that $E(\eta_t|\mathscr{F}_s)=0$ for any $t>s$,and with Eq.(4),for any $k>1$: $Var(X_{t+k} |\mathscr{F}_t ) = E(X_{t+K}^2 |\mathscr{F}_t)$ $= E(\omega + \alpha_1 X_{t+k-1}+ \eta_{t+k}|\mathscr{F}_t )$ $= \omega + \alpha_1 Var(X_{t+k−1}|\mathscr{F}_t),$ which reflects the volatility clustering, that is, large (small) volatility is followed by large (small) one. In addition, we are able to prove that Xt defined by Eq. (3) has heavier tails than the corresponding normal distribution. At last, note that these properties of the ARCH(1) model can be generalized to ARCH(p) models. ### Advantages and disadvantages of the Autoregressive Conditional Heteroskedasticity (ARCH) model: | Advantages | Disadvantages | | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | - The ARCH model is useful for modeling volatility in financial time series, which is important for investment decision making and risk management. | - The ARCH model assumes that the forecast errors are independent and identically distributed, which may not be realistic in some cases. | | - The ARCH model takes heteroscedasticity into account, which means that it can model time series with variances that change over time. | - The ARCH model can be difficult to fit to data with many parameters, which may require large amounts of data or advanced estimation techniques. | | - The ARCH model is relatively easy to use and can be implemented with standard econometrics software. | - The ARCH model does not take into account the possible relationship between the mean and the variance of the time series, which may be important in some cases. | Note: The ARCH model is a useful tool for modeling volatility in financial time series, but like any econometric model, it has limitations and should be used with caution depending on the specific characteristics of the data being modeled. ### Autoregressive Conditional Heteroskedasticity (ARCH) Applications * **Finance** - The ARCH model is widely used in finance to model volatility in financial time series, such as stock prices, exchange rates, interest rates, etc. * **Economics** - The ARCH model can be used to model volatility in economic data, such as GDP, inflation, unemployment, among others. * **Engineering** - The ARCH model can be used in engineering to model volatility in data related to energy, climate, pollution, industrial production, among others. * **Social Sciences** - The ARCH model can be used in the social sciences to model volatility in data related to demography, health, education, among others. * **Biology** - The ARCH model can be used in biology to model volatility in data related to evolution, genetics, epidemiology, among others. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt from statsmodels.graphics.tsaplots import plot_acf from statsmodels.graphics.tsaplots import plot_pacf plt.style.use('fivethirtyeight') plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#212946', 'axes.facecolor': '#212946', 'savefig.facecolor':'#212946', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#2A3459', 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data Let’s pull the S\&P500 stock data from the Yahoo Finance site. ```python theme={null} import datetime import pandas as pd import time import yfinance as yf ticker = '^GSPC' period1 = datetime.datetime(2015, 1, 1) period2 = datetime.datetime(2023, 9, 22) interval = '1d' # 1d, 1m SP_500 = yf.download(ticker, start=period1, end=period2, interval=interval, progress=False) SP_500 = SP_500.reset_index() SP_500.head() ``` | Price | Date | Adj Close | Close | High | Low | Open | Volume | | ------ | ------------------------- | ----------- | ----------- | ----------- | ----------- | ----------- | ---------- | | Ticker | | ^GSPC | ^GSPC | ^GSPC | ^GSPC | ^GSPC | ^GSPC | | 0 | 2015-01-02 00:00:00+00:00 | 2058.199951 | 2058.199951 | 2072.360107 | 2046.040039 | 2058.899902 | 2708700000 | | 1 | 2015-01-05 00:00:00+00:00 | 2020.579956 | 2020.579956 | 2054.439941 | 2017.339966 | 2054.439941 | 3799120000 | | 2 | 2015-01-06 00:00:00+00:00 | 2002.609985 | 2002.609985 | 2030.250000 | 1992.439941 | 2022.150024 | 4460110000 | | 3 | 2015-01-07 00:00:00+00:00 | 2025.900024 | 2025.900024 | 2029.609985 | 2005.550049 | 2005.550049 | 3805480000 | | 4 | 2015-01-08 00:00:00+00:00 | 2062.139893 | 2062.139893 | 2064.080078 | 2030.609985 | 2030.609985 | 3934010000 | ```python theme={null} df=SP_500[["Date","Close"]].copy() ``` The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------------- | ----------- | ---------- | | 0 | 2015-01-02 00:00:00+00:00 | 2058.199951 | 1 | | 1 | 2015-01-05 00:00:00+00:00 | 2020.579956 | 1 | | 2 | 2015-01-06 00:00:00+00:00 | 2002.609985 | 1 | | 3 | 2015-01-07 00:00:00+00:00 | 2025.900024 | 1 | | 4 | 2015-01-08 00:00:00+00:00 | 2062.139893 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds datetime64[ns] y float64 unique_id object dtype: object ``` ## Explore data with the plot method Plot a series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### The Augmented Dickey-Fuller Test An Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis. Augmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis. Null Hypothesis: Time Series is non-stationary. It gives a time-dependent trend. Alternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time. ADF or t Statistic \< critical values: Reject the null hypothesis, time series is stationary. ADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary. Let’s check if our series that we are analyzing is a stationary series. Let’s create a function to check, using the `Dickey Fuller` test ```python theme={null} from statsmodels.tsa.stattools import adfuller def Augmented_Dickey_Fuller_Test_func(series , column_name): print (f'Dickey-Fuller test results for columns: {column_name}') dftest = adfuller(series, autolag='AIC') dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used']) for key,value in dftest[4].items(): dfoutput['Critical Value (%s)'%key] = value print (dfoutput) if dftest[1] <= 0.05: print("Conclusion:====>") print("Reject the null hypothesis") print("The data is stationary") else: print("Conclusion:====>") print("The null hypothesis cannot be rejected") print("The data is not stationary") ``` ```python theme={null} Augmented_Dickey_Fuller_Test_func(df["y"],'S&P500') ``` ```text theme={null} Dickey-Fuller test results for columns: S&P500 Test Statistic -0.814971 p-value 0.814685 No Lags Used 10.000000 ... Critical Value (1%) -3.433341 Critical Value (5%) -2.862861 Critical Value (10%) -2.567473 Length: 7, dtype: float64 Conclusion:====> The null hypothesis cannot be rejected The data is not stationary ``` In the previous result we can see that the `Augmented_Dickey_Fuller` test gives us a `p-value` of 0.864700, which tells us that the null hypothesis cannot be rejected, and on the other hand the data of our series are not stationary. We need to differentiate our time series, in order to convert the data to stationary. ### Return Series Since the 1970s, the financial industry has been very prosperous with advancement of computer and Internet technology. Trade of financial products (including various derivatives) generates a huge amount of data which form financial time series. For finance, the return on a financial product is most interesting, and so our attention focuses on the return series. If $P_t$ is the closing price at time t for a certain financial product, then the return on this product is $X_t = \frac{(P_t − P_{t−1})}{P_{t−1}} ≈ log(P_t ) − log(P_{t−1}).$ It is return series $\{X_t \}$ that have been much independently studied. And important stylized features which are common across many instruments, markets, and time periods have been summarized. Note that if you purchase the financial product, then it becomes your asset, and its returns become your asset returns. Now let us look at the following examples. We can estimate the series of returns using the [pandas](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pct_change.html), `DataFrame.pct_change()` function. The `pct_change()` function has a periods parameter whose default value is 1. If you want to calculate a 30-day return, you must change the value to 30. ```python theme={null} df['return'] = 100 * df["y"].pct_change() df.dropna(inplace=True, how='any') df.head() ``` | | ds | y | unique\_id | return | | - | ------------------------- | ----------- | ---------- | --------- | | 1 | 2015-01-05 00:00:00+00:00 | 2020.579956 | 1 | -1.827811 | | 2 | 2015-01-06 00:00:00+00:00 | 2002.609985 | 1 | -0.889347 | | 3 | 2015-01-07 00:00:00+00:00 | 2025.900024 | 1 | 1.162984 | | 4 | 2015-01-08 00:00:00+00:00 | 2062.139893 | 1 | 1.788828 | | 5 | 2015-01-09 00:00:00+00:00 | 2044.810059 | 1 | -0.840381 | ```python theme={null} import plotly.express as px fig = px.line(df, x=df["ds"], y="return",title="SP500 Return Chart",template = "plotly_dark") fig.show() ``` ### Creating Squared Returns ```python theme={null} df['sq_return'] = df["return"].mul(df["return"]) df.head() ``` | | ds | y | unique\_id | return | sq\_return | | - | ------------------------- | ----------- | ---------- | --------- | ---------- | | 1 | 2015-01-05 00:00:00+00:00 | 2020.579956 | 1 | -1.827811 | 3.340891 | | 2 | 2015-01-06 00:00:00+00:00 | 2002.609985 | 1 | -0.889347 | 0.790938 | | 3 | 2015-01-07 00:00:00+00:00 | 2025.900024 | 1 | 1.162984 | 1.352532 | | 4 | 2015-01-08 00:00:00+00:00 | 2062.139893 | 1 | 1.788828 | 3.199906 | | 5 | 2015-01-09 00:00:00+00:00 | 2044.810059 | 1 | -0.840381 | 0.706240 | ### Returns vs Squared Returns ```python theme={null} from plotly.subplots import make_subplots import plotly.graph_objects as go fig = make_subplots(rows=1, cols=2) fig.add_trace(go.Scatter(x=df["ds"], y=df["return"], mode='lines', name='return'), row=1, col=1 ) fig.add_trace(go.Scatter(x=df["ds"], y=df["sq_return"], mode='lines', name='sq_return'), row=1, col=2 ) fig.update_layout(height=600, width=800, title_text="Returns vs Squared Returns", template = "plotly_dark") fig.show() ``` ```python theme={null} from scipy.stats import probplot, moment from statsmodels.tsa.stattools import adfuller, q_stat, acf import numpy as np import seaborn as sns def plot_correlogram(x, lags=None, title=None): lags = min(10, int(len(x)/5)) if lags is None else lags fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(14, 8)) x.plot(ax=axes[0][0], title='Return') x.rolling(21).mean().plot(ax=axes[0][0], c='k', lw=1) q_p = np.max(q_stat(acf(x, nlags=lags), len(x))[1]) stats = f'Q-Stat: {np.max(q_p):>8.2f}\nADF: {adfuller(x)[1]:>11.2f}' axes[0][0].text(x=.02, y=.85, s=stats, transform=axes[0][0].transAxes) probplot(x, plot=axes[0][1]) mean, var, skew, kurtosis = moment(x, moment=[1, 2, 3, 4]) s = f'Mean: {mean:>12.2f}\nSD: {np.sqrt(var):>16.2f}\nSkew: {skew:12.2f}\nKurtosis:{kurtosis:9.2f}' axes[0][1].text(x=.02, y=.75, s=s, transform=axes[0][1].transAxes) plot_acf(x=x, lags=lags, zero=False, ax=axes[1][0]) plot_pacf(x, lags=lags, zero=False, ax=axes[1][1]) axes[1][0].set_xlabel('Lag') axes[1][1].set_xlabel('Lag') fig.suptitle(title+ f'Dickey-Fuller: {adfuller(x)[1]:>11.2f}', fontsize=14) sns.despine() fig.tight_layout() fig.subplots_adjust(top=.9) ``` ```python theme={null} plot_correlogram(df["return"], lags=30, title="Time Series Analysis plot \n") ``` ### Ljung-Box Test Ljung-Box is a test for autocorrelation that we can use in tandem with our ACF and PACF plots. The Ljung-Box test takes our data, optionally either lag values to test, or the largest lag value to consider, and whether to compute the Box-Pierce statistic. Ljung-Box and Box-Pierce are two similar test statisitcs, Q , that are compared against a chi-squared distribution to determine if the series is white noise. We might use the Ljung-Box test on the residuals of our model to look for autocorrelation, ideally our residuals would be white noise. * Ho : The data are independently distributed, no autocorrelation. * Ha : The data are not independently distributed; they exhibit serial correlation. The Ljung-Box with the Box-Pierce option will return, for each lag, the Ljung-Box test statistic, Ljung-Box p-values, Box-Pierce test statistic, and Box-Pierce p-values. If $p<\alpha (0.05)$ we reject the null hypothesis. ```python theme={null} from statsmodels.stats.diagnostic import acorr_ljungbox ljung_res = acorr_ljungbox(df["return"], lags= 40, boxpierce=True) ljung_res.head() ``` | | lb\_stat | lb\_pvalue | bp\_stat | bp\_pvalue | | - | --------- | ------------ | --------- | ------------ | | 1 | 49.222273 | 2.285409e-12 | 49.155183 | 2.364927e-12 | | 2 | 62.991348 | 2.097020e-14 | 62.899234 | 2.195861e-14 | | 3 | 63.944944 | 8.433622e-14 | 63.850663 | 8.834380e-14 | | 4 | 74.343652 | 2.742989e-15 | 74.221024 | 2.911751e-15 | | 5 | 80.234862 | 7.494100e-16 | 80.093498 | 8.022242e-16 | ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `ARCH` model 2. Data to test our model For the test data we will use the last 30 day to test and evaluate the performance of our model. ```python theme={null} df=df[["ds","unique_id","return"]] df.columns=["ds", "unique_id", "y"] ``` ```python theme={null} train = df[df.ds<='2023-05-24'] # Let's forecast the last 30 days test = df[df.ds>'2023-05-24'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((2112, 3), (82, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train") sns.lineplot(test, x="ds", y="y", label="Test") plt.show() ``` ## Implementation of ARCH with StatsForecast To also know more about the parameters of the functions of the `ARCH Model`, they are listed below. For more information, visit the [documentation](../../src/core/models.html#arch) ```text theme={null} p : int Number of lagged versions of the series. alias : str Custom name of the model. prediction_intervals : Optional[ConformalIntervals] Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. ``` ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import ARCH ``` ### Building Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful.season\_length. ```python theme={null} season_length = 7 # Daily data horizon = len(test) # number of predictions biasadj=True, include_drift=True, models = [ARCH(p=2)] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='C', # custom business day frequency ) ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[ARCH(2)]) ``` Let’s see the results of our ARCH model. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'p': 2, 'q': 0, 'coeff': array([0.44321058, 0.34706751, 0.35172097]), 'message': 'Optimization terminated successfully', 'y_vals': array([-1.12220267, -0.73186003]), 'sigma2_vals': array([1.38768694, nan, 1.89278112, ..., 0.76423271, 0.45064684, 0.88037072]), 'fitted': array([ nan, nan, 2.23474807, ..., -1.48033228, 1.10018999, -0.98050166]), 'actual_residuals': array([ nan, nan, -1.07176381, ..., 1.49583575, -2.22239266, 0.24864162])} ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("actual_residuals"), columns=["residual Model"]) residual ``` | | residual Model | | ---- | -------------- | | 0 | NaN | | 1 | NaN | | 2 | -1.071764 | | ... | ... | | 2109 | 1.495836 | | 2110 | -2.222393 | | 2111 | 0.248642 | ```python theme={null} import scipy.stats as stats fig, axs = plt.subplots(nrows=2, ncols=2) # plot[1,1] residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); # plot sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); # plot stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') # plot plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | ARCH(2) | | --- | ---------- | ------------------------- | --------- | | 0 | 1 | 2023-05-25 00:00:00+00:00 | 1.681839 | | 1 | 1 | 2023-05-26 00:00:00+00:00 | -0.777029 | | 2 | 1 | 2023-05-29 00:00:00+00:00 | -0.677962 | | ... | ... | ... | ... | | 79 | 1 | 2023-09-13 00:00:00+00:00 | 0.695591 | | 80 | 1 | 2023-09-14 00:00:00+00:00 | -0.176075 | | 81 | 1 | 2023-09-15 00:00:00+00:00 | -0.158605 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | ARCH(2) | | - | ---------- | ------------------------- | --------- | --------- | | 0 | 1 | 2015-01-05 00:00:00+00:00 | -1.827811 | NaN | | 1 | 1 | 2015-01-06 00:00:00+00:00 | -0.889347 | NaN | | 2 | 1 | 2015-01-07 00:00:00+00:00 | 1.162984 | 2.234748 | | 3 | 1 | 2015-01-08 00:00:00+00:00 | 1.788828 | -0.667577 | | 4 | 1 | 2015-01-09 00:00:00+00:00 | -0.840381 | -0.752438 | Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | ARCH(2) | ARCH(2)-lo-95 | ARCH(2)-hi-95 | | --- | ---------- | ------------------------- | --------- | ------------- | ------------- | | 0 | 1 | 2023-05-25 00:00:00+00:00 | 1.681839 | -0.419326 | 3.783003 | | 1 | 1 | 2023-05-26 00:00:00+00:00 | -0.777029 | -3.939054 | 2.384996 | | 2 | 1 | 2023-05-29 00:00:00+00:00 | -0.677962 | -3.907262 | 2.551338 | | ... | ... | ... | ... | ... | ... | | 79 | 1 | 2023-09-13 00:00:00+00:00 | 0.695591 | -0.937585 | 2.328766 | | 80 | 1 | 2023-09-14 00:00:00+00:00 | -0.176075 | -1.405359 | 1.053210 | | 81 | 1 | 2023-09-15 00:00:00+00:00 | -0.158605 | -1.381915 | 1.064705 | ```python theme={null} # Merge the forecasts with the true values Y_hat1 = test.merge(Y_hat, how='left', on=['unique_id', 'ds']) Y_hat1 ``` | | ds | unique\_id | y | ARCH(2) | | --- | ------------------------- | ---------- | --------- | --------- | | 0 | 2023-05-25 00:00:00+00:00 | 1 | 0.875758 | 1.681839 | | 1 | 2023-05-26 00:00:00+00:00 | 1 | 1.304909 | -0.777029 | | 2 | 2023-05-30 00:00:00+00:00 | 1 | 0.001660 | -0.968703 | | ... | ... | ... | ... | ... | | 79 | 2023-09-19 00:00:00+00:00 | 1 | -0.215101 | NaN | | 80 | 2023-09-20 00:00:00+00:00 | 1 | -0.939479 | NaN | | 81 | 2023-09-21 00:00:00+00:00 | 1 | -1.640093 | NaN | ```python theme={null} # Merge the forecasts with the true values fig, ax = plt.subplots(1, 1) plot_df = pd.concat([train, Y_hat1]).set_index('ds') plot_df[['y', "ARCH(2)"]].plot(ax=ax, linewidth=2) ax.set_title(' Forecast', fontsize=22) ax.set_ylabel('Year ', fontsize=20) ax.set_xlabel('Timestamp [t]', fontsize=20) ax.legend(prop={'size': 15}) ax.grid(True) plt.show() ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | ARCH(2) | | --- | ---------- | ------------------------- | --------- | | 0 | 1 | 2023-05-25 00:00:00+00:00 | 1.681839 | | 1 | 1 | 2023-05-26 00:00:00+00:00 | -0.777029 | | 2 | 1 | 2023-05-29 00:00:00+00:00 | -0.677962 | | ... | ... | ... | ... | | 79 | 1 | 2023-09-13 00:00:00+00:00 | 0.695591 | | 80 | 1 | 2023-09-14 00:00:00+00:00 | -0.176075 | | 81 | 1 | 2023-09-15 00:00:00+00:00 | -0.158605 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[80,95]) forecast_df ``` | | unique\_id | ds | ARCH(2) | ARCH(2)-lo-95 | ARCH(2)-lo-80 | ARCH(2)-hi-80 | ARCH(2)-hi-95 | | --- | ---------- | ------------------------- | --------- | ------------- | ------------- | ------------- | ------------- | | 0 | 1 | 2023-05-25 00:00:00+00:00 | 1.681839 | -0.419326 | 0.307961 | 3.055716 | 3.783003 | | 1 | 1 | 2023-05-26 00:00:00+00:00 | -0.777029 | -3.939054 | -2.844566 | 1.290508 | 2.384996 | | 2 | 1 | 2023-05-29 00:00:00+00:00 | -0.677962 | -3.907262 | -2.789488 | 1.433564 | 2.551338 | | ... | ... | ... | ... | ... | ... | ... | ... | | 79 | 1 | 2023-09-13 00:00:00+00:00 | 0.695591 | -0.937585 | -0.372285 | 1.763467 | 2.328766 | | 80 | 1 | 2023-09-14 00:00:00+00:00 | -0.176075 | -1.405359 | -0.979860 | 0.627711 | 1.053210 | | 81 | 1 | 2023-09-15 00:00:00+00:00 | -0.158605 | -1.381915 | -0.958485 | 0.641274 | 1.064705 | We can join the forecast result with the historical data using the pandas function `pd.concat()`, and then be able to use this result for graphing. ```python theme={null} df_plot=pd.concat([df, forecast_df]).set_index('ds').tail(220) df_plot ``` | | unique\_id | y | ARCH(2) | ARCH(2)-lo-95 | ARCH(2)-lo-80 | ARCH(2)-hi-80 | ARCH(2)-hi-95 | | ------------------------- | ---------- | --------- | --------- | ------------- | ------------- | ------------- | ------------- | | ds | | | | | | | | | 2023-03-07 00:00:00+00:00 | 1 | -1.532692 | NaN | NaN | NaN | NaN | NaN | | 2023-03-08 00:00:00+00:00 | 1 | 0.141479 | NaN | NaN | NaN | NaN | NaN | | 2023-03-09 00:00:00+00:00 | 1 | -1.845936 | NaN | NaN | NaN | NaN | NaN | | ... | ... | ... | ... | ... | ... | ... | ... | | 2023-09-13 00:00:00+00:00 | 1 | NaN | 0.695591 | -0.937585 | -0.372285 | 1.763467 | 2.328766 | | 2023-09-14 00:00:00+00:00 | 1 | NaN | -0.176075 | -1.405359 | -0.979860 | 0.627711 | 1.053210 | | 2023-09-15 00:00:00+00:00 | 1 | NaN | -0.158605 | -1.381915 | -0.958485 | 0.641274 | 1.064705 | ```python theme={null} sf.plot(train, test.merge(forecast_df), level=[80, 95], max_insample_length=120) ``` Let’s plot the same graph using the plot function that comes in `Statsforecast`, as shown below. ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=5)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=train, h=horizon, step_size=6, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | ARCH(2) | | --- | ---------- | ------------------------- | ------------------------- | --------- | --------- | | 0 | 1 | 2022-12-21 00:00:00+00:00 | 2022-12-20 00:00:00+00:00 | 1.486799 | 1.382105 | | 1 | 1 | 2022-12-22 00:00:00+00:00 | 2022-12-20 00:00:00+00:00 | -1.445170 | -0.651618 | | 2 | 1 | 2022-12-23 00:00:00+00:00 | 2022-12-20 00:00:00+00:00 | 0.586810 | -0.595213 | | ... | ... | ... | ... | ... | ... | | 407 | 1 | 2023-05-22 00:00:00+00:00 | 2023-01-26 00:00:00+00:00 | 0.015503 | 0.693070 | | 408 | 1 | 2023-05-23 00:00:00+00:00 | 2023-01-26 00:00:00+00:00 | -1.122203 | -0.176181 | | 409 | 1 | 2023-05-24 00:00:00+00:00 | 2023-01-26 00:00:00+00:00 | -0.731860 | -0.157522 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae, mape, mase, rmse, smape ``` ```python theme={null} evaluate( test.merge(Y_hat), train_df=train, metrics=[mae, mape, partial(mase, seasonality=5), rmse, smape], agg_fn='mean', ) ``` | | metric | ARCH(2) | | - | ------ | --------- | | 0 | mae | 0.949721 | | 1 | mape | 11.789856 | | 2 | mase | 0.875298 | | 3 | rmse | 1.164914 | | 4 | smape | 0.725702 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. [Engle, R. F. (1982). Autoregressive conditional heteroscedasticity with estimates of the variance of United Kingdom inflation. Econometrica: Journal of the econometric society, 987-1007.](http://www.econ.uiuc.edu/~econ508/Papers/engle82.pdf). 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla ARCH API](../../src/core/models.html#arch) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # ARIMA Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/arima.html > Step-by-step guide on using the `ARIMA Model` with `Statsforecast`. In this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation`. The text in this article is largely taken from [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”.](https://otexts.com/fpp3/tscv.html) ## Table of Contents * [Introduction](#introduction) * [ARIMA Models](#model) * [The meaning of p, d and q in ARIMA model](#concepts) * [AR and MA models](#ar_ma) * [ARIMA model](#arima) * [How to find the order of differencing (d) in ARIMA model](#order_d) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [How to find the order of the AR term (p)](#order_p) * [How to find the order of the MA term (q)](#order_q) * [How to handle if a time series is slightly under or over differenced](#differencing) * [Implementation of ARIMA with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction * A **Time Series** is defined as a series of data points recorded at different time intervals. The time order can be daily, monthly, or even yearly. * Time Series forecasting is the process of using a statistical model to predict future values of a time series based on past results. * We have discussed various aspects of **Time Series Forecasting** in the previous notebook. * Forecasting is the step where we want to predict the future values the series is going to take. Forecasting a time series is often of tremendous commercial value. **Forecasting a time series can be broadly divided into two types.** * If we use only the previous values of the time series to predict its future values, it is called **Univariate Time Series Forecasting.** * If we use predictors other than the series (like exogenous variables) to forecast it is called **Multi Variate Time Series Forecasting.** * This notebook focuses on a particular type of forecasting method called **ARIMA modeling.** ## Introduction to ARIMA Models * **ARIMA** stands for **Autoregressive Integrated Moving Average Model**. It belongs to a class of models that explains a given time series based on its own past values -i.e.- its own lags and the lagged forecast errors. The equation can be used to forecast future values. Any ‘non-seasonal’ time series that exhibits patterns and is not a random white noise can be modeled with ARIMA models. * So, **ARIMA**, short for **AutoRegressive Integrated Moving Average**, is a forecasting algorithm based on the idea that the information in the past values of the time series can alone be used to predict the future values. * **ARIMA Models** are specified by three order parameters: (p, d, q), where, * p is the order of the AR term * q is the order of the MA term * d is the number of differencing required to make the time series stationary * **AR(p) Autoregression** - a regression model that utilizes the dependent relationship between a current observation and observations over a previous period. An auto regressive (AR(p)) component refers to the use of past values in the regression equation for the time series. * **I(d) Integration** - uses differencing of observations (subtracting an observation from observation at the previous time step) in order to make the time series stationary. Differencing involves the subtraction of the current values of a series with its previous values d number of times. * **MA(q) Moving Average** - a model that uses the dependency between an observation and a residual error from a moving average model applied to lagged observations. A moving average component depicts the error of the model as a combination of previous error terms. The order q represents the number of terms to be included in the model. ### Types of ARIMA Model * **ARIMA** : Non-seasonal Autoregressive Integrated Moving Averages * **SARIMA** : Seasonal ARIMA * **SARIMAX** : Seasonal ARIMA with exogenous variables If a time series, has seasonal patterns, then we need to add seasonal terms and it becomes SARIMA, short for **Seasonal ARIMA**. ## The meaning of p, d and q in ARIMA model ### The meaning of p * `p` is the order of the **Auto Regressive (AR)** term. It refers to the number of lags of Y to be used as predictors. ### The meaning of d * The term **Auto Regressive**’ in ARIMA means it is a linear regression model that uses its own lags as predictors. Linear regression models, as we know, work best when the predictors are not correlated and are independent of each other. So we need to make the time series stationary. * The most common approach to make the series stationary is to difference it. That is, subtract the previous value from the current value. Sometimes, depending on the complexity of the series, more than one differencing may be needed. * The value of d, therefore, is the minimum number of differencing needed to make the series stationary. If the time series is already stationary, then d = 0. ### The meaning of q * **q** is the order of the **Moving Average (MA)** term. It refers to the number of lagged forecast errors that should go into the ARIMA Model. ## AR and MA models ### AR model In an autoregression model, we forecast the variable of interest using a linear combination of past values of the variable. The term autoregression indicates that it is a regression of the variable against itself. Thus, an autoregressive model of order p can be written as $$ \begin{equation} y_{t} = c + \phi_{1}y_{t-1} + \phi_{2}y_{t-2} + \dots + \phi_{p}y_{t-p} + \varepsilon_{t} \tag{1} \end{equation} $$ where $\epsilon_t$ is white noise. This is like a multiple regression but with lagged values of $y_t$ as predictors. We refer to this as an AR( p) model, an autoregressive model of order p. ### MA model Rather than using past values of the forecast variable in a regression, a moving average model uses past forecast errors in a regression-like model, $$ \begin{equation} y_{t} = c + \varepsilon_t + \theta_{1}\varepsilon_{t-1} + \theta_{2}\varepsilon_{t-2} + \dots + \theta_{q}\varepsilon_{t-q} \tag{2} \end{equation} $$ where $\epsilon_t$ is white noise. We refer to this as an MA(q) model, a moving average model of order q. Of course, we do not observe the values of\ $\epsilon_t$ , so it is not really a regression in the usual sense. Notice that each value of yt can be thought of as a weighted moving average of the past few forecast errors (although the coefficients will not normally sum to one). However, moving average models should not be confused with the moving average smoothing . A moving average model is used for forecasting future values, while moving average smoothing is used for estimating the trend-cycle of past values. Thus, we have discussed AR and MA Models respectively. ## ARIMA model If we combine differencing with autoregression and a moving average model, we obtain a non-seasonal ARIMA model. ARIMA is an acronym for AutoRegressive Integrated Moving Average (in this context, “integration” is the reverse of differencing). The full model can be written as where $y'_{t}$ is the differenced series (it may have been differenced more than once). The “predictors” on the right hand side include both lagged values of $y_t$ and lagged errors. We call this an ARIMA(p,d,q) model, where | | | | - | ------------------------------------- | | p | order of the autoregressive part | | d | degree of first differencing involved | | q | order of the moving average part | The same stationarity and invertibility conditions that are used for autoregressive and moving average models also apply to an ARIMA model. Many of the models we have already discussed are special cases of the ARIMA model, as shown in Table | Model | p d q | Differenced | Method | | -------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | Arima(0,0,0) | 0 0 0 | $y_t=Y_t$ | White noise | | ARIMA (0,1,0) | 0 1 0 | $y_t = Y_t - Y_{t-1}$ | Random walk | | ARIMA (0,2,0) | 0 2 0 | $y_t = Y_t - 2Y_{t-1} + Y_{t-2}$ | Constant | | ARIMA (1,0,0) | 1 0 0 | $\hat Y_t = \mu + \Phi_1 Y_{t-1} + \epsilon$ | AR(1): AR(1): First-order regression model | | ARIMA (2, 0, 0) | 2 0 0 | $\hat Y_t = \Phi_0 + \Phi_1 Y_{t-1} + \Phi_2 Y_{t-2} + \epsilon$ | AR(2): Second-order regression model | | ARIMA (1, 1, 0) | 1 1 0 | $\hat Y_t = \mu + Y_{t-1} + \Phi_1 (Y_{t-1}- Y_{t-2})$ | Differenced first-order | | autoregressive model | | | | | ARIMA (0, 1, 1) | 0 1 1 | $\hat Y_t = Y_{t-1} - \Phi_1 e^{t-1}$ | Simple exponential | | smoothing | | | | | ARIMA (0, 0, 1) | 0 0 1 | $\hat Y_t = \mu_0+ \epsilon_t - \omega_1 \epsilon_{t-1}$ | MA(1): First-order | | regression model | | | | | ARIMA (0, 0, 2) | 0 0 2 | $\hat Y_t = \mu_0+ \epsilon_t - \omega_1 \epsilon_{t-1} - \omega_2 \epsilon_{t-2}$ | MA(2): Second-order | | regression model | | | | | ARIMA (1, 0, 1) | 1 0 1 | $\hat Y_t = \Phi_0 + \Phi_1 Y_{t-1}+ \epsilon_t - \omega_1 \epsilon_{t-1}$ | ARMA model | | ARIMA (1, 1, 1) | 1 1 1 | $\Delta Y_t = \Phi_1 Y_{t-1} + \epsilon_t - \omega_1 \epsilon_{t-1}$ | ARIMA model | | ARIMA (1, 1, 2) | 1 1 2 | $\hat Y_t = Y_{t-1} + \Phi_1 (Y_{t-1} - Y_{t-2} )- \Theta_1 e_{t-1} - \Theta_1 e_{t-1}$ Damped-trend linear Exponential smoothing | | | ARIMA (0, 2, 1) OR (0,2,2) | 0 2 1 | $\hat Y_t = 2 Y_{t-1} - Y_{t-2} - \Theta_1 e_{t-1} - \Theta_2 e_{t-2}$ | Linear exponential smoothing | Once we start combining components in this way to form more complicated models, it is much easier to work with the backshift notation. For example, the above equation can be written in backshift notation as ### ARIMA model in words Predicted Yt = Constant + Linear combination Lags of Y (upto p lags) + Linear Combination of Lagged forecast errors (upto q lags) ## How to find the order of differencing (d) in ARIMA model * As stated earlier, the purpose of differencing is to make the time series stationary. But we should be careful to not over-difference the series. An over differenced series may still be stationary, which in turn will affect the model parameters. * So we should determine the right order of differencing. The right order of differencing is the minimum differencing required to get a near-stationary series which roams around a defined mean and the ACF plot reaches to zero fairly quick. * If the autocorrelations are positive for many number of lags (10 or more), then the series needs further differencing. On the other hand, if the lag 1 autocorrelation itself is too negative, then the series is probably over-differenced. * If we can’t really decide between two orders of differencing, then we go with the order that gives the least standard deviation in the differenced series. * Now, we will explain these concepts with the help of an example as follows: * First, I will check if the series is stationary using the **Augmented Dickey Fuller test (ADF Test)**, from the statsmodels package. The reason being is that we need differencing only if the series is non-stationary. Else, no differencing is needed, that is, d=0. * The null hypothesis (Ho) of the ADF test is that the time series is non-stationary. So, if the p-value of the test is less than the significance level (0.05) then we reject the null hypothesis and infer that the time series is indeed stationary. * So, in our case, if P Value > 0.05 we go ahead with finding the order of differencing. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt plt.style.use('fivethirtyeight') plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#212946', 'axes.facecolor': '#212946', 'savefig.facecolor':'#212946', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#2A3459', 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read data ```python theme={null} import pandas as pd import numpy as np df = pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/Esperanza_vida.csv", usecols=[1,2]) df.head() ``` | | year | value | | - | ---------- | --------- | | 0 | 1960-01-01 | 69.123902 | | 1 | 1961-01-01 | 69.760244 | | 2 | 1962-01-01 | 69.149756 | | 3 | 1963-01-01 | 69.248049 | | 4 | 1964-01-01 | 70.311707 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ---------- | --------- | ---------- | | 0 | 1960-01-01 | 69.123902 | 1 | | 1 | 1961-01-01 | 69.760244 | 1 | | 2 | 1962-01-01 | 69.149756 | 1 | | 3 | 1963-01-01 | 69.248049 | 1 | | 4 | 1964-01-01 | 70.311707 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y float64 unique_id object dtype: object ``` We need to convert `ds` from the `object` type to datetime. ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore data with the plot method Plot a series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` Looking at the plot we can observe there is an upward trend over the period of time. ```python theme={null} df["y"].plot(kind='kde',figsize = (16,5)) df["y"].describe() ``` ```text theme={null} count 60.000000 mean 76.632439 std 4.495279 ... 50% 76.895122 75% 80.781098 max 83.346341 Name: y, Length: 8, dtype: float64 ``` ### Seasonal Decomposed How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose ``` ```python theme={null} decomposed=seasonal_decompose(df["y"], model = "add", period=1) decomposed.plot() plt.show() ``` ### The Augmented Dickey-Fuller Test An Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis. Augmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis. Null Hypothesis: Time Series is non-stationary. It gives a time-dependent trend. Alternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time. ADF or t Statistic \< critical values: Reject the null hypothesis, time series is stationary. ADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary. ```python theme={null} from statsmodels.tsa.stattools import adfuller ``` ```python theme={null} def Augmented_Dickey_Fuller_Test_func(series , column_name): print (f'Dickey-Fuller test results for columns: {column_name}') dftest = adfuller(series, autolag='AIC') dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used']) for key,value in dftest[4].items(): dfoutput['Critical Value (%s)'%key] = value print (dfoutput) if dftest[1] <= 0.05: print("Conclusion:====>") print("Reject the null hypothesis") print("The data is stationary") else: print("Conclusion:====>") print("The null hypothesis cannot be rejected") print("The data is not stationary") ``` ```python theme={null} Augmented_Dickey_Fuller_Test_func(df["y"],"Life expectancy") ``` ```text theme={null} Dickey-Fuller test results for columns: Life expectancy Test Statistic -1.578590 p-value 0.494339 No Lags Used 2.000000 ... Critical Value (1%) -3.550670 Critical Value (5%) -2.913766 Critical Value (10%) -2.594624 Length: 7, dtype: float64 Conclusion:====> The null hypothesis cannot be rejected The data is not stationary ``` We can see in the result that we obtained the non-stationary series, because the p-value is greater than 5%. One of the objectives of applying the ADF test is to know if our series is stationary, knowing the result of the ADF test, then we can determine the next step. For our case, it can be seen from the previous result that the time series is not stationary, so we will proceed to the next step, which is to differentiate our time series. We are going to create a copy of our data, with the objective of investigating to find the stationarity in our time series. Once we have made the copy of the time series, we are going to differentiate the time series, and then we will use the augmented Dickey Fuller test to investigate if our time series is stationary. ```python theme={null} df1=df.copy() df1['y_diff'] = df['y'].diff() df1.dropna(inplace=True) df1.head() ``` | | ds | y | unique\_id | y\_diff | | - | ---------- | --------- | ---------- | --------- | | 1 | 1961-01-01 | 69.760244 | 1 | 0.636341 | | 2 | 1962-01-01 | 69.149756 | 1 | -0.610488 | | 3 | 1963-01-01 | 69.248049 | 1 | 0.098293 | | 4 | 1964-01-01 | 70.311707 | 1 | 1.063659 | | 5 | 1965-01-01 | 70.171707 | 1 | -0.140000 | Let’s apply the Dickey Fuller test again to find out if our time series is already stationary. ```python theme={null} Augmented_Dickey_Fuller_Test_func(df1["y_diff"],"Life expectancy") ``` ```text theme={null} Dickey-Fuller test results for columns: Life expectancy Test Statistic -8.510100e+00 p-value 1.173776e-13 No Lags Used 1.000000e+00 ... Critical Value (1%) -3.550670e+00 Critical Value (5%) -2.913766e+00 Critical Value (10%) -2.594624e+00 Length: 7, dtype: float64 Conclusion:====> Reject the null hypothesis The data is stationary ``` We can observe in the previous result that now if our time series is stationary, the p-value is less than 5%. Now our time series is stationary, that is, we have only differentiated 1 time, therefore, the order of our parameter $d=1$. ```python theme={null} from statsmodels.graphics.tsaplots import plot_acf, plot_pacf import matplotlib.pyplot as plt fig, axes = plt.subplots(2, 2, ) axes[0, 0].plot(df1["y"]); axes[0, 0].set_title('Original Series') plot_acf(df1["y"], ax=axes[0, 1],lags=20) axes[1, 0].plot(df1["y"].diff()); axes[1, 0].set_title('1st Order Differencing') plot_acf(df1["y"].diff().dropna(), ax=axes[1, 1],lags=20) plt.show() ``` * For the above data, we can see that the time series reaches stationarity with one orders of differencing. ## How to find the order of the AR term (p) * The next step is to identify if the model needs any AR terms. We will find out the required number of AR terms by inspecting the **Partial Autocorrelation (PACF) plot**. * **Partial autocorrelation** can be imagined as the correlation between the series and its lag, after excluding the contributions from the intermediate lags. So, PACF sort of conveys the pure correlation between a lag and the series. This way, we will know if that lag is needed in the AR term or not. * Partial autocorrelation of lag (k) of a series is the coefficient of that lag in the autoregression equation of $Y$. $Yt = \alpha0 + \alpha1 Y{t-1} + \alpha2 Y{t-2} + \alpha3 Y{t-3}$ * That is, suppose, if $Y_t$ is the current series and $Y_{t-1}$ is the lag 1 of $Y$, then the partial autocorrelation of lag 3 $(Y_{t-3})$ is the coefficient $\alpha_3$ of $Y_{t-3}$ in the above equation. * Now, we should find the number of AR terms. Any autocorrelation in a stationarized series can be rectified by adding enough AR terms. So, we initially take the order of AR term to be equal to as many lags that crosses the significance limit in the PACF plot. ```python theme={null} fig, axes = plt.subplots(1, 2) axes[0].plot(df1["y"].diff()); axes[0].set_title('1st Differencing') axes[1].set(ylim=(0,5)) plot_pacf(df1["y"].diff().dropna(), ax=axes[1],lags=20) plt.show() ``` * We can see that the PACF lag 1 is quite significant since it is well above the significance line. So, we will fix the value of p as 1. ## How to find the order of the MA term (q) * Just like how we looked at the PACF plot for the number of AR terms, we will look at the ACF plot for the number of MA terms. An MA term is technically, the error of the lagged forecast. * The ACF tells how many MA terms are required to remove any autocorrelation in the stationarized series. * Let’s see the autocorrelation plot of the differenced series. ```python theme={null} from statsmodels.graphics.tsaplots import plot_acf fig, axes = plt.subplots(1, 2) axes[0].plot(df1["y"].diff()); axes[0].set_title('1st Differencing') axes[1].set(ylim=(0,1.2)) plot_acf(df["y"].diff().dropna(), ax=axes[1], lags=20) plt.show() ``` * We can see that couple of lags are well above the significance line. So, we will fix q as 1. If there is any doubt, we will go with the simpler model that sufficiently explains the Y. ## How to handle if a time series is slightly under or over differenced * It may happen that the time series is slightly under differenced. Differencing it one more time makes it slightly over-differenced. * If the series is slightly under differenced, adding one or more additional AR terms usually makes it up. Likewise, if it is slightly over-differenced, we will try adding an additional MA term. ## Implementation of ARIMA with StatsForecast Now, we have determined the values of p, d and q. We have everything needed to fit the ARIMA model. We will use the ARIMA() implementation in the `statsforecast` package. The parameters found are: \* For the autoregressive model, $p=1$ \* for the moving average model $q=1$ \* and for the stationarity of the model with a differential with an order $d=1$ Therefore, the model that we are going to test is the ARIMA(1,1,1) model. ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import ARIMA ``` ### Instantiate the model ```python theme={null} sf = StatsForecast(models=[ARIMA(order=(1, 1, 1))], freq='YS') ``` ### Fit the Model ```python theme={null} sf.fit(df) ``` ```text theme={null} StatsForecast(models=[ARIMA]) ``` ### Making the predictions ```python theme={null} y_hat = sf.predict(h=6) y_hat ``` | | unique\_id | ds | ARIMA | | - | ---------- | ---------- | --------- | | 0 | 1 | 2020-01-01 | 83.206903 | | 1 | 1 | 2021-01-01 | 83.203508 | | 2 | 1 | 2022-01-01 | 83.204742 | | 3 | 1 | 2023-01-01 | 83.204293 | | 4 | 1 | 2024-01-01 | 83.204456 | | 5 | 1 | 2025-01-01 | 83.204397 | We can make the predictions by adding the confidence interval, for example with 95%. ```python theme={null} y_hat2 = sf.predict(h=6, level=[95]) y_hat2 ``` | | unique\_id | ds | ARIMA | ARIMA-lo-95 | ARIMA-hi-95 | | - | ---------- | ---------- | --------- | ----------- | ----------- | | 0 | 1 | 2020-01-01 | 83.206903 | 82.412336 | 84.001469 | | 1 | 1 | 2021-01-01 | 83.203508 | 82.094625 | 84.312391 | | 2 | 1 | 2022-01-01 | 83.204742 | 81.848344 | 84.561139 | | 3 | 1 | 2023-01-01 | 83.204293 | 81.640430 | 84.768156 | | 4 | 1 | 2024-01-01 | 83.204456 | 81.457145 | 84.951767 | | 5 | 1 | 2025-01-01 | 83.204397 | 81.291297 | 85.117497 | ### forecast method Memory efficient predictions. This method avoids memory burden due from object storage. It is analogous to fit\_predict without storing information. It assumes you know the forecast horizon in advance. ```python theme={null} Y_hat_df = sf.forecast(df=df, h=6, level=[95]) Y_hat_df ``` | | unique\_id | ds | ARIMA | ARIMA-lo-95 | ARIMA-hi-95 | | - | ---------- | ---------- | --------- | ----------- | ----------- | | 0 | 1 | 2020-01-01 | 83.206903 | 82.412336 | 84.001469 | | 1 | 1 | 2021-01-01 | 83.203508 | 82.094625 | 84.312391 | | 2 | 1 | 2022-01-01 | 83.204742 | 81.848344 | 84.561139 | | 3 | 1 | 2023-01-01 | 83.204293 | 81.640430 | 84.768156 | | 4 | 1 | 2024-01-01 | 83.204456 | 81.457145 | 84.951767 | | 5 | 1 | 2025-01-01 | 83.204397 | 81.291297 | 85.117497 | Once the predictions have been generated we can perform a visualization to see the generated behavior of our model. ```python theme={null} sf.plot(df, Y_hat_df, level=[95]) ``` ## Model Evaluation The commonly used accuracy metrics to judge forecasts are: 1. Mean Absolute Percentage Error (MAPE) 2. Mean Error (ME) 3. Mean Absolute Error (MAE) 4. Mean Percentage Error (MPE) 5. Root Mean Squared Error (RMSE) 6. Correlation between the Actual and the Forecast (corr) ```python theme={null} Y_train_df = df[df.ds<='2013-01-01'] Y_test_df = df[df.ds>'2013-01-01'] Y_train_df.shape, Y_test_df.shape ``` ```text theme={null} ((54, 3), (6, 3)) ``` ```python theme={null} Y_hat_df = sf.forecast(df=Y_train_df, h=len(Y_test_df)) ``` ```python theme={null} import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( Y_test_df.merge(Y_hat_df), metrics=[ufl.mse, ufl.mae, ufl.rmse, ufl.mape], ) ``` | | unique\_id | metric | ARIMA | | - | ---------- | ------ | -------- | | 0 | 1 | mse | 0.184000 | | 1 | 1 | mae | 0.397932 | | 2 | 1 | rmse | 0.428952 | | 3 | 1 | mape | 0.004785 | ## References 1. [Nixtla ARIMA API](../../src/core/models.html#arima) 2. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). # AutoARIMA Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/autoarima.html > Step-by-step guide on using the `AutoARIMA Model` with > `Statsforecast`. The objective of the following article is to obtain a step-by-step guide on building the Arima model using `AutoARIMA` with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation`. The text in this article is largely taken from [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”.](https://otexts.com/fpp3/tscv.html) ## Table of Contents * [What is AutoArima with StatsForecast?](#introduction) * [Definition of the Arima model](#arima) * [Advantages of using AutoArima](#advantages) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of AutoARIMA with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## What is AutoArima with StatsForecast? An autoARIMA is a time series model that uses an automatic process to select the optimal ARIMA (Autoregressive Integrated Moving Average) model parameters for a given time series. ARIMA is a widely used statistical model for modeling and predicting time series. The process of automatic parameter selection in an autoARIMA model is performed using statistical and optimization techniques, such as the Akaike Information Criterion (AIC) and cross-validation, to identify optimal values for autoregression, integration, and moving average parameters. of the ARIMA model. Automatic parameter selection is useful because it can be difficult to determine the optimal parameters of an ARIMA model for a given time series without a thorough understanding of the underlying stochastic process that generates the time series. The autoARIMA model automates the parameter selection process and can provide a fast and effective solution for time series modeling and forecasting. The `statsforecast.models` library brings the `AutoARIMA` function from Python provides an implementation of autoARIMA that allows to automatically select the optimal parameters for an ARIMA model given a time series. ## Definition of the Arima model An Arima model (autoregressive integrated moving average) process is the combination of an autoregressive process AR(p), integration I(d), and the moving average process MA(q). Just like the ARMA process, the ARIMA process states that the present value is dependent on past values, coming from the AR(p) portion, and past errors, coming from the MA(q) portion. However, instead of using the original series, denoted as yt, the ARIMA process uses the differenced series, denoted as $y'_{t}$. Note that $y'_{t}$ can represent a series that has been differenced more than once. Therefore, the mathematical expression of the ARIMA(p,d,q) process states that the present value of the differenced series $y'_{t}$ is equal to the sum of a constant $C$, past values of the differenced series $\phi_{p}y'_{t-p}$, the mean of the differenced series $\mu$, past error terms $\theta_{q}\varepsilon_{t-q}$, and a current error term $\varepsilon_{t}$, as shown in equation where $y'_{t}$ is the differenced series (it may have been differenced more than once). The “predictors” on the right hand side include both lagged values of $y_{t}$ and lagged errors. We call this an **ARIMA( p,d,q)** model, where | | | | - | ------------------------------------- | | p | order of the autoregressive part | | d | degree of first differencing involved | | q | order of the moving average part | The same stationarity and invertibility conditions that are used for autoregressive and moving average models also apply to an ARIMA model. Many of the models we have already discussed are special cases of the ARIMA model, as shown in Table | Model | p d q | Differenced | Method | | -------------------------- | ----- | --------------------------------------------------------------------------------------- | -------------------------------------------- | | Arima(0,0,0) | 0 0 0 | $y_t=Y_t$ | White noise | | ARIMA (0,1,0) | 0 1 0 | $y_t = Y_t - Y_{t-1}$ | Random walk | | ARIMA (0,2,0) | 0 2 0 | $y_t = Y_t - 2Y_{t-1} + Y_{t-2}$ | Constant | | ARIMA (1,0,0) | 1 0 0 | $\hat Y_t = \mu + \Phi_1 Y_{t-1} + \epsilon$ | AR(1): AR(1): First-order regression model | | ARIMA (2, 0, 0) | 2 0 0 | $\hat Y_t = \Phi_0 + \Phi_1 Y_{t-1} + \Phi_2 Y_{t-2} + \epsilon$ | AR(2): Second-order regression model | | ARIMA (1, 1, 0) | 1 1 0 | $\hat Y_t = \mu + Y_{t-1} + \Phi_1 (Y_{t-1}- Y_{t-2})$ | Differenced first-order autoregressive model | | ARIMA (0, 1, 1) | 0 1 1 | $\hat Y_t = Y_{t-1} - \Phi_1 e^{t-1}$ | Simple exponential smoothing | | ARIMA (0, 0, 1) | 0 0 1 | $\hat Y_t = \mu_0+ \epsilon_t - \omega_1 \epsilon_{t-1}$ | MA(1): First-order regression model | | ARIMA (0, 0, 2) | 0 0 2 | $\hat Y_t = \mu_0+ \epsilon_t - \omega_1 \epsilon_{t-1} - \omega_2 \epsilon_{t-2}$ | MA(2): Second-order regression model | | ARIMA (1, 0, 1) | 1 0 1 | $\hat Y_t = \Phi_0 + \Phi_1 Y_{t-1}+ \epsilon_t - \omega_1 \epsilon_{t-1}$ | ARMA model | | ARIMA (1, 1, 1) | 1 1 1 | $\Delta Y_t = \Phi_1 Y_{t-1} + \epsilon_t - \omega_1 \epsilon_{t-1}$ | ARIMA model | | ARIMA (1, 1, 2) | 1 1 2 | $\hat Y_t = Y_{t-1} + \Phi_1 (Y_{t-1} - Y_{t-2} )- \Theta_1 e_{t-1} - \Theta_1 e_{t-1}$ | Damped-trend linear Exponential smoothing | | ARIMA (0, 2, 1) OR (0,2,2) | 0 2 1 | $\hat Y_t = 2 Y_{t-1} - Y_{t-2} - \Theta_1 e_{t-1} - \Theta_2 e_{t-2}$ | Linear exponential smoothing | Once we start combining components in this way to form more complicated models, it is much easier to work with the backshift notation. For example, Equation (1) can be written in backshift notation as: Selecting appropriate values for p, d and q can be difficult. However, the `AutoARIMA()` function from `statsforecast` will do it for you automatically. For more information [here](https://otexts.com/fpp3/non-seasonal-arima.html) ## Loading libraries and data Using an `AutoARIMA()` model to model and predict time series has several advantages, including: 1. Automation of the parameter selection process: The `AutoARIMA()` function automates the ARIMA model parameter selection process, which can save the user time and effort by eliminating the need to manually try different combinations of parameters. 2. Reduction of prediction error: By automatically selecting optimal parameters, the **ARIMA** model can improve the accuracy of predictions compared to manually selected **ARIMA** models. 3. Identification of complex patterns: The `AutoARIMA()` function can identify complex patterns in the data that may be difficult to detect visually or with other time series modeling techniques. 4. Flexibility in the choice of the parameter selection methodology: The **ARIMA** Model can use different methodologies to select the optimal parameters, such as the Akaike Information Criterion (AIC), cross-validation and others, which allows the user to choose the methodology that best suits their needs. In general, using the `AutoARIMA()` function can help improve the efficiency and accuracy of time series modeling and forecasting, especially for users who are inexperienced with manual parameter selection for ARIMA models. ### Main results We compared accuracy and speed against [pmdarima](https://github.com/alkaline-ml/pmdarima), Rob Hyndman’s [forecast](https://github.com/robjhyndman/forecast) package and Facebook’s [Prophet](https://github.com/facebook/prophet). We used the `Daily`, `Hourly` and `Weekly` data from the [M4 competition](https://www.sciencedirect.com/science/article/pii/S0169207019301128). The following table summarizes the results. As can be seen, our `auto_arima` is the best model in accuracy (measured by the `MASE` loss) and time, even compared with the original implementation in R. | dataset | metric | auto\_arima\_nixtla | auto\_arima\_pmdarima \[1] | auto\_arima\_r | prophet | | :------ | :----- | ------------------: | -------------------------: | -------------: | ------: | | Daily | MASE | **3.26** | 3.35 | 4.46 | 14.26 | | Daily | time | **1.41** | 27.61 | 1.81 | 514.33 | | Hourly | MASE | **0.92** | — | 1.02 | 1.78 | | Hourly | time | **12.92** | — | 23.95 | 17.27 | | Weekly | MASE | **2.34** | 2.47 | 2.58 | 7.29 | | Weekly | time | 0.42 | 2.92 | **0.22** | 19.82 | \[1] The model `auto_arima` from `pmdarima` had a problem with Hourly data. An issue was opened. The following table summarizes the data details. | group | n\_series | mean\_length | std\_length | min\_length | max\_length | | :----- | --------: | -----------: | ----------: | ----------: | ----------: | | Daily | 4,227 | 2,371 | 1,756 | 107 | 9,933 | | Hourly | 414 | 901 | 127 | 748 | 1,008 | | Weekly | 359 | 1,035 | 707 | 93 | 2,610 | ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import numpy as np import pandas as pd import scipy.stats as stats ``` ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf from statsmodels.graphics.tsaplots import plot_pacf plt.style.use('fivethirtyeight') plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#212946', 'axes.facecolor': '#212946', 'savefig.facecolor':'#212946', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#2A3459', 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Loading Data ```python theme={null} df = pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/candy_production.csv") df.head() ``` | | observation\_date | IPG3113N | | - | ----------------- | -------- | | 0 | 1972-01-01 | 85.6945 | | 1 | 1972-02-01 | 71.8200 | | 2 | 1972-03-01 | 66.0229 | | 3 | 1972-04-01 | 64.5645 | | 4 | 1972-05-01 | 65.0100 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ---------- | ------- | ---------- | | 0 | 1972-01-01 | 85.6945 | 1 | | 1 | 1972-02-01 | 71.8200 | 1 | | 2 | 1972-03-01 | 66.0229 | 1 | | 3 | 1972-04-01 | 64.5645 | 1 | | 4 | 1972-05-01 | 65.0100 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y float64 unique_id object dtype: object ``` We need to convert `ds` from the `object` type to datetime. ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore data with the plot method Plot a series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=60, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=60, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* multiplicative Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ## Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "add", period=12) a.plot(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `AutoArima` model 2\. Data to test our model For the test data we will use the last 12 months to test and evaluate the performance of our model. ```python theme={null} Y_train_df = df[df.ds<='2016-08-01'] Y_test_df = df[df.ds>'2016-08-01'] ``` ```python theme={null} Y_train_df.shape, Y_test_df.shape ``` ```text theme={null} ((536, 3), (12, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(Y_train_df,x="ds", y="y", label="Train") sns.lineplot(Y_test_df, x="ds", y="y", label="Test") plt.show() ``` # Implementation of AutoArima with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoARIMA from statsforecast.arima import arima_string ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/)) by the master, Rob Hyndmann, can be useful.season\_length ```python theme={null} season_length = 12 # Monthly data horizon = len(Y_test_df) # number of predictions models = [AutoARIMA(season_length=season_length)] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See panda’s available frequencies.) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='MS') ``` ### Fit the Model ```python theme={null} sf.fit(df=Y_train_df) ``` ```text theme={null} StatsForecast(models=[AutoARIMA]) ``` Once we have entered our model, we can use the `arima_string` function to see the parameters that the model has found. ```python theme={null} arima_string(sf.fitted_[0,0].model_) ``` ```text theme={null} 'ARIMA(4,0,3)(0,1,1)[12] ' ``` The automation process gave us that the best model found is a model of the form `ARIMA(4,0,3)(0,1,1)[12]`, this means that our model contains $p=4$ , that is, it has a non-seasonal autogressive element, on the other hand, our model contains a seasonal part, which has an order of $D=1$, that is, it has a seasonal differential, and $q=3$ that contains 3 moving average element. To know the values of the terms of our model, we can use the following statement to know all the result of the model made. ```python theme={null} result=sf.fitted_[0,0].model_ print(result.keys()) print(result['arma']) ``` ```text theme={null} dict_keys(['coef', 'sigma2', 'var_coef', 'mask', 'loglik', 'aic', 'arma', 'residuals', 'code', 'n_cond', 'nobs', 'model', 'bic', 'aicc', 'ic', 'xreg', 'x', 'lambda']) (4, 3, 0, 1, 12, 0, 1) ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("residuals"), columns=["residual Model"]) residual ``` | | residual Model | | --- | -------------- | | 0 | 0.085694 | | 1 | 0.071820 | | 2 | 0.066022 | | ... | ... | | 533 | 1.615486 | | 534 | -0.394285 | | 535 | -6.733548 | ```python theme={null} fig, axs = plt.subplots(nrows=2, ncols=2) # plot[1,1] residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); # plot sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); # plot stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') # plot plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` To generate forecasts we only have to use the predict method specifying the forecast horizon (h). In addition, to calculate prediction intervals associated to the forecasts, we can include the parameter level that receives a list of levels of the prediction intervals we want to build. In this case we will only calculate the 90% forecast interval (level=\[90]). ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like `ARIMA` and `Theta`) ```python theme={null} Y_hat_df = sf.forecast(df=Y_train_df, h=horizon, fitted=True) Y_hat_df.head() ``` | | unique\_id | ds | AutoARIMA | | - | ---------- | ---------- | ---------- | | 0 | 1 | 2016-09-01 | 111.235874 | | 1 | 1 | 2016-10-01 | 124.948376 | | 2 | 1 | 2016-11-01 | 125.401639 | | 3 | 1 | 2016-12-01 | 123.854826 | | 4 | 1 | 2017-01-01 | 110.439451 | ```python theme={null} values=sf.forecast_fitted_values() values ``` | | unique\_id | ds | y | AutoARIMA | | --- | ---------- | ---------- | -------- | ---------- | | 0 | 1 | 1972-01-01 | 85.6945 | 85.608806 | | 1 | 1 | 1972-02-01 | 71.8200 | 71.748180 | | 2 | 1 | 1972-03-01 | 66.0229 | 65.956878 | | ... | ... | ... | ... | ... | | 533 | 1 | 2016-06-01 | 102.4044 | 100.788914 | | 534 | 1 | 2016-07-01 | 102.9512 | 103.345485 | | 535 | 1 | 2016-08-01 | 104.6977 | 111.431248 | Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=Y_train_df, h=12, level=[95]) ``` | | unique\_id | ds | AutoARIMA | AutoARIMA-lo-95 | AutoARIMA-hi-95 | | --- | ---------- | ---------- | ---------- | --------------- | --------------- | | 0 | 1 | 2016-09-01 | 111.235874 | 104.140621 | 118.331128 | | 1 | 1 | 2016-10-01 | 124.948376 | 116.244661 | 133.652090 | | 2 | 1 | 2016-11-01 | 125.401639 | 115.882093 | 134.921185 | | ... | ... | ... | ... | ... | ... | | 9 | 1 | 2017-06-01 | 98.304446 | 85.884572 | 110.724320 | | 10 | 1 | 2017-07-01 | 99.630306 | 87.032356 | 112.228256 | | 11 | 1 | 2017-08-01 | 105.426708 | 92.639159 | 118.214258 | ```python theme={null} Y_hat_df = Y_test_df.merge(Y_hat_df, how='left', on=['unique_id', 'ds']) fig, ax = plt.subplots(1, 1, figsize = (18, 7)) plot_df = pd.concat([Y_train_df, Y_hat_df]).set_index('ds') plot_df[['y', 'AutoARIMA']].plot(ax=ax, linewidth=2) ax.set_title(' Forecast', fontsize=22) ax.set_ylabel('Monthly ', fontsize=20) ax.set_xlabel('Timestamp [t]', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=12) ``` | | unique\_id | ds | AutoARIMA | | --- | ---------- | ---------- | ---------- | | 0 | 1 | 2016-09-01 | 111.235874 | | 1 | 1 | 2016-10-01 | 124.948376 | | 2 | 1 | 2016-11-01 | 125.401639 | | ... | ... | ... | ... | | 9 | 1 | 2017-06-01 | 98.304446 | | 10 | 1 | 2017-07-01 | 99.630306 | | 11 | 1 | 2017-08-01 | 105.426708 | ```python theme={null} forecast_df = sf.predict(h=12, level = [80, 95]) forecast_df ``` | | unique\_id | ds | AutoARIMA | AutoARIMA-lo-95 | AutoARIMA-lo-80 | AutoARIMA-hi-80 | AutoARIMA-hi-95 | | --- | ---------- | ---------- | ---------- | --------------- | --------------- | --------------- | --------------- | | 0 | 1 | 2016-09-01 | 111.235874 | 104.140621 | 106.596537 | 115.875211 | 118.331128 | | 1 | 1 | 2016-10-01 | 124.948376 | 116.244661 | 119.257323 | 130.639429 | 133.652090 | | 2 | 1 | 2016-11-01 | 125.401639 | 115.882093 | 119.177142 | 131.626136 | 134.921185 | | ... | ... | ... | ... | ... | ... | ... | ... | | 9 | 1 | 2017-06-01 | 98.304446 | 85.884572 | 90.183527 | 106.425365 | 110.724320 | | 10 | 1 | 2017-07-01 | 99.630306 | 87.032356 | 91.392949 | 107.867663 | 112.228256 | | 11 | 1 | 2017-08-01 | 105.426708 | 92.639159 | 97.065379 | 113.788038 | 118.214258 | We can join the forecast result with the historical data using the pandas function `pd.concat()`, and then be able to use this result for graphing. ```python theme={null} df_plot=pd.concat([df, forecast_df]).set_index('ds').tail(220) df_plot ``` | | y | unique\_id | AutoARIMA | AutoARIMA-lo-95 | AutoARIMA-lo-80 | AutoARIMA-hi-80 | AutoARIMA-hi-95 | | ---------- | -------- | ---------- | ---------- | --------------- | --------------- | --------------- | --------------- | | ds | | | | | | | | | 2000-05-01 | 108.7202 | 1 | NaN | NaN | NaN | NaN | NaN | | 2000-06-01 | 114.2071 | 1 | NaN | NaN | NaN | NaN | NaN | | 2000-07-01 | 111.8737 | 1 | NaN | NaN | NaN | NaN | NaN | | ... | ... | ... | ... | ... | ... | ... | ... | | 2017-06-01 | NaN | 1 | 98.304446 | 85.884572 | 90.183527 | 106.425365 | 110.724320 | | 2017-07-01 | NaN | 1 | 99.630306 | 87.032356 | 91.392949 | 107.867663 | 112.228256 | | 2017-08-01 | NaN | 1 | 105.426708 | 92.639159 | 97.065379 | 113.788038 | 118.214258 | Now let’s visualize the result of our forecast and the historical data of our time series, also let’s draw the confidence interval that we have obtained when making the prediction with 95% confidence. ```python theme={null} sf.plot(df, forecast_df, level=[95], max_insample_length=12 * 5) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=5)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=Y_train_df, h=12, step_size=12, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df.head() ``` | | unique\_id | ds | cutoff | y | AutoARIMA | | - | ---------- | ---------- | ---------- | -------- | ---------- | | 0 | 1 | 2011-09-01 | 2011-08-01 | 93.9062 | 105.235606 | | 1 | 1 | 2011-10-01 | 2011-08-01 | 116.7634 | 118.739813 | | 2 | 1 | 2011-11-01 | 2011-08-01 | 116.8258 | 114.572924 | | 3 | 1 | 2011-12-01 | 2011-08-01 | 114.9563 | 114.991219 | | 4 | 1 | 2012-01-01 | 2011-08-01 | 99.9662 | 100.133142 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( Y_test_df.merge(Y_hat_df), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=Y_train_df, ) ``` | | unique\_id | metric | AutoARIMA | | - | ---------- | ------ | --------- | | 0 | 1 | mae | 5.012894 | | 1 | 1 | mape | 0.045046 | | 2 | 1 | mase | 0.967601 | | 3 | 1 | rmse | 5.680362 | | 4 | 1 | smape | 0.022673 | ## References 1. [Nixtla AutoARIMA API](../../src/core/models.html#autoarima) 2. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). # AutoCES Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/autoces.html > Step-by-step guide on using the `AutoCES Model` with `Statsforecast`. The objective of the following article is to obtain a step-by-step guide on building the CES model using `AutoCES` with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Ivan Svetunkov, Nikolaos Kourentzes, John Keith Ord, “Complex exponential smoothing”](https://onlinelibrary.wiley.com/doi/full/10.1002/nav.22074) 2\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Complex Exponential Smoothing](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of AutoCES with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction Exponential smoothing has been one of the most popular forecasting methods used to support various decisions in organizations, in activities such as inventory management, scheduling, revenue management, and other areas. Although its relative simplicity and transparency have made it very attractive for research and practice, identifying the underlying trend remains challenging with significant impact on the resulting accuracy. This has resulted in the development of various modifications of trend models, introducing a model selection problem. With the aim of addressing this problem, we propose the complex exponential smoothing (CES), based on the theory of functions of complex variables. The basic CES approach involves only two parameters and does not require a model selection procedure. Despite these simplifications, CES proves to be competitive with, or even superior to existing methods. We show that CES has several advantages over conventional exponential smoothing models: it can model and forecast both stationary and non-stationary processes, and CES can capture both level and trend cases, as defined in the conventional exponential smoothing classification. CES is evaluated on several forecasting competition datasets, demonstrating better performance than established benchmarks. We conclude that CES has desirable features for time series modeling and opens new promising avenues for research. # Complex Exponential Smoothing ### Method and model Using the complex valued representation of time series, we propose the CES in analogy to the conventional exponential smoothing methods. Consider the simple exponential smoothing method: $$ \begin{equation} {\hat{y}}_t=\alpha {y}_{t-1}+\left(1-\alpha \right){\hat{y}}_{t-1} \tag{1} \end{equation} $$ where $\alpha$ is the smoothing parameter and ${\hat{y}}_t$ is the estimated value of series. The same method can be represented as a weighted average of previous actual observations if we substitute ${\hat{y}}_{t-1}$ by the formula (1) with an index instead of (Brown, 1956\): $$ \begin{equation} {\hat{y}}_t=\alpha \sum \limits_{j=1}^{t-1}{\left(1-\alpha \right)}^{j-1}{y}_{t-j} \tag{2} \end{equation} $$ The idea of this representation is to demonstrate how the weights $\alpha {\left(1-\alpha \right)}^{j-1}$ are distributed over time in our sample. If the smoothing parameter $\alpha \in \left(0,1\right)$ then the weights decline exponentially with the increase of . If it lies in the so called “admissible bounds” (Brenner et al., 1968), that is $\alpha \in \left(0,2\right)$ then the weights decline in oscillating manner. Both traditional and admissible bounds have been used efficiently in practice and in academic literature (for application of the latter see for example Gardner & Diaz-Saiz, 2008; Snyder et al., 2017\). However, in real life the distribution of weights can be more complex, with harmonic rather than exponential decline, meaning that some of the past observation might have more importance than the recent ones. In order to implement such distribution of weights, we build upon (2) and introduce complex dynamic interactions by substituting the real variables with the complex ones in (2). First, we substitute ${y}_{t-j}$ by the complex variable ${y}_{t-j}+{ie}_{t-j}$, where ${e}_t$ is the error term of the model and $i$ is the imaginary unit (which satisfies the equation ${i}^2=-1$). The idea behind this is to have the impact of both actual values and the error on each observation in the past on the final forecast. Second, we substitute $\alpha$ with a complex variable ${\alpha}_0+i{\alpha}_1$ and 1 by $1+i$ to introduce the harmonically declining weights. Depending on the values of the complex smoothing parameter, the weights distribution will exhibit a variety of trajectories over time, including exponential, oscillating, and harmonic. Finally, the result of multiplication of two complex numbers will be another complex number, so we substitute ${\hat{y}}_{t-j}$ with ${\hat{y}}_{t-j}+i{\hat{e}}_{t-j}$, where ${\hat{e}}_{t-j}$ is the proxy for the error term. The CES obtained as a result of this can be written as: $$ \begin{equation} {\hat{y}}_t+i{\hat{e}}_t=\left({\alpha}_0+i{\alpha}_1\right)\sum \limits_{j=1}^{t-1}{\left(1+i-\left({\alpha}_0+i{\alpha}_1\right)\right)}^{j-1}\left({y}_{t-j}+{ie}_{t-j}\right) \tag{3} \end{equation} $$ Having arrived to the model with harmonically distributed weights, we can now move to the shorter form by substituting ${\displaystyle \begin{array}{cc}& {\hat{y}}_{t-1}+i{\hat{e}}_{t-1}\\ {}& \kern1em =\left({\alpha}_0+i{\alpha}_1\right)\sum \limits_{j=2}^{t-1}{\left(1+i-\left({\alpha}_0+i{\alpha}_1\right)\right)}^{j-1}\left({y}_{t-j}+{ie}_{t-j}\right)\end{array}}$ in (3) to get: $$ \begin{equation} {\displaystyle \begin{array}{cc}{\hat{y}}_t+i{\hat{e}}_t& =\left({\alpha}_0+i{\alpha}_1\right)\left({y}_{t-1}+{ie}_{t-1}\right)\\ {}& \kern1em +\left(1-{\alpha}_0+i-i{\alpha}_1\right)\left({\hat{y}}_{t-1}+i{\hat{e}}_{t-1}\right).\end{array}} \tag 4 \end{equation} $$ Note that ${\hat{e}}_t$ is not interesting for the time series analysis and forecasting purposes, but is used as a vessel containing the information about the previous errors of the method. Having the complex variables instead of the real ones in (4), allows taking the exponentially weighted values of both actuals and the forecast errors. By changing the value of ${\alpha}_0+i{\alpha}_1$ , we can regulate what proportions of the actual and the forecast error should be carried out to the future in order to produce forecasts. Representing the complex-valued function as a system of two real-valued functions leads to: $$ \begin{equation} {\displaystyle \begin{array}{ll}& {\hat{y}}_t=\left({\alpha}_0{y}_{t-1}+\left(1-{\alpha}_0\right){\hat{y}}_{t-1}\right)-\left({\alpha}_1{e}_{t-1}+\left(1-{\alpha}_1\right){\hat{e}}_{t-1}\right)\\ {}& {\hat{e}}_t=\left({\alpha}_1{y}_{t-1}+\left(1-{\alpha}_1\right){\hat{y}}_{t-1}\right)+\left({\alpha}_0{e}_{t-1}+\left(1-{\alpha}_0\right){\hat{e}}_{t-1}\right).\end{array}} \tag 5 \end{equation} $$ CES introduces an interaction between the real and imaginary parts, and the equations in (6) are connected via the previous values of each other, causing interactions over time, defined by complex smoothing parameter value. But the method itself is restrictive and does not allow easily producing prediction intervals and deriving the likelihood function. It is also important to understand what sort of statistical model underlies CES. This model can be written in the following state space form: $$ \begin{equation} {\displaystyle \begin{array}{ll}& {y}_t={l}_{t-1}+{\epsilon}_t\\ {}& {l}_t={l}_{t-1}-\left(1-{\alpha}_1\right){c}_{t-1}+\left({\alpha}_0-{\alpha}_1\right){\epsilon}_t\\ {}& {c}_t={l}_{t-1}+\left(1-{\alpha}_0\right){c}_{t-1}+\left({\alpha}_0+{\alpha}_1\right){\epsilon}_t,\end{array}} \tag 6 \end{equation} $$ where ${\epsilon}_t$ is the white noise error term, ${l}_t$ is the level component and ${c}_t$ is the nonlinear trend component at observation . Observe that dependencies in time series have an interactive structure and no explicit trend component is present in the time series as this model does not need to artificially break the series into level and trend, as ETS does. Although we call the ${c}_t$ component as “nonlinear trend,” it does not correspond to the conventional trend component, because it contains the information of both previous ${c}_{t-1}$ and the level ${l}_{t-1}$. Also, note that we use ${\epsilon}_t$ instead of ${e}_t$ in (6), which means that the CES has (6) as an underlying statistical model only when there is no misspecification error. In the case of the estimation of this model, the ${\epsilon}_t$ will be substituted by ${e}_t$, which will then lead us to the original formulation (4). This idea allows rewriting (6) in a shorter more generic way, resembling the general single source of error (SSOE) state space framework: $$ \begin{equation} {\displaystyle \begin{array}{ll}& {y}_t={\mathbf{w}}^{\prime }{\mathbf{v}}_{t-1}+{\epsilon}_t\\ {}& {\mathbf{v}}_t={\mathbf{Fv}}_{t-1}+\mathbf{g}{\epsilon}_t,\end{array}} \tag 7 \end{equation} $$ where ${\mathbf{v}}_t=\left(\begin{array}{c}{l}_t\\ {}{c}_t\end{array}\right)$ is the state vector $\mathbf{F}=\left(\begin{array}{cc}1& -\left(1-{\alpha}_1\right)\\ {}1& 1-{\alpha}_0\end{array}\right)$ is the transition matrix $\mathbf{g}=\left(\begin{array}{c}{\alpha}_0-{\alpha}_1\\ {}{\alpha}_0+{\alpha}_1\end{array}\right)$ is the persistence vector and $\mathbf{w}=\left(\begin{array}{c}1\\ {}0\end{array}\right)$ is the measurement vector. The state space form (7) permits extending CES in a similar ways to ETS to include additional states for seasonality or exogenous variables. The main difference between model (7) and the conventional ETS is that the transition matrix in (7) includes smoothing parameters which is not a standard feature of ETS models. Furthermore persistence vector includes the interaction of complex smoothing parameters, rather than smoothing parameters themselves. The error term in (6) is additive, so the likelihood function for CES is trivial and is similar to the one in the additive exponential smoothing models (Hyndman et al., 2008, p. 68): $$ \begin{equation} \mathrm{\mathcal{L}}\left(\mathbf{g},{\mathbf{v}}_0,{\sigma}^2\mid \mathbf{Y}\right)={\left(\frac{1}{\sigma \sqrt{2\pi }}\right)}^T\exp \left(-\frac{1}{2}\sum \limits_{t=1}^T{\left(\frac{\epsilon_t}{\sigma}\right)}^2\right), \tag 8 \end{equation} $$ where ${\mathbf{v}}_0$ is the vector of initial states, ${\sigma}^2$ is the variance of the error term and $\mathbf{Y}$ is the vector of all the in-sample observations. ### Stationarity and stability conditions for CES In order to understand the properties of CES, we need to study its stationarity and stability conditions. The former holds for general exponential smoothing in the state space form (8) when all the eigenvalues of lie inside the unit circle (Hyndman et al., 2008, p. 38). CES can be either stationary or not, depending on the complex smoothing parameter value, in contrast to ETS models that are always non-stationary. Calculating eigenvalues of for CES gives the following roots: $$ \begin{equation} \lambda =\frac{2-{\alpha}_0\pm \sqrt{\alpha_0^2+4{\alpha}_1-4}}{2}. \tag 9 \end{equation} $$ If the absolute values of both roots are less than 1 then the estimated CES is stationary. When ${\alpha}_1>1$ one of the eigenvalues will always be greater than one. In this case both eigenvalues will be real numbers and CES produces a non-stationary trajectory. When ${\alpha}_1=1$ CES becomes equivalent to ETS(A,N,N). Finally, the model becomes stationary when: $$ \begin{equation} \left\{\begin{array}{l}{\alpha}_1<5-2{\alpha}_0\\ {}{\alpha}_1<1\\ {}{\alpha}_1>1-{\alpha}_0\end{array}\right. \tag {10} \end{equation} $$ Note that we are not restricting CES with the conditions (10), we merely show, how the model will behave depending on the value of the complex smoothing parameter. This property of CES means that it is able to model either stationary or non-stationary processes, without the need to switch between them. The property of CES for each separate time series depends on the value of the smoothing parameters. The other important property that arises from (7) is the stability condition for CES. With $\epsilon_t={y}_t-{l}_{t-1}$ the following is obtained: $$ \begin{equation} {\displaystyle \begin{array}{ll}{y}_t& ={l}_{t-1}+{\epsilon}_t\\ {}\left(\begin{array}{c}{l}_t\\ {}{c}_t\end{array}\right)& =\left(\begin{array}{cc}1-{\alpha}_0+{\alpha}_1& -\left(1-{\alpha}_1\right)\\ {}1-{\alpha}_0-{\alpha}_1& 1-{\alpha}_0\end{array}\right)\left(\begin{array}{c}{l}_{t-1}\\ {}{c}_{t-1}\end{array}\right)\\ {}& \kern1em +\left(\begin{array}{c}{\alpha}_0-{\alpha}_1\\ {}{\alpha}_1+{\alpha}_0\end{array}\right){y}_t.\end{array}} \tag {11} \end{equation} $$ The matrix $\mathbf{D}=\left(\begin{array}{cc}1-{\alpha}_0+{\alpha}_1& -\left(1-{\alpha}_1\right)\\ {}1-{\alpha}_0-{\alpha}_1& 1-{\alpha}_0\end{array}\right)$ is called the discount matrix and can be written in the general form: $$ \begin{equation} \mathbf{D}=\mathbf{F}-\mathbf{g}{\mathbf{w}}^{\prime }. \tag {12} \end{equation} $$ The model is said to be stable if all the eigenvalues of (12) lie inside the unit circle. This is more important condition than the stationarity for the model, because it ensures that the complex weights decline over time and that the older observations have smaller weights than the new ones, which is one of the main features of the conventional ETS models. The eigenvalues are given by the following formula: $$ \begin{equation} \lambda =\frac{2-2{\alpha}_0+{\alpha}_1\pm \sqrt{8{\alpha}_1+4{\alpha}_0-4{\alpha}_0{\alpha}_1-4-3{\alpha}_1^2}}{2}. \tag {13} \end{equation} $$ CES will be stable when the following system of inequalities is satisfied: $$ \begin{equation} \left\{\begin{array}{l}{\left({\alpha}_0-2.5\right)}^2+{\alpha}_1^2>1.25\\ {}{\left({\alpha}_0-0.5\right)}^2+{\left({\alpha}_1-1\right)}^2>0.25\\ {}{\left({\alpha}_0-1.5\right)}^2+{\left({\alpha}_1-0.5\right)}^2<1.5\end{array}.\right. \tag {14} \end{equation} $$ Both the stationarity and stability regions are shown in [Figure 1](https://onlinelibrary.wiley.com/cms/asset/2a1bdd28-bbba-4ec3-aa4a-fbcf3823a209/nav22074-fig-0001-m.jpg). The stationarity region (10) corresponds to the triangle. All the combinations of smoothing parameters lying below the curve in the triangle will produce the stationary harmonic trajectories, while the rest lead to the exponential trajectories. The stability condition (14) corresponds to the dark region. The stability region intersects the stationarity region, but in general stable CES can produce both stationary and non-stationary forecasts ### Conditional mean and variance of CES The conditional mean of CES for $h$ steps ahead with known ${l}_t$ and ${c}_t$ can be calculated using the state space model (6): $$ \begin{equation} \mathrm{E}\left({y}_{t+h}\mid {\mathbf{v}}_t\right)={\mathbf{w}}^{\prime }{\mathbf{F}}^{h-1}{\mathbf{v}}_t, \tag {15} \end{equation} $$ where $\mathrm{E}\left({y}_{t+h}\mid {\mathbf{v}}_t\right)={\hat{y}}_{t+h}$ while $\mathbf{F}$ and $\mathbf{w}$ re the matrices from (7). The forecasting trajectories of (15) will differ depending on the values of ${l}_t, {c}_t$, and the complex smoothing parameter. The analysis of stationarity condition shows that there are several types of forecasting trajectories of CES depending on the particular value of the complex smoothing parameter: 1. When ${\alpha}_1=1$ all the values of forecast will be equal to the last obtained forecast, which corresponds to a flat line. This trajectory is shown in [Figure 2A](https://onlinelibrary.wiley.com/cms/asset/16feeb7e-adf2-48f6-9df9-cab3e34b6e67/nav22074-fig-0002-m.jpg). 2. When ${\alpha}_1>1$ the model produces trajectory with exponential growth which is shown in [Figure 2B](https://onlinelibrary.wiley.com/cms/asset/16feeb7e-adf2-48f6-9df9-cab3e34b6e67/nav22074-fig-0002-m.jpg). 3. When $\frac{4-{\alpha}_0^2}{4}<{\alpha}_1<1$ trajectory becomes stationary and CES produces exponential decline shown in [Figure 2C](https://onlinelibrary.wiley.com/cms/asset/16feeb7e-adf2-48f6-9df9-cab3e34b6e67/nav22074-fig-0002-m.jpg). 4. When $1-{\alpha}_0<{\alpha}_1<\frac{4-{\alpha}_0^2}{4}$ trajectory becomes harmonic and will converge to zero [Figure 2D](https://onlinelibrary.wiley.com/cms/asset/16feeb7e-adf2-48f6-9df9-cab3e34b6e67/nav22074-fig-0002-m.jpg). 5. Finally, when $0<{\alpha}_1<1-{\alpha}_0$ the diverging harmonic trajectory is produced, the model becomes non-stationary. This trajectory is of no use in forecasting, that is why we do not show it on graphs. Using (7) the conditional variance of CES for $h$ steps ahead with known ${l}_t$ and ${c}_t$ can be calculated similarly to the pure additive ETS models (Hyndman et al., 2008, p. 96). ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import pandas as pd import scipy.stats as stats ``` ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf from statsmodels.graphics.tsaplots import plot_pacf plt.style.use('fivethirtyeight') plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#212946', 'axes.facecolor': '#212946', 'savefig.facecolor':'#212946', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#2A3459', 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} df = pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/Esperanza_vida.csv", usecols=[1,2]) df.head() ``` | | year | value | | - | ---------- | --------- | | 0 | 1960-01-01 | 69.123902 | | 1 | 1961-01-01 | 69.760244 | | 2 | 1962-01-01 | 69.149756 | | 3 | 1963-01-01 | 69.248049 | | 4 | 1964-01-01 | 70.311707 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ---------- | --------- | ---------- | | 0 | 1960-01-01 | 69.123902 | 1 | | 1 | 1961-01-01 | 69.760244 | 1 | | 2 | 1962-01-01 | 69.149756 | 1 | | 3 | 1963-01-01 | 69.248049 | 1 | | 4 | 1964-01-01 | 70.311707 | 1 | Now, let’s now check the last few rows of our time series using the `.tail()` function. ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y float64 unique_id object dtype: object ``` We need to convert the `ds` from `object` type to datetime. ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=20, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); # Grafico plot_pacf(df["y"], lags=20, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') #plt.savefig("Gráfico de Densidad y qq") plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "add", period=1) a.plot(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our model. 2. Data to test our model. For the test data we will use the last 12 months to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2013-01-01'] test = df[df.ds>'2013-01-01'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((54, 3), (6, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train") sns.lineplot(test, x="ds", y="y", label="Test") plt.show() ``` ## Implementation of AutoCES with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoCES ``` ### Instantiate Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/)) by the master, Rob Hyndmann, can be useful `season_length` **Note** Automatically selects the best `Complex Exponential Smoothing model` using an information criterion. Default is Akaike Information Criterion (AICc), while particular models are estimated using maximum likelihood. The state-space equations can be determined based on their $S$ simple, $P$ parial, $Z$ optimized or $N$ ommited components. The model string parameter defines the kind of CES model: $N$ for simple CES (withous seasonality), $S$ for simple seasonality (lagged CES), $P$ for partial seasonality (without complex part), $F$ for full seasonality (lagged CES with real and complex seasonal parts). If the component is selected as $Z$, it operates as a placeholder to ask the AutoCES model to figure out the best parameter. ```python theme={null} season_length = 1 # year data horizon = len(test) # number of predictions # We call the model that we are going to use models = [AutoCES(season_length=season_length)] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='YS') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[CES]) ``` ```python theme={null} result=sf.fitted_[0,0].model_ print(result.keys()) print(result['fit']) ``` ```text theme={null} dict_keys(['loglik', 'aic', 'bic', 'aicc', 'mse', 'amse', 'fit', 'fitted', 'residuals', 'm', 'states', 'par', 'n', 'seasontype', 'sigma2', 'actual_residuals']) results(x=array([1.63706552, 1.00511519]), fn=76.78049826760919, nit=27, simplex=array([[1.63400329, 1.00510199], [1.63706552, 1.00511519], [1.63638944, 1.00512037]])) ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("residuals"), columns=["residual Model"]) residual ``` | | residual Model | | --- | -------------- | | 0 | -0.727729 | | 1 | 0.144552 | | 2 | -0.762086 | | ... | ... | | 51 | -0.073258 | | 52 | -0.234578 | | 53 | 0.109990 | ```python theme={null} fig, axs = plt.subplots(nrows=2, ncols=2) # plot[1,1] residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); # plot sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); # plot stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') # plot plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like `ARIMA` and `Theta`) ```python theme={null} # Prediction Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | CES | | - | ---------- | ---------- | --------- | | 0 | 1 | 2014-01-01 | 82.906075 | | 1 | 1 | 2015-01-01 | 83.166687 | | 2 | 1 | 2016-01-01 | 83.424744 | | 3 | 1 | 2017-01-01 | 83.685760 | | 4 | 1 | 2018-01-01 | 83.946213 | | 5 | 1 | 2019-01-01 | 84.208359 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | CES | | - | ---------- | ---------- | --------- | --------- | | 0 | 1 | 1960-01-01 | 69.123902 | 69.851631 | | 1 | 1 | 1961-01-01 | 69.760244 | 69.615692 | | 2 | 1 | 1962-01-01 | 69.149756 | 69.911842 | | 3 | 1 | 1963-01-01 | 69.248049 | 69.657822 | | 4 | 1 | 1964-01-01 | 70.311707 | 69.601196 | ```python theme={null} StatsForecast.plot(values) ``` Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | CES | CES-lo-95 | CES-hi-95 | | - | ---------- | ---------- | --------- | --------- | --------- | | 0 | 1 | 2014-01-01 | 82.906075 | 82.342483 | 83.454016 | | 1 | 1 | 2015-01-01 | 83.166687 | 82.604029 | 83.717271 | | 2 | 1 | 2016-01-01 | 83.424744 | 82.858573 | 83.975870 | | 3 | 1 | 2017-01-01 | 83.685760 | 83.118946 | 84.239582 | | 4 | 1 | 2018-01-01 | 83.946213 | 83.376905 | 84.501133 | | 5 | 1 | 2019-01-01 | 84.208359 | 83.637738 | 84.765408 | ```python theme={null} # Merge the forecasts with the true values Y_hat = test.merge(Y_hat, how='left', on=['unique_id', 'ds']) Y_hat ``` | | ds | y | unique\_id | CES | | - | ---------- | --------- | ---------- | --------- | | 0 | 2014-01-01 | 83.090244 | 1 | 82.906075 | | 1 | 2015-01-01 | 82.543902 | 1 | 83.166687 | | 2 | 2016-01-01 | 83.243902 | 1 | 83.424744 | | 3 | 2017-01-01 | 82.946341 | 1 | 83.685760 | | 4 | 2018-01-01 | 83.346341 | 1 | 83.946213 | | 5 | 2019-01-01 | 83.197561 | 1 | 84.208359 | ```python theme={null} sf.plot(train, Y_hat) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | CES | | - | ---------- | ---------- | --------- | | 0 | 1 | 2014-01-01 | 82.906075 | | 1 | 1 | 2015-01-01 | 83.166687 | | 2 | 1 | 2016-01-01 | 83.424744 | | 3 | 1 | 2017-01-01 | 83.685760 | | 4 | 1 | 2018-01-01 | 83.946213 | | 5 | 1 | 2019-01-01 | 84.208359 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[95]) forecast_df ``` | | unique\_id | ds | CES | CES-lo-95 | CES-hi-95 | | - | ---------- | ---------- | --------- | --------- | --------- | | 0 | 1 | 2014-01-01 | 82.906075 | 82.342483 | 83.454016 | | 1 | 1 | 2015-01-01 | 83.166687 | 82.604029 | 83.717271 | | 2 | 1 | 2016-01-01 | 83.424744 | 82.858573 | 83.975870 | | 3 | 1 | 2017-01-01 | 83.685760 | 83.118946 | 84.239582 | | 4 | 1 | 2018-01-01 | 83.946213 | 83.376905 | 84.501133 | | 5 | 1 | 2019-01-01 | 84.208359 | 83.637738 | 84.765408 | Now let’s visualize the result of our forecast and the historical data of our time series, also let’s draw the confidence interval that we have obtained when making the prediction with 95% confidence. ```python theme={null} sf.plot(train, test.merge(forecast_df), level=[95]) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=5)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=train, h=horizon, step_size=12, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df.head() ``` | | unique\_id | ds | cutoff | y | CES | | - | ---------- | ---------- | ---------- | --------- | --------- | | 0 | 1 | 1984-01-01 | 1983-01-01 | 75.389512 | 74.952705 | | 1 | 1 | 1985-01-01 | 1983-01-01 | 75.470732 | 75.161736 | | 2 | 1 | 1986-01-01 | 1983-01-01 | 75.770732 | 75.377945 | | 3 | 1 | 1987-01-01 | 1983-01-01 | 76.219512 | 75.590378 | | 4 | 1 | 1988-01-01 | 1983-01-01 | 76.370732 | 75.806343 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( Y_hat, metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | CES | | - | ---------- | ------ | -------- | | 0 | 1 | mae | 0.556314 | | 1 | 1 | mape | 0.006699 | | 2 | 1 | mase | 1.770512 | | 3 | 1 | rmse | 0.630183 | | 4 | 1 | smape | 0.003336 | ## References 1. [Nixtla AutoCES API](../../src/core/models.html#autoces) 2. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 3. [Ivan Svetunkov, Nikolaos Kourentzes, John Keith Ord, “Complex exponential smoothing”](https://onlinelibrary.wiley.com/doi/full/10.1002/nav.22074) # AutoETS Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/autoets.html > Step-by-step guide on using the `AutoETS Model` with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [ETS Models](#model) * [ETS Estimation](#estimation) * [Model Selection](#selection) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of AutoETS with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction Automatic forecasts of large numbers of univariate time series are often needed in business. It is common to have over one thousand product lines that need forecasting at least monthly. Even when a smaller number of forecasts are required, there may be nobody suitably trained in the use of time series models to produce them. In these circumstances, an automatic forecasting algorithm is an essential tool. Automatic forecasting algorithms must determine an appropriate time series model, estimate the parameters and compute the forecasts. They must be robust to unusual time series patterns, and applicable to large numbers of series without user intervention. The most popular automatic forecasting algorithms are based on either exponential smoothing or ARIMA models. # Exponential smoothing Although exponential smoothing methods have been around since the 1950s, a modelling framework incorporating procedures for model selection was not developed until relatively recently. `Ord, Koehler`, and `Snyder (1997), Hyndman, Koehler, Snyder, and Grose (2002)` and `Hyndman, Koehler, Ord`, and `Snyder (2005b)` have shown that all exponential smoothing methods (including non-linear methods) are optimal forecasts from innovations state space models. Exponential smoothing methods were originally classified by `Pegels’ (1969) taxonomy`. This was later extended by `Gardner (1985), modified by Hyndman et al. (2002)`, and extended again by `Taylor (2003)`, giving a total of fifteen methods seen in the following table. | Trend Component | Seasonal Component | | --------------- | ------------------ | | Component | N(None)) | A (Additive) | M (Multiplicative) | | -------------------------- | -------- | ------------ | ------------------ | | N (None) | (N,N) | (N,A) | (N,M) | | A (Additive) | (A,N) | (A,A) | (A,M) | | Ad (Additive damped) | (Ad,N) | (Ad,A) | (Ad,M) | | M (Multiplicative) | (M,N ) | (M,A ) | (M,M) | | Md (Multiplicative damped) | (Md,N ) | ( Md,A) | (Md,M) | Some of these methods are better known under other names. For example, cell `(N,N)` describes the **simple exponential smoothing (or SES) method**, cell `(A,N)` describes **Holt’s linear method**, and cell `(Ad,N)` describes **the damped trend method**. The **additive Holt-Winters’ method** is given by cell `(A,A)` and the **multiplicative Holt-Winters’ method** is given by cell `(A,M)`. The other cells correspond to less commonly used but analogous methods. ### Point forecasts for all methods We denote the observed time series by $y_1,y_2,...,y_n$. A forecast of $y_{t+h}$ based on all of the data up to time $t$ is denoted by $\hat y_{t+h|t}$. To illustrate the method, we give the point forecasts and updating equation for method `(A,A)`, the he Holt-Winters’ additive method: where $m$ is the length of seasonality (e.g., the number of months or quarters in a year), $\ell_{t}$ represents the level of the series, $b_t$ denotes the growth, $s_t$ is the seasonal component, $\hat y_{t+h|t}$ is the forecast for $h$ periods ahead, and $h_{m}^{+} = [(h − 1) mod \ m] + 1$. To use method (1), we need values for the initial states $\ell_{0}$, $b_0$ and $s_{1−m}, . . . , s_0$, and for the smoothing parameters $\alpha, \beta^{*}$ and $\gamma$. All of these will be estimated from the observed data. Equation (1c) is slightly different from the usual Holt-Winters equations such as those in Makridakis et al. (1998) or Bowerman, O’Connell, and Koehler (2005). These authors replace (1c) with $s_{t} = \gamma^* (y_{t}-\ell_{t})+ (1-\gamma^*)s_{t-m}.$ If $\ell_{t}$ is substituted using (1a), we obtain $s_{t} = \gamma^*(1-\alpha) (y_{t}-\ell_{t-1}-b_{t-1})+ [1-\gamma^*(1-\alpha)]s_{t-m},$ Thus, we obtain identical forecasts using this approach by replacing $\gamma$ in (1c) with $\gamma^{*} (1-\alpha)$. The modification given in (1c) was proposed by Ord et al. (1997) to make the state space formulation simpler. It is equivalent to Archibald’s (1990) variation of the Holt-Winters’ method. ### Innovations state space models For each exponential smoothing method in [Other ETS models](https://otexts.com/fpp3/ets.html) , Hyndman et al. (2008b) describe two possible innovations state space models, one corresponding to a model with additive errors and the other to a model with multiplicative errors. If the same parameter values are used, these two models give equivalent point forecasts, although different prediction intervals. Thus there are 30 potential models described in this classification. Historically, the nature of the error component has often been ignored, because the distinction between additive and multiplicative errors makes no difference to point forecasts. We are careful to distinguish exponential smoothing methods from the underlying state space models. An exponential smoothing method is an algorithm for producing point forecasts only. The underlying stochastic state space model gives the same point forecasts, but also provides a framework for computing prediction intervals and other properties. To distinguish the models with additive and multiplicative errors, we add an extra letter to the front of the method notation. The triplet `(E,T,S)` refers to the three components: error, trend and seasonality. So the model `ETS(A,A,N)` has additive errors, additive trend and no seasonality—in other words, this is Holt’s linear method with additive errors. Similarly, `ETS(M,Md,M)` refers to a model with multiplicative errors, a damped multiplicative trend and multiplicative seasonality. The notation `ETS(·,·,·)` helps in remembering the order in which the components are specified. Once a model is specified, we can study the probability distribution of future values of the series and find, for example, the conditional mean of a future observation given knowledge of the past. We denote this as $\mu_{t+h|t} = E(y_{t+h | xt})$, where xt contains the unobserved components such as $\ell_t$, $b_t$ and $s_t$. For $h = 1$ we use $\mu_t ≡ \mu_{t+1|t}$ as a shorthand notation. For many models, these conditional means will be identical to the point forecasts given in Table [Other ETS models](https://otexts.com/fpp3/ets.html), so that $\mu_{t+h|t} = \hat y_{t+h|t}$. However, for other models (those with multiplicative trend or multiplicative seasonality), the conditional mean and the point forecast will differ slightly for $h ≥ 2$. We illustrate these ideas using the damped trend method of Gardner and McKenzie (1985). Each model consists of a measurement equation that describes the observed data, and some state equations that describe how the unobserved components or states (level, trend, seasonal) change over time. Hence, these are referred to as state space models. For each method there exist two models: one with additive errors and one with multiplicative errors. The point forecasts produced by the models are identical if they use the same smoothing parameter values. They will, however, generate different prediction intervals. To distinguish between a model with additive errors and one with multiplicative errors (and also to distinguish the models from the methods), we add a third letter to the classification of in the above Table. We label each state space model as `ETS(⋅,.,.)` for (Error, Trend, Seasonal). This label can also be thought of as ExponenTial Smoothing. Using the same notation as in the above Table, the possibilities for each component (or state) are: `Error ={ A,M }`, `Trend ={N,A,Ad}` and `Seasonal ={ N,A,M }`. ### **ETS(A,N,N): simple exponential smoothing with additive errors** Recall the component form of simple exponential smoothing: If we re-arrange the smoothing equation for the level, we get the “error correction” form, where $e_{t}=y_{t}-\ell_{t-1}=y_{t}-\hat{y}_{t|t-1}$ s the residual at time $t$. The training data errors lead to the adjustment of the estimated level throughout the smoothing process for $t=1,\dots,T$. For example, if the error at time $t$ is negative, then $y_t < \hat{y}_{t|t-1}$ and so the level at time $t-1$ has been over-estimated. The new level $\ell_{t}$ is then the previous level $\ell_{t-1}$ adjusted downwards. The closer $\alpha$ is to one, the “rougher” the estimate of the level (large adjustments take place). The smaller the $\alpha$, the “smoother” the level (small adjustments take place). We can also write $y_t = \ell_{t-1} + e_t$, so that each observation can be represented by the previous level plus an error. To make this into an innovations state space model, all we need to do is specify the probability distribution for $e_t$. For a model with additive errors, we assume that residuals (the one-step training errors) $e_t$ are normally distributed white noise with mean 0 and variance $\sigma^2$. A short-hand notation for this is $e_t = \varepsilon_t\sim\text{NID}(0,\sigma^2)$; NID stands for “normally and independently distributed”. Then the equations of the model can be written as We refer to (2) as the measurement (or observation) equation and (3) as the state (or transition) equation. These two equations, together with the statistical distribution of the errors, form a fully specified statistical model. Specifically, these constitute an innovations state space model underlying simple exponential smoothing. The term “innovations” comes from the fact that all equations use the same random error process, $\varepsilon_t$. For the same reason, this formulation is also referred to as a “single source of error” model. There are alternative multiple source of error formulations which we do not present here. The measurement equation shows the relationship between the observations and the unobserved states. In this case, observation $y_t$ is a linear function of the level $\ell_{t-1}$, the predictable part of $y_t$, and the error $\varepsilon_t$, the unpredictable part of $y_t$. For other innovations state space models, this relationship may be nonlinear. The state equation shows the evolution of the state through time. The influence of the smoothing parameter $\alpha$ is the same as for the methods discussed earlier. For example, $\alpha$ governs the amount of change in successive levels: high values of $\alpha$ allow rapid changes in the level; low values of $\alpha$ lead to smooth changes. If $\alpha=0$, the level of the series does not change over time; if $\alpha=1$, the model reduces to a random walk model, $y_t=y_{t-1}+\varepsilon_t$. ### **ETS(M,N,N): simple exponential smoothing with multiplicative errors** In a similar fashion, we can specify models with multiplicative errors by writing the one-step-ahead training errors as relative errors $\varepsilon_t = \frac{y_t-\hat{y}_{t|t-1}}{\hat{y}_{t|t-1}}$ where $\varepsilon_t \sim \text{NID}(0,\sigma^2)$. Substituting $\hat{y}_{t|t-1}=\ell_{t-1}$ gives $y_t = \ell_{t-1}+\ell_{t-1}\varepsilon_t$ and $e_t = y_t - \hat{y}_{t|t-1} = \ell_{t-1}\varepsilon_t$. Then we can write the multiplicative form of the state space model as ### **ETS(A,A,N): Holt’s linear method with additive errors** For this model, we assume that the one-step-ahead training errors are given by $\varepsilon_t=y_t-\ell_{t-1}-b_{t-1} \sim \text{NID}(0,\sigma^2)$ Substituting this into the error correction equations for Holt’s linear method we obtain where for simplicity we have set $\beta=\alpha \beta^*$. ### **ETS(M,A,N): Holt’s linear method with multiplicative errors** Specifying one-step-ahead training errors as relative errors such that $\varepsilon_t=\frac{y_t-(\ell_{t-1}+b_{t-1})}{(\ell_{t-1}+b_{t-1})}$ and following an approach similar to that used above, the innovations state space model underlying Holt’s linear method with multiplicative errors is specified as where again $\beta=\alpha \beta^*$ and $\varepsilon_t \sim \text{NID}(0,\sigma^2)$. ## Estimating ETS models An alternative to estimating the parameters by minimising the sum of squared errors is to maximise the “likelihood”. The likelihood is the probability of the data arising from the specified model. Thus, a large likelihood is associated with a good model. For an additive error model, maximising the likelihood (assuming normally distributed errors) gives the same results as minimising the sum of squared errors. However, different results will be obtained for multiplicative error models. In this section, we will estimate the smoothing parameters $\alpha, \beta, \gamma$ and $\phi$ and the initial states $\ell_0, b_0, s_0,s_{-1},\dots,s_{-m+1}$, by maximising the likelihood. The possible values that the smoothing parameters can take are restricted. Traditionally, the parameters have been constrained to lie between 0 and 1 so that the equations can be interpreted as weighted averages. That is, $0< \alpha,\beta^*,\gamma^*,\phi<1$. For the state space models, we have set $\beta=\alpha\beta^*$ and $\gamma=(1-\alpha)\gamma^*$. Therefore, the traditional restrictions translate to $0< \alpha <1, 0 < \beta < \alpha$ and $0< \gamma < 1-\alpha$. In practice, the damping parameter $\phi$ is usually constrained further to prevent numerical difficulties in estimating the model. Another way to view the parameters is through a consideration of the mathematical properties of the state space models. The parameters are constrained in order to prevent observations in the distant past having a continuing effect on current forecasts. This leads to some admissibility constraints on the parameters, which are usually (but not always) less restrictive than the traditional constraints region `(Hyndman et al., 2008, pp. 149-161)`. For example, for the `ETS(A,N,N)` model, the traditional parameter region is $0< \alpha <1$ but the admissible region is $0< \alpha <2$. For the `ETS(A,A,N)` model, the traditional parameter region is $0<\alpha<1$ and $0<\beta<\alpha$ but the admissible region is $0<\alpha<2$ and $0<\beta<4-2\alpha$. ## Model selection A great advantage of the `ETS` statistical framework is that information criteria can be used for model selection. The `AIC, AIC_c` and `BIC`, can be used here to determine which of the `ETS` models is most appropriate for a given time series. For `ETS` models, Akaike’s Information Criterion (`AIC)` is defined as $\text{AIC} = -2\log(L) + 2k,$ where $L$ is the likelihood of the model and $k$ is the total number of parameters and initial states that have been estimated (including the residual variance). The `AIC` corrected for small sample bias `(AIC_c)` is defined as $AIC_c = AIC + \frac{2k(k+1)}{T-k-1}$ and the Bayesian Information Criterion `(BIC)` is $\text{BIC} = \text{AIC} + k[\log(T)-2]$ Three of the combinations of (Error, Trend, Seasonal) can lead to numerical difficulties. Specifically, the models that can cause such instabilities are `ETS(A,N,M), ETS(A,A,M)`, and `ETS(A,Ad,M)`, due to division by values potentially close to zero in the state equations. We normally do not consider these particular combinations when selecting a model. Models with multiplicative errors are useful when the data are strictly positive, but are not numerically stable when the data contain zeros or negative values. Therefore, multiplicative error models will not be considered if the time series is not strictly positive. In that case, only the six fully additive models will be applied. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf from statsmodels.graphics.tsaplots import plot_pacf plt.style.use('fivethirtyeight') plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#212946', 'axes.facecolor': '#212946', 'savefig.facecolor':'#212946', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#2A3459', 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} df = pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/Esperanza_vida.csv", usecols=[1,2]) df.head() ``` | | year | value | | - | ---------- | --------- | | 0 | 1960-01-01 | 69.123902 | | 1 | 1961-01-01 | 69.760244 | | 2 | 1962-01-01 | 69.149756 | | 3 | 1963-01-01 | 69.248049 | | 4 | 1964-01-01 | 70.311707 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ---------- | --------- | ---------- | | 0 | 1960-01-01 | 69.123902 | 1 | | 1 | 1961-01-01 | 69.760244 | 1 | | 2 | 1962-01-01 | 69.149756 | 1 | | 3 | 1963-01-01 | 69.248049 | 1 | | 4 | 1964-01-01 | 70.311707 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y float64 unique_id object dtype: object ``` We need to convert the `ds` from `object` type to datetime. ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ## Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=20, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); # Plot plot_pacf(df["y"], lags=20, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "add", period=1) a.plot(); ``` Breaking down a time series into its components helps us to identify the behavior of the time series we are analyzing. In addition, it helps us to know what type of models we can apply, for our example of the Life expectancy data set, we can observe that our time series shows an increasing trend throughout the year, on the other hand, it can be observed also that the time series has no seasonality. By looking at the previous graph and knowing each of the components, we can get an idea of which model we can apply: \* We have trend \* There is no seasonality ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our model. 2. Data to test our model. For the test data we will use the last 6 years to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2013-01-01'] test = df[df.ds>'2013-01-01'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((54, 3), (6, 3)) ``` ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train") sns.lineplot(test, x="ds", y="y", label="Test") plt.show() ``` ## Implementation of AutoETS with StatsForecast ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoETS ``` ### Instantiate Model ```python theme={null} sf = StatsForecast(models=[AutoETS(model="AZN")], freq='YS') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[AutoETS]) ``` ### Model Prediction ```python theme={null} y_hat = sf.predict(h=6) y_hat ``` | | unique\_id | ds | AutoETS | | - | ---------- | ---------- | --------- | | 0 | 1 | 2014-01-01 | 82.952553 | | 1 | 1 | 2015-01-01 | 83.146150 | | 2 | 1 | 2016-01-01 | 83.339747 | | 3 | 1 | 2017-01-01 | 83.533344 | | 4 | 1 | 2018-01-01 | 83.726940 | | 5 | 1 | 2019-01-01 | 83.920537 | ```python theme={null} sf.plot(train, y_hat) ``` Let’s add a confidence interval to our forecast. ```python theme={null} y_hat = sf.predict(h=6, level=[80,90,95]) y_hat ``` | | unique\_id | ds | AutoETS | AutoETS-lo-95 | AutoETS-lo-90 | AutoETS-lo-80 | AutoETS-hi-80 | AutoETS-hi-90 | AutoETS-hi-95 | | - | ---------- | ---------- | --------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | | 0 | 1 | 2014-01-01 | 82.952553 | 82.500416 | 82.573107 | 82.656916 | 83.248190 | 83.331999 | 83.404691 | | 1 | 1 | 2015-01-01 | 83.146150 | 82.693437 | 82.766221 | 82.850137 | 83.442163 | 83.526078 | 83.598863 | | 2 | 1 | 2016-01-01 | 83.339747 | 82.884744 | 82.957897 | 83.042237 | 83.637257 | 83.721597 | 83.794749 | | 3 | 1 | 2017-01-01 | 83.533344 | 83.073235 | 83.147208 | 83.232495 | 83.834192 | 83.919479 | 83.993452 | | 4 | 1 | 2018-01-01 | 83.726940 | 83.257894 | 83.333304 | 83.420247 | 84.033634 | 84.120577 | 84.195987 | | 5 | 1 | 2019-01-01 | 83.920537 | 83.437859 | 83.515461 | 83.604931 | 84.236144 | 84.325614 | 84.403216 | ```python theme={null} sf.plot(train, y_hat, level=[95]) ``` ### Forecast method Memory Efficient Exponential Smoothing predictions. This method avoids memory burden due from object storage. It is analogous to fit\_predict without storing information. It assumes you know the forecast horizon in advance. ```python theme={null} y_hat = sf.forecast(df=train, h=6, fitted=True) y_hat ``` | | unique\_id | ds | AutoETS | | - | ---------- | ---------- | --------- | | 0 | 1 | 2014-01-01 | 82.952553 | | 1 | 1 | 2015-01-01 | 83.146150 | | 2 | 1 | 2016-01-01 | 83.339747 | | 3 | 1 | 2017-01-01 | 83.533344 | | 4 | 1 | 2018-01-01 | 83.726940 | | 5 | 1 | 2019-01-01 | 83.920537 | ### In sample predictions Access fitted Exponential Smoothing insample predictions. ```python theme={null} sf.forecast_fitted_values() ``` | | unique\_id | ds | y | AutoETS | | --- | ---------- | ---------- | --------- | --------- | | 0 | 1 | 1960-01-01 | 69.123902 | 69.005305 | | 1 | 1 | 1961-01-01 | 69.760244 | 69.237346 | | 2 | 1 | 1962-01-01 | 69.149756 | 69.495763 | | ... | ... | ... | ... | ... | | 51 | 1 | 2011-01-01 | 82.187805 | 82.348633 | | 52 | 1 | 2012-01-01 | 82.239024 | 82.561938 | | 53 | 1 | 2013-01-01 | 82.690244 | 82.758963 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( y_hat.merge(test), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=1), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | AutoETS | | - | ---------- | ------ | -------- | | 0 | 1 | mae | 0.421060 | | 1 | 1 | mape | 0.005073 | | 2 | 1 | mase | 1.340056 | | 3 | 1 | rmse | 0.483558 | | 4 | 1 | smape | 0.002528 | ## References 1. [Nixtla AutoETS API](../../src/core/models.html#autoets) 2. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). # AutoRegressive Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/autoregressive.html > Step-by-step guide on using the `AutoRegressive Model` with > `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). 3\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html) ## Table of Contents * [Introduction](#introduction) * [Autoregressive Models](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of AutoRegressive with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The `autoregressive` time series model `(AutoRegressive)` is a `statistical` technique used to analyze and predict univariate time series. In essence, the `autoregressive model` is based on the idea that previous values of the time series can be used to predict future values. In this model, the dependent variable (the time series) returns to itself at different moments in time, creating a dependency relationship between past and present values. The idea is that past values can help us understand and predict future values of the series. The `autoregressive model` can be fitted to different orders, which indicate how many past values are used to predict the present value. For example, an `autoregressive model` of order 1 $(AR(1))$ uses only the immediately previous value to predict the current value, while an `autoregressive model` of order $p (AR(p))$ uses the $p$ previous values. The `autoregressive model` is one of the basic models of time series analysis and is widely used in a variety of fields, from finance and economics to meteorology and social sciences. The model’s ability to capture nonlinear dependencies in time series data makes it especially useful for forecasting and long-term trend analysis. In a `multiple regression model`, we forecast the variable of interest using a linear combination of predictors. In an `autoregression model`, we forecast the variable of interest using a `linear combination` of past values of the variable. The term `autoregression` indicates that it is a regression of the variable against itself. ## Definition of Autoregressive Models Before giving a formal definition of the ARCH model, let’s define the components of an ARCH model in a general way: * Autoregressive, a concept that we have already known, is the construction of a univariate time series model using statistical methods, which means that the current value of a variable is influenced by past values of itself in different periods. * Heteroscedasticity means that the model can have different magnitudes or variability at different time points (variance changes over time). * Conditional, since volatility is not fixed, the reference here is the constant that we put in the model to limit heteroscedasticity and make it conditionally dependent on the previous value or values of the variable. The AR model is the most basic building block of univariate time series. As you have seen before, univariate time series are a family of models that use only information about the target variable’s past to forecast its future, and do not rely on other explanatory variables. **Definition 1.** (1) The following equation is called the autoregressive model of order $p$ and denoted by $\text{AR(p)}$: $$ \begin{equation} Xt =\varphi_0 +\varphi_1X_{t−1}+\varphi_2X_{t−2}+\cdots+\varphi_p X_{t−p}+\varepsilon_t \tag 1 \end{equation} $$ where $\{\varepsilon_t \} \sim WN(0,\sigma_{\epsilon}^2)$, $E(X_s \varepsilon_t) = 0$ if $s < t$ and $\varphi_0,\varphi_1,\cdots ,\varphi_p$ are real-valued parameters (coefficients) with $\varphi_p \neq 0$. 1. If a time series $\{X_t \}$ is stationary and satisfies such an equation as (1), then we call it an $\text{AR(p)}$ process. Note the following remarks about this definition: * For simplicity, we often assume that the intercept (const term) $\varphi_0 = 0$; otherwise, we can consider $\{X_t −\mu \}$ where $\mu =\varphi_0 /(1−\varphi_1 − \cdots −\varphi_p)$. * We distinguish the concept of $\text{AR}$ models from the concept of $\text{AR}$ processes. $\text{AR}$ models may or may not be stationary and $\text{AR}$ processes must be stationary. * $E(X_s \varepsilon_t) = 0(s < t)$ means that $X_s$ in the past has nothing to do with $\varepsilon_t$ at the current time $t$. * Like the definition of MA models, sometimes εt in Eq.(1) is called the innovation or shock term. In addition, using the backshift([see](https://otexts.com/fpp3/backshift.html)) operator $B$, the $\text{AR(p)}$ model can be rewritten as $\varphi(B)X_t = \varepsilon_t$ where $\varphi(z) = 1 − \varphi_1z − \cdots − \varphi_p z^p$ is called the (corresponding) $\text{AR}$ polynomial. Besides, in the Python package |StatsModels|, $\varphi(B)$ is called the $\text{AR}$ lag polynomial. ### Definition of PACF Let $\{X_t \}$ be a stationary time series with $E(X_t) = 0$. Here the assumption $E(X_t ) = 0$ is for conciseness only. If $E(X_t) = \mu \neq 0$, it is okay to replace $\{X_t \}$ by $\{X_t − \mu \}$. Now consider the linear regression (prediction) of $X_t$ on $\{X_{t−k+1:t−1} \}$ for any integer $k ≥ 2$. We use $\hat X_t$ to denote this regression (prediction): $\hat X_t =\alpha_1 X_{t−1}+ \cdots +\alpha_{k−1} X_{t−k+1}$ where $\{\alpha_1, \cdots , \alpha_{k−1} \}$ satisfy $\{\alpha_1, \cdots , \alpha_{k−1} \}=\argmin_{β1,···,βk−1} E[X_t −(\beta_1 X_{t−1} +\cdots +\beta_{k−1}X_{t−k+1})]^2$ That is, $\{\alpha_1, \cdots , \alpha_{k−1} \}$ are chosen by minimizing the mean squared error of prediction. Similarly, let $\hat X_{t −k}$ denote the regression (prediction) of $X_{t −k}$ on $\{X_{t −k+1:t −1} \}$: $\hat X_{t−k} =\eta_1 X_{t−1}+ \cdots +\eta_{k−1} X_{t−k+1}$ Note that if $\{X_t \}$ is stationary, then $\{ \alpha_{1:k−1}\} = \{\eta_{1:k−1} \}$. Now let $\hat Z_{t−k} = X_{t−k} − \hat X_{t−k}$ and $\hat Z_t = X_t − \hat X_t$. Then $\hat Z_{t−k}$ is the residual of removing the effect of the intervening variables $\{X_{t−k+1:t−1} \}$ from $X_{t−k}$, and $\hat Z_t$ is the residual of removing the effect of $\{X_{t −k+1:t −1} \}$ from $X_t$. **Definition 2.** The partial autocorrelation function(PACF) at lag $k$ of astationary time series $\{X_t \}$ with $E(X_t ) = 0$ is $\phi_{11} = Corr(X_{t−1}, X_t ) = \frac{Cov(X_{t−1}, X_t )} {[Var(X_{t−1})Var(X_t)]^1/2}=\rho_1$ and $\phi_{kk} = Corr(\hat Z_{t−k},\hat Z_t)=\frac{Cov(\hat Z_{t−k},\hat Z_t)} {[Var(\hat Z_{t−k})Var(\hat Z_t)]^{1/2}}$ According to the property of correlation coefficient (see, e.g., P172, Casella and Berger 2002), |φkk| ≤ 1. On the other hand, the following theorem paves the way to estimate the PACF of a stationary time series, and its proof can be seen in Fan and Yao (2003). On the other hand, the following theorem paves the way to estimate the PACF of a stationary time series, and its proof can be seen in Fan and Yao (2003). **Theorem 1.** Let $\{X_t \}$ be a stationary time series with $E(X_t) = 0$, and $\{a_{1k},\cdots ,a_{kk} \}$ satisfy $\{a_{1k},\cdots,a_{kk} \}=\argmin_{a_1 ,\cdots ,a_k} E(X_{t −a1}X_{t−1}−\cdots −a_k X_{t−k})^2$ Then $\phi_{kk}=a_{kk}$ for $k≥1$. ### Properties of Autoregressive Models From the $\text{AR(p)}$ model, namely, Eq. (1), we can see that it is in the same form as the multiple linear regression model. However, it explains current itself with its own past. Given the past $\{X_{(t−p):(t−1)} \} = \{x_{(t−p):(t−1)} \}$ we have $E(X_t |X_{(t−p):(t−1)}) = \varphi_0 + \varphi_1x_{t−1} + \varphi_2 x_{t−2} + \cdots + \varphi_p x_{t−p}$ This suggests that given the past, the right-hand side of this equation is a good estimate of $X_t$ . Besides $Var(X_t |X_{(t −p):(t −1)}) = Var(\varepsilon_t ) = \sigma_{\varepsilon}^2$ Now we suppose that the AR(p) model, namely, Eq. (1), is stationary; then we have 1. The model mean $E(_Xt)=\mu =\varphi_0 / (1−\varphi_1−···−\varphi_p)$ .Thus,themodelmean $\mu=0$ if and only if $\varphi_0 =0$. 2. If the mean is zero or $\varphi_0 = 0$ ((3) and (4) below have the same assumption), noting that $E(X_t \varepsilon_t ) = \sigma_{\varepsilon}^2$ , we multiply Eq. (1) by $X_t$ , take expectations, and then get $\text {Var} (X_t) = \gamma_0 = \varphi_1 \gamma_1 + \varphi_2 \gamma2 + \cdots + \varphi_p \gamma_p + \sigma_{\varepsilon}^2$ Furthermore $\gamma_0 = \sigma_{\varepsilon}^2 / ( 1 − \varphi_1 \rho_1 − \varphi_2 \rho_2 − \cdots − \varphi_p \rho_p )$ 1. For all $k > p$, the partial autocorrelation $\phi_{kk} = 0$, that is, the PACF of $\text{AR(p)}$ models cuts off after lag $p$, which is very helpful in identifying an $\text{AR}$ model. In fact, at this point, the predictor or regression of $X_t$ on $\{X_{t−k+1:t−1} \}$ is $\hat X_t =\varphi_1 X_{t−1}+\cdots +\varphi_{k−1} X_{t−k+1}$ Thus, $X_t − \hat X_t = \varepsilon_t$. Moreover, $X_{t−k} − \hat X_{t−k}$ is a function of $\{ X_{t−k:t−1} \}$, and $\varepsilon_t$ is uncorrelated to everyone in $\{X_{t−k:t−1} \}$. Therefore $Cov(X_{t−k} −\hat X_{t−k},X_t −\hat X_t)=Cov(X_{t−k} −\hat X_{t−k},\varepsilon_t)=0.$ By Definition 2, $\phi_{kk} = 0$. 1. We multiply Eq.(1)by $X_{t−k}$,take expectations,divide by $\gamma_0$,and then obtain the recursive relationship between the autocorrelations: $$ \begin{equation} for \ k ≥ 1, \rho_k = \varphi_1 \rho_{k−1} + \varphi_2 \rho_{k−2} + \cdots + \varphi_p \rho_{k−p} \tag 2 \end{equation} $$ For Eq.(2), let $k = 1,2,··· ,p$. Then we arrive at a set of difference equations, which is known as the Yule-Walker equations. If the $\text{ACF} \{\rho_{1:p} \}$ are given, then we can solve the Yule-Walker equations to obtain the estimates for $\{\varphi_{1:p} \}$, and the solutions are called the Yule-Walker estimates. 1. Since the model is a stationary $\text{AR(p)}$ now, naturally it satisfies $X_t =\varphi_1 X_{t−1}+ \varphi_2 X_{t−2} + \cdots + \varphi_p X_{t−p} + \varepsilon_t$. Hence $\phi_{pp} = \varphi_p$. If the $\text{AR(p)}$ model is further Gaussian and a sample of size $\text{T}$ is given, then (a) $\hat \phi_{pp} → \varphi_p$ as $T → ∞$; (b) according to Quenouille (1949), for $k > p, \sqrt{T} \hat \phi_{kk}$ asymptotically follows the standard normal(Gaussian) distribution $\text{N(0,1)}$, or $\phi_{kk}$ is asymptotically distributed as $\text{N(0, 1/T )}$. ### Stationarity and Causality of AR Models Consider the AR(1) model: $$ \begin{equation} X_t = \varphi X_{t − 1} + \varepsilon_t , \varepsilon_t \sim W N( 0 , \sigma_{\varepsilon}^2 ) \tag 3 \end{equation} $$ For $|\varphi|<1$,let $X_{1t} =\sum_{j=0}^{\infty} \varphi^j \varepsilon_{t−j}$ and for $|\varphi|>1$,let $X_{2t} =− \sum_{j=1}^{\infty} \varphi^{-j} \varepsilon_{t+j}$. It is easy to show that both $\{X_{1t } \}$ and $\{X_{2t } \}$ are stationary and satisfy Eq. (3). That is, both are the stationary solution of Eq. (3). This gives rise to a question: which one of both is preferable? Obviously, $\{X_{2t } \}$ depends on future values of unobservable $\{\varepsilon_t \}$, and so it is unnatural. Hence we take $\{X_{1t } \}$ and abandon $\{X_{2t } \}$. In other words, we require that the coefficient $\varphi$ in Eq. (3) is less 1 in absolute value. At this point, the $\text{AR}(1)$ model is said to be causal and its causal expression is $X_t = \sum_{j=0}^{\infty} \varphi^j \varepsilon_{t−j}$. In general, the definition of causality is given below. **Definition 3** (1) A time series $\{X_t \}$ is causal if there exist coefficients $\psi_j$ such that $X_t =\sum_{j=0}^{\infty} \psi_j \varepsilon_{t-j}, \ \ \sum_{j=0}^{\infty} |\psi_j |< \infty$ where $\psi_0 = 1, \{\varepsilon_t \} \sim WN(0, \sigma_{\varepsilon}^2 )$. At this point, we say that the time series $\{X_t \}$ has an $\text{MA}(\infty)$ representation. 1. We say that a model is causal if the time series generated by it is causal. Causality suggests that the time series $\{X_t\}$ is caused by the white noise (or innovations) from the past up to time t . Besides, the time series $\{X_{2t } \}$ is an example that is stationary but not causal. In order to determine whether an $\text{AR}$ model is causal, similar to the invertibility for the $\text{MA}$ model, we have the following theorem. **Theorem 2(CausalityTheorem)** An $\text{AR}$ model defined by Eq.(1) is causal if and only if the roots of its $\text{AR}$ polynomial $\varphi(z)=1−\varphi_1 z− \cdots − \varphi_p z^p$ exceed 1 in modulus or lie outside the unit circle on the complex plane. Note the following remarks: \* In the light of the existence and uniqueness on page 75 of Brockwell and Davis (2016), an $\text{AR}$ model defined by Eq.(1) is stationary if and only if its $\text{AR}$ polynomial $\varphi(z)=1−\varphi_1 z− \cdots − \varphi_p z^p \neq 0$ for all $|z|=1$ or all the roots of the $\text{AR}$ polynomial do not lie on the unit circle. Hence for the AR model defined by Eq. (1), its stationarity condition is weaker than its causality condition. * A causal time series is surely a stationary one. So an $\text{AR}$ model that satisfies the causal condition is naturally stationary. But a stationary $\text{AR}$ model is not necessarily causal. * If the time series $\{X_t \}$ generated by Eq. (1) is not from the remote past, namely, $t \in T = {\cdots ,−n,\cdots ,−1,0,1,\cdots ,n,\cdots}$ but starts from an initial value $X_0$, then it may be nonstationary, not to mention causality. * According to the relationship between the roots and the coefficients of the degree 2 polynomial $\varphi(z) = 1 − \varphi_1 z − \varphi_2 z^2$, it may be proved that both of the roots of the polynomial exceed 1 in modulus if and only if Thus, we can conveniently use the three inequations to decide whether a $\text{AR(2)}$ model is causal or not. * It may be shown that for an $\text{AR(p)}$ model defined by Eq. (1), the coefficients $\{\psi_j \}$ in Definition 3 satisfy $\psi_0=1$ and $\psi_j=\sum_{k=1}^{j} \varphi '_k \psi_{j-k}, \ \ j \geq 1 \ where \ \ \varphi '_k =\varphi_k \ \ if \ \ k \leq p \ \ and \ \ \varphi '_k =0 \ \ if \ \ k>p$ ### Autocorrelation: the past influences the present The autoregressive model describes a relationship between the present of a variable and its past. Therefore, it is suitable for variables in which the past and present values are correlated. As an intuitive example, consider the waiting line at the doctor. Imagine that the doctor has a plan in which each patient has 20 minutes with him. If each patient takes exactly 20 minutes, this works well. But what if a patient takes a little longer? An autocorrelation could be present if the duration of one query has an impact on the duration of the next query. So if the doctor needs to speed up an appointment because the previous appointment took too long, look at a correlation between the past and the present. Past values influence future values. ### Positive and negative autocorrelation Like “regular” correlation, autocorrelation can be positive or negative. Positive autocorrelation means that a high value now is likely to give a high value in the next period. This can be observed, for example, in stock trading: as soon as a lot of people want to buy a stock, its price goes up. This positive trend makes people want to buy this stock even more as it has positive returns. The more people buy the stock, the higher it goes and the more people will want to buy it. A positive correlation also works in downtrends. If today’s stock value is low, tomorrow’s value is likely to be even lower as people start selling. When many people sell, the value falls, and even more people will want to sell. This is also a case of positive autocorrelation since the past and the present go in the same direction. If the past is low, the present is low; and if the past is high, the present is high. There is negative autocorrelation if two trends are opposite. This is the case in the example of the duration of the doctor’s visit. If one query takes longer, the next one will be shorter. If one visit takes less time, the doctor may take a little longer for the next one. ### Stationarity and the ADF test The problem of having a trend in our data is general in univariate time series modeling. The stationarity of a time series means that a time series does not have a (long-term) trend: it is stable around the same average. Otherwise, a time series is said to be non-stationary. In theory, AR models can have a trend coefficient in the model, but since stationarity is an important concept in general time series theory, it’s best to learn to deal with it right away. Many models can only work on stationary time series. A time series that is growing or falling strongly over time is obvious to spot. But sometimes it’s hard to tell if a time series is stationary. This is where the Augmented Dickey Fuller (ADF) test comes in handy. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import pandas as pd import scipy.stats as stats import statsmodels.api as sm import statsmodels.tsa.api as smt ``` ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('fivethirtyeight') plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#212946', 'axes.facecolor': '#212946', 'savefig.facecolor':'#212946', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#2A3459', 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} df= pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/catfish.csv") df.head() ``` | | Date | Total | | - | --------- | ----- | | 0 | 1986-1-01 | 9034 | | 1 | 1986-2-01 | 9596 | | 2 | 1986-3-01 | 10558 | | 3 | 1986-4-01 | 9002 | | 4 | 1986-5-01 | 9239 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | --------- | ----- | ---------- | | 0 | 1986-1-01 | 9034 | 1 | | 1 | 1986-2-01 | 9596 | 1 | | 2 | 1986-3-01 | 10558 | 1 | | 3 | 1986-4-01 | 9002 | 1 | | 4 | 1986-5-01 | 9239 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `ds` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints 8 random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### The Augmented Dickey-Fuller Test An Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis. Augmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis. Null Hypothesis: Time Series is non-stationary. It gives a time-dependent trend. Alternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time. ADF or t Statistic \< critical values: Reject the null hypothesis, time series is stationary. ADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary. Let’s check if our series that we are analyzing is a stationary series. Let’s create a function to check, using the `Dickey Fuller` test ```python theme={null} from statsmodels.tsa.stattools import adfuller ``` ```python theme={null} def Augmented_Dickey_Fuller_Test_func(series , column_name): print (f'Dickey-Fuller test results for columns: {column_name}') dftest = adfuller(series, autolag='AIC') dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used']) for key,value in dftest[4].items(): dfoutput['Critical Value (%s)'%key] = value print (dfoutput) if dftest[1] <= 0.05: print("Conclusion:====>") print("Reject the null hypothesis") print("The data is stationary") else: print("Conclusion:====>") print("The null hypothesis cannot be rejected") print("The data is not stationary") ``` ```python theme={null} Augmented_Dickey_Fuller_Test_func(df["y"],'Sales') ``` ```text theme={null} Dickey-Fuller test results for columns: Sales Test Statistic -1.589903 p-value 0.488664 No Lags Used 14.000000 ... Critical Value (1%) -3.451691 Critical Value (5%) -2.870939 Critical Value (10%) -2.571778 Length: 7, dtype: float64 Conclusion:====> The null hypothesis cannot be rejected The data is not stationary ``` In the previous result we can see that the `Augmented_Dickey_Fuller` test gives us a `p-value` of 0.488664, which tells us that the null hypothesis cannot be rejected, and on the other hand the data of our series are not stationary. We need to differentiate our time series, in order to convert the data to stationary. ```python theme={null} Augmented_Dickey_Fuller_Test_func(df["y"].diff().dropna(),"Sales") ``` ```text theme={null} Dickey-Fuller test results for columns: Sales Test Statistic -4.310935 p-value 0.000425 No Lags Used 17.000000 ... Critical Value (1%) -3.451974 Critical Value (5%) -2.871063 Critical Value (10%) -2.571844 Length: 7, dtype: float64 Conclusion:====> Reject the null hypothesis The data is stationary ``` By applying a differential, our time series now is stationary. ```python theme={null} def tsplot(y, lags=None, figsize=(12, 7), style='bmh'): # [3] if not isinstance(y, pd.Series): y = pd.Series(y) with plt.style.context(style): fig = plt.figure(figsize=figsize) layout = (2, 2) ts_ax = plt.subplot2grid(layout, (0, 0), colspan=2) acf_ax = plt.subplot2grid(layout, (1, 0)) pacf_ax = plt.subplot2grid(layout, (1, 1)) y.plot(ax=ts_ax) p_value = sm.tsa.stattools.adfuller(y)[1] ts_ax.set_title('Time Series Analysis plot\n Dickey-Fuller: p={0:.5f}'.format(p_value)) smt.graphics.plot_acf(y, lags=lags, ax=acf_ax) smt.graphics.plot_pacf(y, lags=lags, ax=pacf_ax) plt.tight_layout() ``` ```python theme={null} tsplot(df["y"].diff().dropna(), lags=20); ``` As you can see, based on the blue background shaded area of the graph, the PACF shows the first, second, third, fourth, sixth, seventh, ninth, and tenth etc. delay outside the shaded area. This means that it would be interesting to also include these lags in the AR model. ### How many lags should we include? Now, the **big question in time series analysis is always how many lags to include**. This is called the order of the time series. The notation is AR(1) for order 1 and AR(p) for order p. The order is up to you. Theoretically speaking, you can base your order on the PACF chart. Theory tells you to take the number of lags before you get an autocorrelation of 0. All other lags should be 0. In theory, you often see great charts where the first peak is very high and the rest equal zero. In those cases, the choice is easy: you are working with a very “pure” example of AR(1). Another common case is when your autocorrelation starts high and slowly decreases to zero. In this case, you should use all delays where the PACF is not yet zero. However, in practice, it is not always that simple. Remember the famous saying *“all models are wrong, but some are useful”*. It is very rare to find cases that fit an AR model perfectly. In general, the autoregression process can help explain part of the variation of a variable, but not all. In practice, you will try to select the number of lags that gives your model the best predictive performance. The best predictive performance is often not defined by looking at autocorrelation plots: those plots give you a theoretical estimate. However, predictive performance is best defined by model evaluation and benchmarking, using the techniques you have seen in Module 2. Later in this module, we will see how to use model evaluation to choose a performance order for the AR model. But before we get into that, it’s time to dig into the exact definition of the AR model. ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `AutoRegressive` model 2. Data to test our model For the test data we will use the last 12 months to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2011-12-01'] test = df[df.ds>'2011-12-01'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((312, 3), (12, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train") sns.lineplot(test, x="ds", y="y", label="Test") plt.show() ``` # Implementation of AutoRegressive with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoRegressive ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful.season\_length. **Method 1:** We use the lags parameter in an integer format, that is, we put the lags we want to evaluate in the model. ```python theme={null} season_length = 12 # Monthly data horizon = len(test) # number of predictions biasadj=True, include_drift=True, models2 = [AutoRegressive(lags=[14], include_mean=True)] ``` **Method 2:** We use the lags parameter in a list format, that is, we put the lags that we want to evaluate in the model in the form of a list as shown below. ```python theme={null} season_length = 12 # Monthly data horizon = len(test) # number of predictions models = [AutoRegressive(lags=[3,4,6,7,9,10,11,12,13,14], include_mean=True)] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='MS') ``` ### Fit Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[AutoRegressive]) ``` Let’s see the results of our Theta model. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ print(result.keys()) ``` ```text theme={null} dict_keys(['coef', 'sigma2', 'var_coef', 'mask', 'loglik', 'aic', 'arma', 'residuals', 'code', 'n_cond', 'nobs', 'model', 'aicc', 'bic', 'xreg', 'lambda', 'x']) ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("residuals"), columns=["residual Model"]) residual ``` | | residual Model | | --- | -------------- | | 0 | -11998.537347 | | 1 | NaN | | 2 | NaN | | ... | ... | | 309 | -2718.312961 | | 310 | -1306.795172 | | 311 | -2713.284999 | ```python theme={null} fig, axs = plt.subplots(nrows=2, ncols=2) # plot[1,1] residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); # plot sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); # plot stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') # plot plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like `ARIMA` and `Theta`) ```python theme={null} # Prediction Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | AutoRegressive | | --- | ---------- | ---------- | -------------- | | 0 | 1 | 2012-01-01 | 15905.582031 | | 1 | 1 | 2012-02-01 | 13597.894531 | | 2 | 1 | 2012-03-01 | 15488.883789 | | ... | ... | ... | ... | | 9 | 1 | 2012-10-01 | 14087.901367 | | 10 | 1 | 2012-11-01 | 13274.105469 | | 11 | 1 | 2012-12-01 | 12498.226562 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | AutoRegressive | | - | ---------- | ---------- | ------- | -------------- | | 0 | 1 | 1986-01-01 | 9034.0 | 21032.537109 | | 1 | 1 | 1986-02-01 | 9596.0 | NaN | | 2 | 1 | 1986-03-01 | 10558.0 | NaN | | 3 | 1 | 1986-04-01 | 9002.0 | 126172.937500 | | 4 | 1 | 1986-05-01 | 9239.0 | 10020.040039 | Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | AutoRegressive | AutoRegressive-lo-95 | AutoRegressive-hi-95 | | --- | ---------- | ---------- | -------------- | -------------------- | -------------------- | | 0 | 1 | 2012-01-01 | 15905.582031 | 2119.586426 | 29691.578125 | | 1 | 1 | 2012-02-01 | 13597.894531 | -188.101135 | 27383.890625 | | 2 | 1 | 2012-03-01 | 15488.883789 | 1702.888062 | 29274.878906 | | ... | ... | ... | ... | ... | ... | | 9 | 1 | 2012-10-01 | 14087.901367 | -1050.068359 | 29225.871094 | | 10 | 1 | 2012-11-01 | 13274.105469 | -1886.973145 | 28435.183594 | | 11 | 1 | 2012-12-01 | 12498.226562 | -2675.547607 | 27672.001953 | ```python theme={null} # Merge the forecasts with the true values Y_hat1 = test.merge(Y_hat, how='left', on=['unique_id', 'ds']) Y_hat1 ``` | | ds | y | unique\_id | AutoRegressive | | --- | ---------- | ----- | ---------- | -------------- | | 0 | 2012-01-01 | 13427 | 1 | 15905.582031 | | 1 | 2012-02-01 | 14447 | 1 | 13597.894531 | | 2 | 2012-03-01 | 14717 | 1 | 15488.883789 | | ... | ... | ... | ... | ... | | 9 | 2012-10-01 | 13795 | 1 | 14087.901367 | | 10 | 2012-11-01 | 13352 | 1 | 13274.105469 | | 11 | 2012-12-01 | 12716 | 1 | 12498.226562 | ```python theme={null} sf.plot(train, Y_hat1) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | AutoRegressive | | --- | ---------- | ---------- | -------------- | | 0 | 1 | 2012-01-01 | 15905.582031 | | 1 | 1 | 2012-02-01 | 13597.894531 | | 2 | 1 | 2012-03-01 | 15488.883789 | | ... | ... | ... | ... | | 9 | 1 | 2012-10-01 | 14087.901367 | | 10 | 1 | 2012-11-01 | 13274.105469 | | 11 | 1 | 2012-12-01 | 12498.226562 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[95]) forecast_df ``` | | unique\_id | ds | AutoRegressive | AutoRegressive-lo-95 | AutoRegressive-hi-95 | | --- | ---------- | ---------- | -------------- | -------------------- | -------------------- | | 0 | 1 | 2012-01-01 | 15905.582031 | 2119.586426 | 29691.578125 | | 1 | 1 | 2012-02-01 | 13597.894531 | -188.101135 | 27383.890625 | | 2 | 1 | 2012-03-01 | 15488.883789 | 1702.888062 | 29274.878906 | | ... | ... | ... | ... | ... | ... | | 9 | 1 | 2012-10-01 | 14087.901367 | -1050.068359 | 29225.871094 | | 10 | 1 | 2012-11-01 | 13274.105469 | -1886.973145 | 28435.183594 | | 11 | 1 | 2012-12-01 | 12498.226562 | -2675.547607 | 27672.001953 | ```python theme={null} sf.plot(train, test.merge(forecast_df), level=[95]) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=5)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=train, h=horizon, step_size=6, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | AutoRegressive | | --- | ---------- | ---------- | ---------- | ------- | -------------- | | 0 | 1 | 2009-01-01 | 2008-12-01 | 19262.0 | 24295.837891 | | 1 | 1 | 2009-02-01 | 2008-12-01 | 20658.0 | 23993.947266 | | 2 | 1 | 2009-03-01 | 2008-12-01 | 22660.0 | 21201.121094 | | ... | ... | ... | ... | ... | ... | | 57 | 1 | 2011-10-01 | 2010-12-01 | 12893.0 | 19349.708984 | | 58 | 1 | 2011-11-01 | 2010-12-01 | 11843.0 | 16899.849609 | | 59 | 1 | 2011-12-01 | 2010-12-01 | 11321.0 | 18159.574219 | We’ll now plot the forecast for each cutoff period. To make the plots clearer, we’ll rename the actual values in each period. ```python theme={null} from IPython.display import display ``` ```python theme={null} crossvalidation_df.rename(columns = {'y' : 'actual'}, inplace = True) # rename actual values cutoff = crossvalidation_df['cutoff'].unique() for k in range(len(cutoff)): cv = crossvalidation_df[crossvalidation_df['cutoff'] == cutoff[k]] display(StatsForecast.plot(df, cv.loc[:, cv.columns != 'cutoff'])) ``` ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, agg_fn='mean', ) ``` | | metric | AutoRegressive | | - | ------ | -------------- | | 0 | mae | 962.023763 | | 1 | mape | 0.072733 | | 2 | mase | 0.601808 | | 3 | rmse | 1195.013050 | | 4 | smape | 0.034858 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). 3. [Nixtla AutoRegressive API](../../src/core/models.html#autoregressive) 4. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 5. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html) 6. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # AutoTheta Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/autotheta.html > Step-by-step guide on using the `AutoTheta Model` with > `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is copied from and inspired by: 1. [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). 2\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html) ## Table of Contents * [Introduction](#introduction) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of AutoTheta with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The `AutoTheta` model in `StatsForecast` automatically selects the best **Theta model** based on the **mean squared error (MSE)**. In this section, we will discuss each of the models that `AutoTheta` considers and then explain how it selects the best one. ### 1. Standard Theta Model (STM) The **Standard Theta Model** is the original version of the Theta model introduced by Assimakopoulos and Nikolopoulos (2000). It decomposes a time series into two modified versions of the original series, called **theta lines**. These lines are created by applying a linear transformation to the second differences of the original series, controlled by a parameter called **theta $\theta$**. One theta line captures the long-term trend, while the other captures short-term fluctuations. The two theta lines are then combined to produce the final forecast. The STM assumes that model parameters remain constant over time. ### 2. Optimized Theta Model (OTM) The **Optimized Theta Model** extends STM by searching for the best theta parameters rather than using fixed values. This optimization step allows the model to better fit series with higher variability. ### 3. Dynamic Standard Theta Model (DSTM) The **Dynamic Standard Theta Model** allows STM to adapt over time. Instead of keeping parameters static, it updates them dynamically as new data becomes available. This dynamic behavior can be useful when forecasting series with evolving trends or seasonality. ### 4. Dynamic Optimized Theta Model (DOTM) The **Dynamic Optimized Theta Model** combines features of both OTM and DSTM. Like OTM, it optimizes the theta parameters. Like DSTM, it updates the model dynamically with new data. ## How AutoTheta Selects the Best Model 1. `AutoTheta` fits all four variants of the Theta model (STM, OTM, DSTM, and DOTM) to your data. 2. Each model is evaluated using cross-validation or a hold-out validation strategy, depending on the configuration. 3. The model that achieves the lowest mean squared error (MSE) is selected. 4. The selected model is then used to generate the forecast. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import pandas as pd import scipy.stats as stats ``` ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('fivethirtyeight') plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#212946', 'axes.facecolor': '#212946', 'savefig.facecolor':'#212946', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#2A3459', 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} df = pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/candy_production.csv") df.head() ``` | | observation\_date | IPG3113N | | - | ----------------- | -------- | | 0 | 1972-01-01 | 85.6945 | | 1 | 1972-02-01 | 71.8200 | | 2 | 1972-03-01 | 66.0229 | | 3 | 1972-04-01 | 64.5645 | | 4 | 1972-05-01 | 65.0100 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ---------- | ------- | ---------- | | 0 | 1972-01-01 | 85.6945 | 1 | | 1 | 1972-02-01 | 71.8200 | 1 | | 2 | 1972-03-01 | 66.0229 | 1 | | 3 | 1972-04-01 | 64.5645 | 1 | | 4 | 1972-05-01 | 65.0100 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y float64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints aa random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=60, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=60, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `AutoTheta` model 2\. Data to test our model For the test data we will use the last 12 months to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2016-08-01'] test = df[df.ds>'2016-08-01'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((536, 3), (12, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train", linewidth=3, linestyle=":") sns.lineplot(test, x="ds", y="y", label="Test") plt.ylabel("Candy Production") plt.xlabel("Month") plt.show() ``` ## Implementation of AutoTheta with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoTheta ``` ### Instantiate Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful.season\_length. Automatically selects the best Theta (Standard Theta Model `(‘STM’)`, Optimized Theta Model `(‘OTM’)`, Dynamic Standard Theta Model `(‘DSTM’)`, Dynamic Optimized Theta Model `(‘DOTM’))` model using mse. ```python theme={null} season_length = 12 # Monthly data horizon = len(test) # number of predictions # We call the model that we are going to use models = [AutoTheta(season_length=season_length, decomposition_type="additive", model="STM")] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='MS') ``` ### Fit Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[AutoTheta]) ``` Let’s see the results of our Theta model. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'mse': 100.57831864069415, 'amse': array([26.13585578, 38.60211513, 44.70605915]), 'fit': results(x=array([258.45064973, 0.7664297 ]), fn=100.57831864069415, nit=32, simplex=array([[250.37338496, 0.76970741], [232.03915522, 0.76429422], [258.45064973, 0.7664297 ]])), 'residuals': array([-2.14815337e+02, -6.20562800e+01, -2.13256707e+01, -1.25845480e+01, -1.19719350e+01, -9.40876632e+00, -8.60141525e+00, -9.00054652e+00, -1.98778836e+00, 3.14564857e+01, 1.98519673e+01, 2.04962370e+01, 4.98120196e+00, -1.08735375e+01, -1.12328024e+01, -8.08115377e+00, -9.98197589e+00, -8.39937098e+00, -1.25789505e+01, -1.05952806e+01, 8.47229127e-01, 2.25644616e+01, 2.54401546e+01, 1.73989716e+01, 2.40287275e+00, -2.53475866e+00, -8.00591135e+00, -1.79241479e+01, -6.36590693e+00, -5.76986468e+00, -2.26766759e+01, -8.95260931e+00, -7.19719166e+00, 2.74032238e+01, 2.21368457e+01, 6.43171676e+00, -3.51755220e+00, -1.31441941e+01, -6.13166031e+00, 1.51512150e+00, -8.05777104e+00, -8.59603388e+00, -1.08617851e+01, -6.72940177e+00, -6.24861641e+00, 2.85996828e+01, 2.98048030e+01, 1.90032238e+01, 5.07597842e+00, -9.59170058e+00, -1.64521034e+01, -7.52212744e+00, -5.16540394e+00, -1.27924628e+01, -9.68434625e+00, -8.76758703e+00, -8.27475947e-01, 3.08424002e+01, 2.47947352e+01, 2.35867208e+01, 3.75664716e+00, -4.47305717e+00, -1.48000403e+01, -1.08431546e+01, -1.01249972e+01, -1.12379765e+01, -1.28624644e+01, -9.47780103e+00, -2.17960841e-01, 2.49398648e+01, 1.66027782e+01, 2.62581230e+01, -1.94879264e+00, -8.10877843e+00, -6.93183679e+00, -6.80707596e+00, -1.17809892e+01, -1.05320670e+01, -1.59715849e+01, -9.07599923e+00, 6.11988125e-01, 2.24925163e+01, 2.57389503e+01, 2.38907614e+01, 4.99776202e+00, -1.07054696e+01, -7.24194672e+00, -1.17412084e+01, -1.10031559e+01, -9.10138831e+00, -1.62277209e+01, -1.02585250e+01, -2.79431476e+00, 1.96746051e+01, 2.40620700e+01, 2.00041920e+01, 8.38674843e-01, -3.01708830e-01, -1.10576372e+01, -1.76502404e+01, -4.79853028e+00, -7.74057206e+00, -1.55628746e+01, -6.19663664e+00, -4.85267830e+00, 2.17819325e+01, 2.48075790e+01, 2.16186207e+01, 9.21215745e+00, -1.71191202e+00, -1.38314188e+01, -9.44161337e+00, -6.35863884e+00, -1.10470671e+01, -1.41408736e+01, -9.60039945e+00, -4.80959619e+00, 3.41173952e+01, 2.02685767e+01, 1.65177446e+01, 1.45004431e+00, -6.65011083e-01, -1.11027939e+01, -1.82545876e+01, -1.08637878e+01, -9.67573606e+00, -1.22946714e+01, -1.02064815e+01, -2.94225894e+00, 3.21840497e+01, 2.21586046e+01, 2.09073990e+01, -2.49862821e-01, -6.05605889e+00, -1.16741825e+01, -1.31096470e+01, -1.07043825e+01, -1.25489037e+01, -9.16715807e+00, -7.70278723e+00, -2.55657034e+00, 2.69936351e+01, 1.62042780e+01, 1.67614452e+01, 8.62186552e+00, -3.51518668e+00, -9.27421021e+00, -1.15442848e+01, -9.96136043e+00, -1.17898558e+01, -1.13147670e+01, -7.10440489e+00, -1.10170600e+00, 2.60646482e+01, 2.32687942e+01, 1.82272063e+01, 3.98792378e+00, -7.64233782e+00, -1.07945901e+01, -1.16024004e+01, -1.10645345e+01, -1.33282245e+01, -1.15534843e+01, -6.76286215e+00, 3.93786824e+00, 2.37018431e+01, 2.07922131e+01, 2.37645505e+01, 7.00182907e-01, -1.59605643e+00, -1.62277584e+01, -1.51068271e+01, -1.01377645e+01, -1.13639586e+01, -1.38275901e+01, -5.87092572e+00, 3.43469809e+00, 2.82932175e+01, 2.39510218e+01, 1.71053544e+01, 6.00992500e-01, -7.61224365e-01, -1.18686664e+01, -1.51989727e+01, -1.23352870e+01, -1.09931345e+01, -1.34086766e+01, -4.52127997e+00, 2.09363525e+00, 3.13825850e+01, 2.43980063e+01, 1.89899567e+01, -7.55702038e+00, -2.76893846e-01, -6.52574120e+00, -1.67167241e+01, -1.17498886e+01, -7.68050287e+00, -5.60844424e+00, -2.79087739e+00, -2.92094111e-01, 2.31896495e+01, 1.70158799e+01, 1.84177113e+01, -3.39879920e-01, 1.31241579e+00, -9.65552567e+00, -1.30840488e+01, -1.33540036e+01, -9.72077648e+00, -1.09022916e+01, -4.49636288e+00, -6.88544858e-01, 1.88878504e+01, 2.15227074e+01, 2.32009723e+01, -5.72605223e+00, 1.87746593e+00, -6.95944675e+00, -1.41944248e+01, -1.25398544e+01, -8.09461542e+00, -5.46316863e+00, -4.73324533e+00, 1.12162644e+00, 1.61183526e+01, 2.63470350e+01, 2.28827919e+01, -6.75326971e+00, 4.34023844e+00, -6.61711624e+00, -1.64533666e+01, -1.44473761e+01, -4.85575583e+00, -1.14659672e+01, -1.83412077e+00, -3.17492418e+00, 1.22586060e+01, 2.19162129e+01, 1.62630835e+01, -1.99943697e+00, 2.59255529e-03, -8.89996147e+00, -1.10976714e+01, -1.43864448e+01, -9.48222409e+00, -1.06785728e+01, -7.24340882e+00, 2.15092681e+00, 1.53607666e+01, 2.06126854e+01, 1.96076182e+01, 3.03104699e+00, -8.52358190e-02, -8.52357557e+00, -1.33461589e+01, -1.37600247e+01, -6.08841095e+00, -8.32367886e+00, -3.02117555e+00, 4.08615082e-01, 1.63346143e+01, 1.76259473e+01, 1.75724049e+01, 1.52688162e+00, -2.23616417e+00, -3.82136854e+00, -1.61943630e+01, -1.55739806e+01, -6.10489716e+00, -6.56542955e+00, -3.79160074e+00, 1.79366664e+00, 1.37690213e+01, 1.71704010e+01, 2.12969028e+01, 2.55881370e+00, -5.89333549e+00, -5.43867513e+00, -9.34441775e+00, -1.23296368e+01, -7.43701484e+00, -9.59827267e+00, -6.98198280e+00, -7.94911839e-01, 1.30601062e+01, 2.03392195e+01, 2.52824447e+01, -3.95418211e+00, 2.43162216e+00, -3.09611231e+00, -1.49779647e+01, -1.07287660e+01, -8.40149898e+00, -1.18887475e+01, -1.74756969e+00, 2.17909158e+00, 1.20038451e+01, 2.42508083e+01, 2.34572756e+01, -5.17568738e+00, -1.96585193e-01, -4.18458348e+00, -1.55118992e+01, -1.38833773e+01, -8.29522246e+00, -1.30003245e+01, -1.67001046e-01, 9.35165464e-01, 1.47274009e+01, 2.29308500e+01, 2.17103726e+01, 3.68218796e+00, 2.64751368e-01, -7.34442896e+00, -1.25122452e+01, -1.14503472e+01, -8.19533891e+00, -1.15456946e+01, -2.81694273e+00, -1.50158220e+00, 1.14252490e+01, 2.08253654e+01, 1.93274939e+01, 7.94218283e-01, -5.10392562e-01, -8.74257956e+00, -9.01561168e+00, -1.00192375e+01, -1.10908742e+01, -1.09129057e+01, -6.64424202e+00, -1.50482563e+00, 1.46897914e+01, 1.73829656e+01, 2.23508516e+01, 8.64908482e+00, 6.22670938e-01, -6.68012958e+00, -5.70808463e+00, -1.80391974e+01, -7.97569860e+00, -1.19962932e+01, -5.55858916e+00, 2.35415063e+00, 1.17526337e+01, 1.54009327e+01, 2.21564076e+01, 3.90926848e+00, 2.21699063e+00, -3.80724386e+00, -1.09345639e+01, -1.37938477e+01, -1.00726110e+01, -1.19963696e+01, -5.40000702e+00, -1.51910929e+00, 1.69895520e+00, 1.74367921e+01, 2.04883238e+01, 7.55305367e+00, 7.29570618e-01, -5.09536099e+00, -1.29493298e+01, -1.53454372e+01, -2.46711622e+00, -1.01903520e+01, -4.03697494e+00, -3.08084548e+00, 3.86928001e+00, 1.92764155e+01, 1.55958052e+01, 7.35560665e+00, 1.85905286e+00, -5.61647492e-01, -1.23394890e+01, -9.90369650e+00, -7.50968724e+00, -1.83651468e+01, -2.77916418e+00, -1.07805825e+00, 8.15877162e+00, 2.33477133e+01, 1.69720395e+01, 6.19355409e+00, 4.92033190e+00, -1.36452236e+01, -1.10382237e+01, -4.45625959e+00, -1.37976278e+01, -1.12070229e+01, -1.28293907e+00, 1.02615489e-01, 1.16373419e+01, 1.73964040e+01, 1.64050904e+01, 1.32632316e+01, 4.44789857e+00, -1.66636700e+01, -1.04932431e+01, -7.27536831e+00, -1.52095878e+01, -8.33331485e+00, -6.12562623e+00, -6.19892381e-01, 1.73375856e+01, 1.71076116e+01, 2.30092371e+01, -1.39793588e+00, 1.20108534e+00, -1.01506292e+01, -9.35709025e+00, -1.72524967e+01, -1.33257487e+01, -1.11436060e+01, -1.07822676e+00, 2.29723021e+00, 1.15489387e+01, 1.72661557e+01, 2.11762682e+01, 9.51783705e+00, -1.02191435e+00, -5.14895585e+00, -2.05301479e+01, -1.56429911e+01, -1.60412160e+01, -1.50915585e+01, -2.94815119e+00, 4.61947140e+00, 6.94204531e+00, 1.79378222e+01, 2.19333496e+01, 8.01926876e+00, -3.09873539e+00, -6.33383956e+00, -1.29668016e+01, -1.54450181e+01, -1.27736754e+01, -1.46733580e+01, -8.76927199e+00, 8.56843050e+00, 1.28259048e+01, 1.86473170e+01, 5.73666651e+00, 4.33460471e+00, 2.08833654e+00, -3.96959363e+00, -1.29223840e+01, -1.19550435e+01, -1.27279210e+01, -8.02537118e+00, -3.92329973e+00, 7.09140567e+00, 2.42153157e+01, 1.28924451e+01, 1.79711994e+01, 2.89522816e+00, 1.30474094e+00, -7.77941829e+00, -1.04361458e+01, -1.14357321e+01, -1.23868252e+01, -3.73410135e+00, 6.47313429e-01, 5.14176514e+00, 1.16621376e+01, 8.00349556e+00, 1.83900860e+01, 3.46846764e+00, 2.29413265e+00, -4.06962578e+00, -8.55164849e+00, -1.76399695e+01, -1.50423508e+01, -1.13765532e+01, -9.17973632e+00, -4.22254178e+00, 2.19090137e+01, 1.90170614e+01, 1.80606278e+01, 4.08981599e+00, 2.02346117e+00, -5.45474659e+00, -1.38725716e+01, -1.50622791e+01, -1.15367789e+01, -7.55445577e+00, -1.77510788e+00, 9.46335947e+00, 4.88813367e+00, 1.61490895e+01, 1.93212548e+01, 1.03075610e+01, -6.46758291e-01, -5.79530543e-01, -1.35917659e+01, -1.62148912e+01, -1.29823949e+01, -1.02149087e+01, -3.24211066e+00, 3.05411201e-01, 1.19385090e+01, 2.08979477e+01, 2.19927470e+01, 1.32364223e+00, 1.68626515e+00, -3.52030557e+00, -1.50337436e+01, -1.75865944e+01, -1.23980840e+01, -1.19670311e+01, -1.59575440e+00, 4.32015112e+00, 1.39461330e+01, 2.63901690e+01, 2.11431667e+01, 1.19960552e+00, 1.22769386e+00, -3.12851420e+00, -1.23388328e+01, -1.66429432e+01, -9.08277509e+00, -7.92637338e+00, 2.43702321e+00, -3.53211182e+00, 1.00606776e+01, 1.39608421e+01, 1.44689452e+01, 6.50770562e+00, 3.13940836e+00, -4.89894478e-01, -1.05833296e+01, -1.34863098e+01, -1.20763793e+01, -1.00738904e+01, -9.39207297e+00]), 'm': 12, 'states': array([[1.24021769e+02, 8.30544193e+01, 8.40569047e+01, 6.14692129e-02, 3.00509837e+02], [8.50161150e+01, 7.80917592e+01, 8.40569047e+01, 6.14692129e-02, 1.33876280e+02], [7.65735210e+01, 7.67280499e+01, 8.40569047e+01, 6.14692129e-02, 8.73485707e+01], ..., [1.12984989e+02, 1.00517846e+02, 8.40569047e+01, 6.14692129e-02, 1.14480779e+02], [1.14049672e+02, 1.00543746e+02, 8.40569047e+01, 6.14692129e-02, 1.13025090e+02], [1.10946036e+02, 1.00561388e+02, 8.40569047e+01, 6.14692129e-02, 1.14089773e+02]]), 'par': {'initial_smoothed': 258.45064973324986, 'alpha': 0.7664297044277045, 'theta': 2.0}, 'n': 536, 'modeltype': 'STM', 'mean_y': 100.56138830499272, 'decompose': True, 'decomposition_type': 'additive', 'seas_forecast': {'mean': array([ 0.08977811, 18.09442035, 20.24848682, 19.4306462 , 2.64008067, -1.30909907, -7.97773123, -12.32640613, -12.02777406, -10.1369666 , -11.42293515, -5.30249992])}, 'fitted': array([300.50983667, 133.87628004, 87.34857069, 77.149048 , 76.98193501, 77.05546632, 77.64431525, 79.83754652, 77.03398836, 75.47241431, 85.74423271, 85.47106297, 86.31849804, 88.14353755, 80.8438024 , 78.37975377, 81.66417589, 83.26287098, 84.62535048, 83.7700806 , 79.74427087, 80.35553844, 83.81224541, 87.82202841, 86.29562725, 86.14455866, 85.23591135, 85.24504787, 80.98550693, 85.35566468, 88.73347592, 80.13900931, 77.37219166, 71.81797618, 78.98325428, 80.46128324, 70.5292522 , 65.84059407, 56.80056031, 58.2461785 , 68.88547104, 71.95893388, 73.17068509, 73.63150177, 72.56861641, 67.74141718, 75.823697 , 83.17867619, 82.88182158, 84.77950058, 78.46220336, 71.99792744, 75.71080394, 81.00106285, 78.99654625, 80.35978703, 77.73477595, 77.0624998 , 86.86366484, 90.37877922, 93.59485284, 94.48135717, 92.0871403 , 86.88905458, 88.05659723, 89.54567652, 88.73256441, 87.66000103, 84.49066084, 84.28553517, 89.56282177, 86.79937702, 92.06289264, 88.57657843, 83.39583679, 84.22817596, 88.48908916, 88.70896704, 88.43688493, 84.98139923, 82.12001187, 82.55098375, 85.9525497 , 90.19133861, 93.64043798, 95.47816961, 88.30724672, 88.90190843, 89.38115594, 90.19718831, 91.02162087, 87.36982498, 83.60211476, 81.42239492, 82.66423005, 85.61780804, 86.08812516, 84.73820883, 85.54103724, 83.21124039, 79.16163028, 84.73307206, 86.60047462, 83.45823664, 82.8036783 , 79.0463675 , 81.90332096, 85.42827927, 87.13594255, 92.20371202, 91.92571881, 87.47001337, 89.71173884, 94.08746707, 93.42067364, 91.36829945, 88.10499619, 84.3807048 , 96.69192329, 96.73805538, 94.53625569, 93.65491108, 94.17929385, 91.81488763, 87.30208784, 88.22493606, 88.60917145, 87.97178146, 84.24395894, 81.95085029, 92.78029537, 94.275001 , 95.43756282, 93.25335889, 89.64588248, 86.84354705, 86.27398255, 87.31900372, 85.50115807, 87.26078723, 85.45187034, 83.45436489, 90.30572204, 87.23685484, 85.22183448, 89.83718668, 88.17711021, 87.21418481, 87.84436043, 89.45885582, 88.22276703, 88.33640489, 86.986106 , 86.10365179, 92.24300578, 94.58859369, 93.69697622, 94.76073782, 89.93749012, 87.8093004 , 88.3949345 , 89.16392452, 86.74878426, 86.67946215, 85.59093176, 88.57095695, 92.89938688, 93.34684947, 96.69921709, 95.24315643, 95.05395839, 88.76162712, 86.66136449, 88.14065857, 87.23099008, 85.41872572, 85.01380191, 87.60818255, 95.45557821, 98.3240456 , 96.5726075 , 95.04052437, 95.49116642, 92.53977272, 90.36888696, 90.16393454, 89.5384766 , 88.04727997, 88.67676475, 90.24331499, 100.45849371, 103.6695433 , 103.36252038, 95.57789385, 96.3997412 , 97.54332409, 94.2091886 , 94.45290287, 96.36634424, 100.85347739, 102.80919411, 102.5472505 , 106.48312008, 104.03628873, 103.29067992, 101.03748421, 103.07742567, 101.82224878, 101.27230355, 100.28657648, 100.63629155, 101.06606288, 101.71464486, 101.14884962, 101.78769257, 102.7950277 , 105.71545223, 99.33413407, 101.80714675, 102.61832483, 101.21735442, 100.85561542, 102.45166863, 107.05014533, 107.51717356, 108.33874738, 106.85496498, 111.55980808, 114.23636971, 107.06776156, 111.42831624, 112.50186659, 109.36957611, 107.54585583, 111.62426724, 111.62202077, 114.31102418, 111.83959398, 107.39758714, 108.70651652, 106.30953697, 102.78440744, 103.82046147, 103.14437142, 104.11684481, 102.33982409, 102.8723728 , 103.47360882, 102.01677319, 103.62723339, 101.56281457, 101.87268181, 102.03905301, 102.36943582, 103.33817557, 102.95055886, 102.19972468, 100.90281095, 104.03647886, 106.44257555, 108.22178492, 108.49688565, 107.17885267, 105.19959511, 103.80611838, 102.98366417, 102.30386854, 105.52016297, 102.58638056, 99.89919716, 103.02022955, 106.77390074, 107.96263336, 109.29927875, 106.01489901, 103.6864972 , 105.1475863 , 105.11603549, 101.63327513, 103.61001775, 105.92623683, 105.72561484, 107.82567267, 109.2548828 , 107.99841184, 107.35109379, 103.5233805 , 103.62365533, 108.13938211, 103.11607784, 106.01381231, 109.78596466, 107.78446605, 108.81079898, 110.17164752, 109.84536969, 112.60070842, 114.23275493, 109.59549173, 112.69372438, 115.81058738, 109.85108519, 110.73448348, 113.67239919, 111.26167729, 109.87022246, 111.31252448, 110.13430105, 114.10103454, 114.77969912, 112.22984999, 114.31642742, 116.09441204, 116.92384863, 118.16082896, 118.67694524, 118.56524723, 119.03853891, 120.55739465, 120.49404273, 122.4297822 , 121.24085099, 116.16013458, 116.63300608, 116.58468172, 115.20069256, 115.84357956, 115.28811168, 117.8563375 , 119.42647419, 118.72610568, 119.14774202, 118.15012563, 116.95870856, 114.38003444, 112.21454843, 114.48341518, 119.11962906, 120.63092958, 121.65618463, 126.75939743, 122.1827986 , 123.8699932 , 123.46128916, 123.29574937, 125.06196634, 120.23216725, 116.54759242, 118.66743152, 119.67090937, 122.40414386, 125.63126387, 126.72874773, 125.40591101, 125.48596965, 125.07720702, 125.03320929, 123.8308448 , 111.2956079 , 109.17137615, 110.01274633, 113.80892938, 115.40216099, 117.64202977, 117.19533719, 114.68331622, 120.59245198, 121.56787494, 122.56854548, 120.16921999, 109.29738449, 108.58309477, 105.67469335, 109.31954714, 111.77844749, 117.49308896, 117.5137965 , 119.17248724, 121.21684679, 115.92686418, 117.89155825, 117.02722838, 109.44298667, 111.84906053, 109.99544591, 112.7496681 , 117.55482364, 113.24182371, 114.25985959, 120.09362779, 117.31872292, 117.51493907, 120.62638451, 120.66695807, 115.74879597, 113.59360961, 111.30546837, 119.47810143, 123.92117003, 117.29474313, 118.73046831, 122.40358785, 118.54651485, 120.94522623, 120.34509238, 119.83191444, 119.28258839, 116.90606293, 119.67953588, 116.61541466, 118.57002916, 116.93539025, 119.24189675, 115.26824869, 112.85500598, 113.09982676, 116.36816979, 118.09076126, 113.10484433, 110.84983175, 112.21846295, 117.52051435, 117.77135585, 119.97014793, 113.71329113, 110.97321598, 106.47875848, 103.69775119, 105.5329286 , 109.03535469, 100.5185778 , 98.7783504 , 100.72723124, 104.88073539, 103.53983956, 104.83050157, 104.37041809, 101.78207536, 99.79195805, 97.33147199, 94.7051695 , 101.23419515, 97.22698298, 96.03053349, 85.56579529, 86.89526346, 89.52989363, 92.63258395, 92.20654345, 92.29302095, 90.33797118, 92.97269973, 94.06049433, 99.45748428, 104.17945492, 98.57230063, 97.48447184, 97.71075906, 99.74481829, 99.92754582, 101.40703208, 101.89152524, 100.19790135, 106.12158657, 110.71243486, 114.61516239, 109.71600444, 100.36181401, 99.59503236, 100.26066735, 103.05302578, 106.07904849, 109.00286948, 104.73225081, 101.00335324, 101.06963632, 98.12874178, 94.85438633, 97.80873857, 96.89567218, 95.87638401, 97.01823883, 99.60314659, 101.56757157, 100.41327905, 98.11827889, 97.07615577, 100.07180788, 102.80604053, 110.02096633, 99.93001054, 96.81884524, 96.765739 , 102.67305829, 103.21143054, 108.91236591, 107.9732912 , 104.79489485, 102.64480872, 103.60141066, 105.2112888 , 105.40729101, 100.7199523 , 101.24845301, 103.24285777, 102.26463485, 104.59110557, 108.03814361, 105.99389435, 101.76418396, 100.06193105, 99.6756544 , 102.54734888, 105.82036702, 102.67173097, 107.40963325, 108.75289448, 107.67960614, 109.6546142 , 113.40193278, 113.42314323, 109.91667509, 110.75537338, 113.46597679, 119.42851182, 116.6833224 , 110.55675793, 105.76845483, 101.99639438, 104.99139164, 108.43159448, 114.20122959, 115.56790983, 114.4807793 , 113.0250904 , 114.08977297])} ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("residuals"), columns=["residual Model"]) residual ``` | | residual Model | | --- | -------------- | | 0 | -214.815337 | | 1 | -62.056280 | | 2 | -21.325671 | | ... | ... | | 533 | -12.076379 | | 534 | -10.073890 | | 535 | -9.392073 | ```python theme={null} fig, axs = plt.subplots(nrows=2, ncols=2) residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like `ARIMA` and `Theta`) ```python theme={null} # Prediction Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | AutoTheta | | --- | ---------- | ---------- | ---------- | | 0 | 1 | 2016-09-01 | 111.075915 | | 1 | 1 | 2016-10-01 | 129.111292 | | 2 | 1 | 2016-11-01 | 131.296093 | | ... | ... | ... | ... | | 9 | 1 | 2017-06-01 | 101.125782 | | 10 | 1 | 2017-07-01 | 99.870548 | | 11 | 1 | 2017-08-01 | 106.021718 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | AutoTheta | | - | ---------- | ---------- | ------- | ---------- | | 0 | 1 | 1972-01-01 | 85.6945 | 300.509837 | | 1 | 1 | 1972-02-01 | 71.8200 | 133.876280 | | 2 | 1 | 1972-03-01 | 66.0229 | 87.348571 | | 3 | 1 | 1972-04-01 | 64.5645 | 77.149048 | | 4 | 1 | 1972-05-01 | 65.0100 | 76.981935 | ```python theme={null} StatsForecast.plot(values) ``` Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | AutoTheta | AutoTheta-lo-95 | AutoTheta-hi-95 | | --- | ---------- | ---------- | ---------- | --------------- | --------------- | | 0 | 1 | 2016-09-01 | 111.075915 | 90.139234 | 136.011109 | | 1 | 1 | 2016-10-01 | 129.111292 | 94.795409 | 160.387128 | | 2 | 1 | 2016-11-01 | 131.296093 | 90.579813 | 168.268538 | | ... | ... | ... | ... | ... | ... | | 9 | 1 | 2017-06-01 | 101.125782 | 41.186268 | 159.159903 | | 10 | 1 | 2017-07-01 | 99.870548 | 35.144354 | 152.867267 | | 11 | 1 | 2017-08-01 | 106.021718 | 38.753454 | 166.048584 | ```python theme={null} # Merge the forecasts with the true values Y_hat1 = test.merge(Y_hat, how='left', on=['unique_id', 'ds']) Y_hat1 ``` | | ds | y | unique\_id | AutoTheta | | --- | ---------- | -------- | ---------- | ---------- | | 0 | 2016-09-01 | 109.3191 | 1 | 111.075915 | | 1 | 2016-10-01 | 119.0502 | 1 | 129.111292 | | 2 | 2016-11-01 | 116.8431 | 1 | 131.296093 | | ... | ... | ... | ... | ... | | 9 | 2017-06-01 | 104.2022 | 1 | 101.125782 | | 10 | 2017-07-01 | 102.5861 | 1 | 99.870548 | | 11 | 2017-08-01 | 114.0613 | 1 | 106.021718 | ```python theme={null} sf.plot(train, Y_hat1) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | AutoTheta | | --- | ---------- | ---------- | ---------- | | 0 | 1 | 2016-09-01 | 111.075915 | | 1 | 1 | 2016-10-01 | 129.111292 | | 2 | 1 | 2016-11-01 | 131.296093 | | ... | ... | ... | ... | | 9 | 1 | 2017-06-01 | 101.125782 | | 10 | 1 | 2017-07-01 | 99.870548 | | 11 | 1 | 2017-08-01 | 106.021718 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[95]) forecast_df ``` | | unique\_id | ds | AutoTheta | AutoTheta-lo-95 | AutoTheta-hi-95 | | --- | ---------- | ---------- | ---------- | --------------- | --------------- | | 0 | 1 | 2016-09-01 | 111.075915 | 90.139234 | 136.011109 | | 1 | 1 | 2016-10-01 | 129.111292 | 94.795409 | 160.387128 | | 2 | 1 | 2016-11-01 | 131.296093 | 90.579813 | 168.268538 | | ... | ... | ... | ... | ... | ... | | 9 | 1 | 2017-06-01 | 101.125782 | 41.186268 | 159.159903 | | 10 | 1 | 2017-07-01 | 99.870548 | 35.144354 | 152.867267 | | 11 | 1 | 2017-08-01 | 106.021718 | 38.753454 | 166.048584 | ```python theme={null} sf.plot(train, test.merge(forecast_df), level=[95]) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=5)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation( df=train, h=horizon, step_size=12, n_windows=5 ) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier. * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | AutoTheta | | --- | ---------- | ---------- | ---------- | -------- | ---------- | | 0 | 1 | 2011-09-01 | 2011-08-01 | 93.9062 | 98.167469 | | 1 | 1 | 2011-10-01 | 2011-08-01 | 116.7634 | 116.969932 | | 2 | 1 | 2011-11-01 | 2011-08-01 | 116.8258 | 119.135142 | | ... | ... | ... | ... | ... | ... | | 57 | 1 | 2016-06-01 | 2015-08-01 | 102.4044 | 109.600469 | | 58 | 1 | 2016-07-01 | 2015-08-01 | 102.9512 | 108.260160 | | 59 | 1 | 2016-08-01 | 2015-08-01 | 104.6977 | 114.248270 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | AutoTheta | | - | ---------- | ------ | --------- | | 0 | 1 | mae | 6.281513 | | 1 | 1 | mape | 0.055683 | | 2 | 1 | mase | 1.212473 | | 3 | 1 | rmse | 7.683669 | | 4 | 1 | smape | 0.027399 | ## References 1. [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). 2. [Nixtla AutoTheta API](../../src/core/models.html#autotheta) 3. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 4. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html) 5. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Conformal Seasonal Pool (CSP) Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/conformalseasonalpool.html > Step-by-step guide on using the `ConformalSeasonalPool` model in > `StatsForecast`. The Conformal Seasonal Pool (CSP) is a **training-free probabilistic forecaster** introduced in [Manokhin (2026), *Training-Free Probabilistic Time-Series Forecasting with Conformal Seasonal Pools*](https://arxiv.org/abs/2605.03789). It produces a seasonal naive point forecast with **empirically well-calibrated prediction intervals**. It does this without estimating a single parameter, as the predictive distribution is built entirely from empirical draws of the training history. In the paper’s benchmark, CSP matches or beats deep probabilistic baselines such as DeepNPTS on CRPS while keeping empirical coverage close to the nominal level. It also runs **orders of magnitude faster** (seconds instead of hours) because there is nothing to train. In this notebook, we walk through the model on the [UCI ElectricityLoadDiagrams20112014 dataset](https://archive.ics.uci.edu/dataset/321/electricityloaddiagrams20112014), the raw source of the electricity benchmark used in the paper, and mirrors the paper’s rolling-origin evaluation protocol to confirm that the intervals are calibrated. ## How the model works The point forecast is exactly a [Seasonal Naive](../../src/core/models.html#seasonalnaive) forecast $\mu_h$, which is the value observed one seasonal period ago. All of the model’s machinery goes into the **predictive distribution around** $\mu_h$. The distribution is estimated from `n_samples` draws of a two-component mixture. 1. **Seasonal pool.** Historical observations at the *same seasonal phase* as the forecast target (e.g., all past 3 PM values when forecasting 3 PM), sampled with exponentially decaying weights $\propto e^{-\lambda \cdot \text{age}}$ so that newer observations are drawn more often (`decay` = $\lambda$). 2. **Calibration residuals.** Signed seasonal naive errors $r_t = y_t - y_{t-m}$ computed on the most recent `calib_frac` fraction of the history and added back to the point forecast as $\mu_h + r$. Each sample comes from the seasonal pool with probability $w$ and from the residual component with probability $1-w$. The `variant` parameter controls $w$. * `"fixed"` (**CSP-Fixed**) uses $w = 0.5$ always. * `"adaptive"` (**CSP-Adaptive**) uses $w = 0$ when there is no seasonality (`season_length` ≤ 1), $w = 0.3$ when the seasonal pool is thin (fewer than 3 same-phase observations), and $w = 0.5$ otherwise. Prediction intervals are empirical quantiles of the samples with a **finite-sample conformal correction**. The lower cut uses $\lfloor(n+1)q\rfloor/n$ and the upper cut uses $\lceil(n+1)q\rceil/n$, which orients the quantile estimates conservatively and helps keep empirical coverage close to the nominal level, even with a modest sample budget. **Hyperparameters** | Parameter | Default | Meaning | | --------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `season_length` | Required | Observations per seasonal cycle (24 for hourly data with a daily cycle). | | `n_samples` | 100 | Mixture samples used to estimate the intervals. A level-$L$ interval needs at least $\lceil 2/(1-L/100)\rceil - 1$ samples (≥ 39 for 95%). | | `variant` | `"adaptive"` | `"adaptive"` or `"fixed"` mixture weight (see above). | | `calib_frac` | 0.5 | Fraction of the most recent history used for the calibration residual pool. | | `decay` | 0.01 | Exponential recency rate for the seasonal pool weights. | **When to use CSP?** Strongly seasonal series where you need calibrated prediction intervals at essentially zero computational cost, for example as a probabilistic baseline before reaching for heavier models, or in large-scale settings where per-series training is too expensive. ## Loading libraries and data > **Tip** > > You need StatsForecast to run this notebook. To install it, follow > these [instructions](../getting-started/installation.html). ```python theme={null} import numpy as np import pandas as pd from statsforecast import StatsForecast from statsforecast.models import ConformalSeasonalPool, SeasonalNaive ``` ### Download the electricity dataset We use the [ElectricityLoadDiagrams20112014](https://archive.ics.uci.edu/dataset/321/electricityloaddiagrams20112014) dataset from the UCI Machine Learning Repository, which contains the electricity consumption (kW) of 370 Portuguese clients, recorded every 15 minutes from 2011 to 2014. This is the raw source behind the hourly `electricity` benchmark on which the CSP paper is evaluated. > **Warning** > > The zip file is about 250 MB. The cell below caches it locally, so it > is only downloaded once. ```python theme={null} import zipfile from pathlib import Path from urllib.request import urlretrieve DATA_DIR = Path("data") DATA_DIR.mkdir(exist_ok=True) DATA_URL = "https://archive.ics.uci.edu/static/public/321/electricityloaddiagrams20112014.zip" zip_path = DATA_DIR / "electricityloaddiagrams20112014.zip" txt_path = DATA_DIR / "LD2011_2014.txt" if not txt_path.exists(): if not zip_path.exists(): urlretrieve(DATA_URL, zip_path) with zipfile.ZipFile(zip_path) as f: f.extract("LD2011_2014.txt", DATA_DIR) ``` The file is semicolon-separated with decimal commas. Each timestamp marks the end of a 15-minute interval, so we aggregate to hourly totals (the convention used by the GluonTS `electricity` benchmark). To keep the notebook fast we work with ten clients over the last three months of 2014, keeping only clients that are active (non-zero) throughout the window, since some clients joined after 2011 and their earlier readings are recorded as zero. ```python theme={null} raw = pd.read_csv(txt_path, sep=";", decimal=",", index_col=0, parse_dates=True) hourly = raw.resample("h").sum() window = hourly.loc["2014-10-01":"2014-12-31"] active_clients = window.columns[(window > 0).all()] clients = list(active_clients[:10]) df = ( window[clients] .reset_index(names="ds") .melt(id_vars="ds", var_name="unique_id", value_name="y") ) df ``` | | ds | unique\_id | y | | ----- | ------------------- | ---------- | ------ | | 0 | 2014-10-01 00:00:00 | MT\_002 | 92.46 | | 1 | 2014-10-01 01:00:00 | MT\_002 | 79.66 | | 2 | 2014-10-01 02:00:00 | MT\_002 | 77.52 | | ... | ... | ... | ... | | 22077 | 2014-12-31 21:00:00 | MT\_012 | 727.66 | | 22078 | 2014-12-31 22:00:00 | MT\_012 | 691.49 | | 22079 | 2014-12-31 23:00:00 | MT\_012 | 665.96 | ### Explore the data The hourly load shows the strong daily seasonality that CSP exploits. ```python theme={null} StatsForecast.plot(df, max_insample_length=24 * 7) ``` ## Forecasting with prediction intervals We instantiate `ConformalSeasonalPool` alongside `SeasonalNaive` and forecast the next day (`h=24`) with 80% and 95% prediction intervals. The default `n_samples=100` comfortably exceeds the minimum number of samples required for non-degenerate 80% and 95% intervals. Note that CSP requires no training, so `forecast` returns almost instantly. ```python theme={null} season_length = 24 # daily seasonality for hourly data horizon = 24 # forecast one day ahead, as in the paper sf = StatsForecast( models=[ ConformalSeasonalPool(season_length=season_length), SeasonalNaive(season_length=season_length), ], freq="h", ) fcst = sf.forecast(df=df, h=horizon, level=[80, 95]) fcst ``` | | unique\_id | ds | CSP-Adaptive | CSP-Adaptive-lo-95 | CSP-Adaptive-lo-80 | CSP-Adaptive-hi-80 | CSP-Adaptive-hi-95 | SeasonalNaive | SeasonalNaive-lo-80 | SeasonalNaive-lo-95 | SeasonalNaive-hi-80 | SeasonalNaive-hi-95 | | --- | ---------- | ------------------- | ------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------- | ------------------- | ------------------- | ------------------- | ------------------- | | 0 | MT\_002 | 2015-01-01 00:00:00 | 78.95 | 65.38 | 75.32 | 81.79 | 94.02 | 78.95 | 69.05 | 63.81 | 88.85 | 94.09 | | 1 | MT\_002 | 2015-01-01 01:00:00 | 73.26 | 60.46 | 68.85 | 79.79 | 83.94 | 73.26 | 63.36 | 58.12 | 83.16 | 88.40 | | 2 | MT\_002 | 2015-01-01 02:00:00 | 68.99 | 58.29 | 66.07 | 75.39 | 91.04 | 68.99 | 59.09 | 53.85 | 78.89 | 84.13 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 237 | MT\_012 | 2015-01-01 21:00:00 | 727.66 | 569.32 | 637.23 | 812.77 | 936.60 | 727.66 | 589.71 | 516.69 | 865.61 | 938.63 | | 238 | MT\_012 | 2015-01-01 22:00:00 | 691.49 | 482.98 | 595.53 | 783.36 | 951.49 | 691.49 | 553.54 | 480.52 | 829.44 | 902.46 | | 239 | MT\_012 | 2015-01-01 23:00:00 | 665.96 | 435.57 | 553.19 | 721.66 | 910.26 | 665.96 | 528.01 | 454.99 | 803.90 | 876.93 | The point forecast of CSP is *exactly* the seasonal naive forecast, and the value the model adds is the calibrated intervals around it: ```python theme={null} np.allclose(fcst["CSP-Adaptive"], fcst["SeasonalNaive"]) ``` ```text theme={null} True ``` ```python theme={null} sf.plot(df, fcst, level=[80, 95], max_insample_length=24 * 4, models=["CSP-Adaptive"]) ``` ## Evaluating calibration with the paper’s protocol The paper evaluates each method with a **rolling-origin protocol** using 7 non-overlapping evaluation windows per series, each one day long (`h=24`). `StatsForecast.cross_validation` implements exactly this. We request the levels `[20, 40, 60, 80]`, whose interval bounds correspond to the quantile grid $q \in \{0.1, 0.2, \ldots, 0.9\}$ used by the paper’s CRPS and quantile-loss metrics, plus level 95 for the coverage check. ```python theme={null} %%time cv_df = sf.cross_validation( df=df, h=horizon, n_windows=7, step_size=horizon, level=[20, 40, 60, 80, 95], ) cv_df ``` ```text theme={null} CPU times: user 116 ms, sys: 4.05 ms, total: 120 ms Wall time: 122 ms ``` | | unique\_id | ds | cutoff | y | CSP-Adaptive | CSP-Adaptive-lo-95 | CSP-Adaptive-lo-80 | CSP-Adaptive-lo-60 | CSP-Adaptive-lo-40 | CSP-Adaptive-lo-20 | ... | SeasonalNaive-lo-20 | SeasonalNaive-lo-40 | SeasonalNaive-lo-60 | SeasonalNaive-lo-80 | SeasonalNaive-lo-95 | SeasonalNaive-hi-20 | SeasonalNaive-hi-40 | SeasonalNaive-hi-60 | SeasonalNaive-hi-80 | SeasonalNaive-hi-95 | | ---- | ---------- | ------------------- | ------------------- | ------ | ------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | --- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | 0 | MT\_002 | 2014-12-25 00:00:00 | 2014-12-24 23:00:00 | 79.66 | 81.79 | 72.50 | 75.39 | 76.81 | 76.81 | 77.95 | ... | 79.83 | 77.74 | 75.28 | 71.88 | 66.63 | 83.75 | 85.85 | 88.30 | 91.70 | 96.95 | | 1 | MT\_002 | 2014-12-25 01:00:00 | 2014-12-24 23:00:00 | 81.08 | 72.55 | 66.09 | 68.28 | 69.70 | 69.70 | 71.12 | ... | 70.59 | 68.49 | 66.04 | 62.63 | 57.39 | 74.51 | 76.60 | 79.06 | 82.46 | 87.70 | | 2 | MT\_002 | 2014-12-25 02:00:00 | 2014-12-24 23:00:00 | 76.10 | 68.28 | 55.85 | 62.59 | 66.86 | 66.86 | 66.86 | ... | 66.32 | 64.22 | 61.77 | 58.37 | 53.12 | 70.24 | 72.33 | 74.79 | 78.19 | 83.44 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 1677 | MT\_012 | 2014-12-31 21:00:00 | 2014-12-30 23:00:00 | 727.66 | 806.38 | 597.53 | 634.47 | 696.60 | 761.70 | 768.09 | ... | 779.07 | 749.86 | 715.66 | 668.24 | 595.11 | 833.69 | 862.91 | 897.11 | 944.53 | 1017.66 | | 1678 | MT\_012 | 2014-12-31 22:00:00 | 2014-12-30 23:00:00 | 691.49 | 744.68 | 550.21 | 616.17 | 653.19 | 666.17 | 716.17 | ... | 717.37 | 688.15 | 653.96 | 606.54 | 533.41 | 771.99 | 801.21 | 835.40 | 882.83 | 955.96 | | 1679 | MT\_012 | 2014-12-31 23:00:00 | 2014-12-30 23:00:00 | 665.96 | 661.70 | 468.09 | 553.19 | 592.77 | 608.51 | 640.85 | ... | 634.39 | 605.17 | 570.98 | 523.56 | 450.43 | 689.01 | 718.23 | 752.43 | 799.85 | 872.98 | Note the wall-clock time above. The full 7-window evaluation of both models over ten series runs in about a second on CPU, matching the training-free speed the paper reports, while its deep baseline needs hours for the same protocol. We now compute the paper’s metrics with [utilsforecast](https://nixtlaverse.nixtla.io/utilsforecast). * **Scaled CRPS** and **mean quantile loss** over $q \in \{0.1, \ldots, 0.9\}$ are the headline distributional accuracy metrics. * **Empirical 95% coverage** is the fraction of observations inside the 95% interval, which should be close to 0.95 for a calibrated model. * **Mean 95% interval width** measures the sharpness of the intervals. ```python theme={null} import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate accuracy = evaluate( cv_df, metrics=[ufl.scaled_crps, ufl.mqloss], level=[20, 40, 60, 80], agg_fn="mean", ) accuracy.groupby("metric")[["CSP-Adaptive", "SeasonalNaive"]].mean() ``` | | CSP-Adaptive | SeasonalNaive | | ------------ | ------------ | ------------- | | metric | | | | mqloss | 22.66 | 28.77 | | scaled\_crps | 0.10 | 0.13 | ```python theme={null} coverage = evaluate(cv_df, metrics=[ufl.coverage], level=[95], agg_fn="mean") summary = coverage.groupby("metric")[["CSP-Adaptive", "SeasonalNaive"]].mean() summary.loc["mean 95% interval width"] = [ (cv_df[f"{m}-hi-95"] - cv_df[f"{m}-lo-95"]).mean() for m in ["CSP-Adaptive", "SeasonalNaive"] ] summary ``` | | CSP-Adaptive | SeasonalNaive | | ----------------------- | ------------ | ------------- | | metric | | | | coverage\_level95 | 0.93 | 0.84 | | mean 95% interval width | 306.75 | 257.12 | The results mirror the paper’s findings: **CSP’s empirical 95% coverage is close to the nominal level** (the paper reports a mean of 0.89 across its six datasets), while the parametric Gaussian intervals of `SeasonalNaive` undercover noticeably. All of this comes from a model with **zero trained parameters** that evaluates in a fraction of a second. ## Fixed vs. adaptive variant With `variant="fixed"`, the mixture weight is always $w=0.5$. The adaptive variant only deviates from this when seasonality is absent (`season_length` ≤ 1) or the seasonal pool is thin. On a series this long, the two variants behave identically. To see the adaptive rule act, we now forecast from a **short history** of two and a half days. Each hour of the day has been observed at most 3 times, and the adaptive rule shrinks the seasonal pool’s weight to 0.3, leaning more on the calibration residuals. ```python theme={null} short_series = df.query("unique_id == 'MT_002'").tail(60) # 2.5 days of history sf_variants = StatsForecast( models=[ ConformalSeasonalPool(season_length=season_length, variant="adaptive"), ConformalSeasonalPool(season_length=season_length, variant="fixed"), ], freq="h", ) fcst_short = sf_variants.forecast(df=short_series, h=horizon, level=[95]) pd.DataFrame({ "mean 95% width": [ (fcst_short[f"{m}-hi-95"] - fcst_short[f"{m}-lo-95"]).mean() for m in ["CSP-Adaptive", "CSP-Fixed"] ] }, index=["CSP-Adaptive", "CSP-Fixed"]) ``` | | mean 95% width | | ------------ | -------------- | | CSP-Adaptive | 31.06 | | CSP-Fixed | 29.96 | On this well-behaved series, the resulting widths are similar because the two sampling pools happen to agree. The adaptive safeguard matters when they don’t. With only a couple of same-season observations, a single outlier in the seasonal pool can distort the intervals, and down-weighting the pool keeps them stable. If in doubt, keep the default `variant="adaptive"`, as it reduces to the fixed rule whenever the history is rich enough. ## References * [Valery Manokhin (2026). “Training-Free Probabilistic Time-Series Forecasting with Conformal Seasonal Pools”.](https://arxiv.org/abs/2605.03789) * [`ConformalSeasonalPool` API reference.](../../src/core/models.html#conformalseasonalpool) * [`SeasonalNaive` API reference.](../../src/core/models.html#seasonalnaive) * [Conformal prediction intervals for any StatsForecast model.](../tutorials/conformalprediction.html) CSP is *natively* conformal. For models that are not, the `ConformalIntervals` wrapper described in that tutorial adds conformal intervals on top. * [Trindade, Artur (2015). “ElectricityLoadDiagrams20112014”. UCI Machine Learning Repository.](https://archive.ics.uci.edu/dataset/321/electricityloaddiagrams20112014) # CrostonClassic Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/crostonclassic.html > Step-by-step guide on using the `CrostonClassic Model` with > `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Croston Classic Model](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of CrostonClassic with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The Croston model is a method used in time series analysis to forecast demand in situations where there are intermittent data or frequent zeros. It was developed by J.D. Croston in 1972 and is especially useful in industries such as inventory management, retail sales, and demand forecasting for products with low sales frequency. The Croston model is based on two main components: 1. Intermittent Demand Rate: Calculates the demand rate for periods in which sales or events occur, ignoring periods without sales. This rate is used to estimate the probability that a claim will occur in the future. 2. Demand Interval: Calculates the time interval between sales or events occurring, again ignoring non-sales periods. This interval is used to estimate the probability that a demand will occur in the next period. The Croston model combines these two estimates to generate a weighted forecast that takes into account both the rate of intermittent demand and the interval between demands. This approach helps address the challenge of forecasting demand in situations where the time series has many zeros or missing values. It is important to note that the Croston model is a simplification and does not account for other possible sources of variability or patterns in the demand data. Therefore, its accuracy may be affected in situations where there are external factors or changes in demand behavior. ## Croston Classic Model ### What is intermittent demand? Intermittent demand is a demand pattern characterized by the irregular and sporadic occurrence of events or sales. In other words, it refers to situations in which the demand for a product or service occurs intermittently, with periods of time in which there are no sales or significant events. Intermittent demand differs from constant or regular demand, where sales occur in a predictable and consistent manner over time. In contrast, in intermittent demand, periods without sales may be long and there may not be a regular sequence of events. This type of demand can occur in different industries and contexts, such as low consumption products, seasonal products, high variability products, products with short life cycles, or in situations where demand depends on specific events or external factors. Intermittent demand can pose challenges in forecasting and inventory management, as it is difficult to predict when sales will occur and in what quantity. Methods like the Croston model, which I mentioned earlier, are used to address intermittent demand and generate more accurate and appropriate forecasts for this type of demand pattern. ### Problem with intermittent demand Intermittent demand can present various challenges and issues in inventory management and demand forecasting. Some of the common problems associated with intermittent demand are as follows: 1. Unpredictable variability: Intermittent demand can have unpredictable variability, making planning and forecasting difficult. Demand patterns can be irregular and fluctuate dramatically between periods with sales and periods without sales. 2. Low frequency of sales: Intermittent demand is characterized by long periods without sales. This can lead to inventory management difficulties, as it is necessary to hold enough stock to meet demand when it occurs, while avoiding excess inventory during non-sales periods. 3. Forecast error: Forecasting intermittent demand can be more difficult to pin down than constant demand. Traditional forecast models may not be adequate to capture the variability and lack of patterns in intermittent demand, which can lead to significant errors in estimates of future demand. 4. Impact on the supply chain: Intermittent demand can affect the efficiency of the supply chain and create difficulties in production planning, supplier management and logistics. Lead times and inventory levels must be adjusted to meet unpredictable demand. 5. Operating costs: Managing inventory in situations of intermittent demand can increase operating costs. Maintaining adequate inventory during non-sales periods and managing stock levels may require additional investments in storage and logistics. To address these issues, specific approaches to intermittent demand management are used, such as specialized forecasting models, product classification techniques, and tailored inventory strategies. These solutions seek to minimize the impacts of variability and lack of patterns in intermittent demand, optimizing inventory management and improving supply chain efficiency. ### Croston’s method(CR) Croston’s method(CR) is a classic method that specifically dealing with intermittent demand, it was developed base upon the Simple Exponential Smoothing method. When Croston dealing with the intermittent demand, he found out that by using the SES, the level of forecasting in each period’s demand are normally higher than it’s actual value, which lead to a very low accuracy. After a period of times of research, he came out a method that optimize the result of the intermittent demand forecasting. This method basically decompose the intermittent demand into two parts: the size of non-zero demand and the time interval of those demand occurred, and then apply the simple exponential smoothing on both part. Where the formula is follow: if $Z_t=0$ then: $Z'_t= Z'_{t-1}$ $P'_t= P'_{t-1}$ Otherwise $Z'_t=\alpha Z_t +(1-\alpha) Z'_{t-1}$ $P'_t=\alpha P_t +(1-\alpha) P'_{t-1}$ where $0< \alpha < 1$ And finally by combining these forecasts ${Y'}_t = \frac{{Z'}_t}{{P'}_t}$ Where * ${Y'}_t:$ Average demand per period. * $Z_t:$ Actual demand at period $t$. * $Z'_t:$ Time between two positive demand. * $P:$ Demand size forecast for next period. * $P_t:$ Forecast of demand interval. * $\alpha :$ Smoothing constant. Croston’s method converse the intermittent demand time series into a non-zero demand time series and a demand interval time series, many cases show that this method work quite well, but before apply Croston’s method, three assumptions should be made: * The non-zero demand are independent and obey normal distribution; * The demand intervals are independent and obey geometric distribution; * There are mutual independence between the demand size and demand intervals. According to many real cases show that, Croston’s method is suitable for the situation which the lead time obey normal distribution, for those demand series which contain large amount of zero values, Croston’s method did not shows a outstanding performance, sometimes even worse than SES method. Additionally, Croston’s method can only provide the average demand for each period, it can not give a forecast of the demand size for each period, it can not forecast which period will occurred a demand, and it also can not come out a probability of whether a period will occurred a demand. After all, although Croston’s method is a very classic and wide use method, it still has a lots of limitations, but after years of research carried by statisticians and scholars, few variations of Croston’s method were brought up. ### Croston’s variations Croston’s method is the main model used in demand forecasting area, most of the works are based upon this model. However, in 2001 Syntetos and Boylan proposed that Croston’s method is no a unbiased method, while some empirical evidence also showed that the losses in performance which use the Croston’s method (Sani and Kingsman, 1997). Plenty of further research is done in improving the Croston’s method. Syntetos and Boylan (2005) proposed an approximate unbiased procedure that provide less variance in the result of estimate, which is known as SBA (Syntetos and Boylan Approximate). Recently, Teunter et al. (2011) also proposed a intermittent forecasting method that can deal with obsolescence, which is based on Croston’s method known as TSB method (Teunter, Syntetos and Babai). ### Area of application of the Croston method The Croston method is commonly applied in the field of inventory management and demand forecasting in situations of intermittent demand. Some specific areas where the Croston model can be applied are: 1. Inventory management: The Croston model is used to forecast demand for products with sporadic or intermittent sales. Helps determine optimal inventory levels and replenishment policies, minimizing inventory costs and ensuring adequate availability to meet intermittent demand. 2. Retail sales: In the retail sector, especially in products with low sales frequency or irregular sales, the Croston model can be useful for forecasting demand and optimizing inventory planning in stores or warehouses. 3. Demand forecasting: In general, the Croston model is applied in demand forecasting when there is a lack of clear patterns or high variability in the time series. It can be used in various industries, such as the pharmaceutical industry, the automotive industry, the perishable goods industry, and other sectors where intermittent demand is common. 4. Supply Chain Planning: The Croston model can be used in supply chain planning and management to improve the accuracy of intermittent demand forecasts. This helps streamline production, inventory management, supplier order scheduling, and other aspects of the supply chain. It is important to note that Croston’s model is just one of many approaches available to address intermittent demand. Depending on the context and the specific characteristics of the time series, there may be other more appropriate methods and techniques. ### Croston Method for Stationary Time Series No, the time series in the Croston method does not have to be stationary. The Croston method is an effective forecasting method for intermittent time series, even if they are not stationary. However, if the time series is stationary, the Croston method may be more accurate. The Croston method is based on the idea that intermittent time series can be decomposed into two components: a demand component and a time between demands component. The demand component is forecast using a standard time series forecasting method, such as single or double exponential smoothing. The time component between demands is forecast using a probability distribution function, such as a Poisson distribution or a Weibull distribution. The Croston method then combines the forecasts for the two components to obtain a total demand forecast for the next period. If the time series is stationary, the two components of the time series will be stationary as well. This means that the Croston method will be able to forecast the two components more accurately. However, even if the time series is not stationary, the Croston method can still be an effective forecasting method. The Croston method is a robust method that can handle time series with irregular demand patterns. If you are using the Croston method to forecast an intermittent time series that is not stationary, it is important to choose a standard time series forecast method that is effective for nonstationary time series. Double exponential smoothing is an effective forecasting method for non-stationary time series. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/intermittend_demand2") df.head() ``` | | date | sales | | - | ------------------- | ----- | | 0 | 2022-01-01 00:00:00 | 0 | | 1 | 2022-01-01 01:00:00 | 10 | | 2 | 2022-01-01 02:00:00 | 0 | | 3 | 2022-01-01 03:00:00 | 0 | | 4 | 2022-01-01 04:00:00 | 100 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | --- | ---------- | | 0 | 2022-01-01 00:00:00 | 0 | 1 | | 1 | 2022-01-01 01:00:00 | 10 | 1 | | 2 | 2022-01-01 02:00:00 | 0 | 1 | | 3 | 2022-01-01 03:00:00 | 0 | 1 | | 4 | 2022-01-01 04:00:00 | 100 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose from plotly.subplots import make_subplots import plotly.graph_objects as go def plotSeasonalDecompose( x, model='additive', filt=None, period=None, two_sided=True, extrapolate_trend=0, title="Seasonal Decomposition"): result = seasonal_decompose( x, model=model, filt=filt, period=period, two_sided=two_sided, extrapolate_trend=extrapolate_trend) fig = make_subplots( rows=4, cols=1, subplot_titles=["Observed", "Trend", "Seasonal", "Residuals"]) for idx, col in enumerate(['observed', 'trend', 'seasonal', 'resid']): fig.add_trace( go.Scatter(x=result.observed.index, y=getattr(result, col), mode='lines'), row=idx+1, col=1, ) return fig ``` ```python theme={null} plotSeasonalDecompose( df["y"], model="additive", period=24, title="Seasonal Decomposition") ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Croston Classic Model`. 2. Data to test our model For the test data we will use the last 500 hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2023-01-31 19:00:00'] test = df[df.ds>'2023-01-31 19:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((9500, 3), (500, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train", linestyle="--",linewidth=2) sns.lineplot(test, x="ds", y="y", label="Test", linewidth=2, color="yellow") plt.title("Store visit"); plt.show() ``` ## Implementation of CrostonClassic with StatsForecast To also know more about the parameters of the functions of the `CrostonClassic Model`, they are listed below. For more information, visit the [documentation](../../src/core/models.html#crostonclassic) ```text theme={null} alias : str Custom name of the model. ``` ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import CrostonClassic ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 24 # Hourly data horizon = len(test) # number of predictions models = [CrostonClassic()] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[CrostonClassic]) ``` Let’s see the results of our `Croston Classic Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'mean': array([27.41841685]), 'fitted': array([ nan, 0. , 5. , ..., 30.61961, 30.61961, 30.61961], dtype=float32), 'sigma': np.float32(49.5709)} ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 25 week ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon) Y_hat ``` | | unique\_id | ds | CrostonClassic | | --- | ---------- | ------------------- | -------------- | | 0 | 1 | 2023-01-31 20:00:00 | 27.418417 | | 1 | 1 | 2023-01-31 21:00:00 | 27.418417 | | 2 | 1 | 2023-01-31 22:00:00 | 27.418417 | | ... | ... | ... | ... | | 497 | 1 | 2023-02-21 13:00:00 | 27.418417 | | 498 | 1 | 2023-02-21 14:00:00 | 27.418417 | | 499 | 1 | 2023-02-21 15:00:00 | 27.418417 | ```python theme={null} sf.plot(train, Y_hat, max_insample_length=500) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 500 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecast_df = sf.predict(h=horizon) forecast_df ``` | | unique\_id | ds | CrostonClassic | | --- | ---------- | ------------------- | -------------- | | 0 | 1 | 2023-01-31 20:00:00 | 27.418417 | | 1 | 1 | 2023-01-31 21:00:00 | 27.418417 | | 2 | 1 | 2023-01-31 22:00:00 | 27.418417 | | ... | ... | ... | ... | | 497 | 1 | 2023-02-21 13:00:00 | 27.418417 | | 498 | 1 | 2023-02-21 14:00:00 | 27.418417 | | 499 | 1 | 2023-02-21 15:00:00 | 27.418417 | ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second hour `(step_size=50)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents $h$ steps into the future that are being forecasted. In this case, 500 hours ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=50, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier. * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | CrostonClassic | | ---- | ---------- | ------------------- | ------------------- | ---- | -------------- | | 0 | 1 | 2023-01-23 12:00:00 | 2023-01-23 11:00:00 | 0.0 | 23.655830 | | 1 | 1 | 2023-01-23 13:00:00 | 2023-01-23 11:00:00 | 0.0 | 23.655830 | | 2 | 1 | 2023-01-23 14:00:00 | 2023-01-23 11:00:00 | 0.0 | 23.655830 | | ... | ... | ... | ... | ... | ... | | 2497 | 1 | 2023-02-21 13:00:00 | 2023-01-31 19:00:00 | 60.0 | 27.418417 | | 2498 | 1 | 2023-02-21 14:00:00 | 2023-01-31 19:00:00 | 20.0 | 27.418417 | | 2499 | 1 | 2023-02-21 15:00:00 | 2023-01-31 19:00:00 | 20.0 | 27.418417 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | CrostonClassic | | - | ---------- | ------ | -------------- | | 0 | 1 | mae | 33.704756 | | 1 | 1 | mape | 0.632593 | | 2 | 1 | mase | 0.804074 | | 3 | 1 | rmse | 45.262709 | | 4 | 1 | smape | 0.767960 | # References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla CrostonClassic API](../../src/core/models.html#crostonclassic) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # CrostonOptimized Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/crostonoptimized.html > Step-by-step guide on using the `CrostonOptimized Model` with > `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Croston Optimized Model](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of CrostonOptimized with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The Croston Optimized model is a forecasting method designed for intermittent demand time series data. It is an extension of the Croston’s method, which was originally developed for forecasting sporadic demand patterns. Intermittent demand time series are characterized by irregular and sporadic occurrences of non-zero demand values, often with long periods of zero demand. Traditional forecasting methods may struggle to handle such patterns effectively. The Croston Optimized model addresses this challenge by incorporating two key components: exponential smoothing and intermittent demand estimation. 1. Exponential Smoothing: The Croston Optimized model uses exponential smoothing to capture the trend and seasonality in the intermittent demand data. This helps in identifying the underlying patterns and making more accurate forecasts. 2. Intermittent Demand Estimation: Since intermittent demand data often consists of long periods of zero demand, the Croston Optimized model employs a separate estimation process for the occurrence and size of non-zero demand values. It estimates the probability of occurrence and the average size of non-zero demand intervals, enabling better forecasting of intermittent demand. The Croston Optimized model aims to strike a balance between over-forecasting and under-forecasting intermittent demand, which are common challenges in traditional forecasting methods. By explicitly modeling intermittent demand patterns, it can provide more accurate forecasts for intermittent demand time series data. It is worth noting that there are variations and adaptations of the Croston Optimized model, with different modifications and enhancements made to suit specific forecasting scenarios. These variations may incorporate additional features or algorithms to further improve the accuracy of the forecasts. ## Croston Optimized method The Croston Optimized model can be mathematically defined as follows: 1. Initialization: * Let $(y_t)$ represent the intermittent demand time series data at time $t$. * Initialize two sets of variables: $(p_t)$ for the probability of occurrence and $(q_t)$ for the average size of non-zero demand intervals. * Initialize the forecast $(F_t)$ and forecast error $(E_t)$ variables as zero. 2. Calculation of $(p_t)$ and $(q_t)$: * Calculate the intermittent demand occurrence probability $(p_t)$ using exponential smoothing: $[p_t = \alpha + (1 - \alpha)(p_{t-1}),]$ where $(\alpha)$ is the smoothing parameter (typically set between 0.1 and 0.3). * Calculate the average size of non-zero demand intervals $(q_t)$ using exponential smoothing: $[q_t = \beta \cdot y_t + (1 - \beta)(q_{t-1}),]$ where $(\beta)$ is the smoothing parameter (typically set between 0.1 and 0.3). 3. Forecasting: * If $(y_t > 0)$ (non-zero demand occurrence): * Calculate the forecast $(F_t)$ as the previous forecast $(F_{t-1})$ divided by the average size of non-zero demand intervals $(q_{t-1})$: $[F_t = \frac{{F_{t-1}}}{{q_{t-1}}}]$ * Calculate the forecast error $(E_t)$ as the difference between the actual demand $(y_t)$ and the forecast $(F_t)$: $[E_t = y_t - F_t]$ * If $(y_t = 0)$ (zero demand occurrence): * Set the forecast $(F_t)$ and forecast error $(E_t)$ as zero. 4. Updating the model: * Update the intermittent demand occurrence probability $(p_t)$ and the average size of non-zero demand intervals $(q_t)$ using exponential smoothing as described in step 2. 5. Repeat steps 3 and 4 for each time point in the time series. The Croston Optimized model leverages exponential smoothing to capture the trend and seasonality in the intermittent demand data, and it estimates the occurrence probability and average size of non-zero demand intervals separately to handle intermittent demand patterns effectively. By updating the model parameters based on the observed data, it provides improved forecasts for intermittent demand time series. ### Some properties of the Optimized Croston Model The optimized Croston model is a modification of the classic Croston model used to forecast intermittent demand. The classic Croston model forecasts demand using a weighted average of historical orders and the average interval between orders. The optimized Croston model uses a probability function to forecast the mean interval between orders. The optimized Croston model has been shown to be more accurate than the classical Croston model for time series with irregular demand. The optimized Croston model is also more adaptable to different types of intermittent time series. The optimized Croston model has the following properties: * It is accurate, even for time series with irregular demand. * It is adaptable to different types of intermittent time series. * It is easy to implement and understand. * It is robust to outliers. The optimized Croston model has been used successfully to forecast a wide range of intermittent time series, including product demand, service demand, and resource demand. Here are some of the properties of the optimized Croston model: * **Precision:** The optimized Croston model has been shown to be more accurate than the classic Croston model for time series with irregular demand. This is because the optimized Croston model uses a probability function to forecast the average interval between orders, which is more accurate than the weighted average of historical orders. * **Adaptability:** The optimized Croston model is also more adaptable to different types of intermittent time series. This is because the optimized Croston model uses a probability function to forecast the mean interval between orders, allowing it to accommodate different demand patterns. * **Ease of Implementation and Understanding:** The optimized Croston model is easy to implement and understand. This is because the optimized Croston model is a modification of the classical Croston model, which is a well-known and well-understood model. * **Robustness:** The optimized Croston model is also robust to outliers. This is because the optimized Croston model uses a probability function to forecast the mean interval between orders, which allows it to ignore outliers. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf import plotly.graph_objects as go plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/intermittend_demand2") df.head() ``` | | date | sales | | - | ------------------- | ----- | | 0 | 2022-01-01 00:00:00 | 0 | | 1 | 2022-01-01 01:00:00 | 10 | | 2 | 2022-01-01 02:00:00 | 0 | | 3 | 2022-01-01 03:00:00 | 0 | | 4 | 2022-01-01 04:00:00 | 100 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | --- | ---------- | | 0 | 2022-01-01 00:00:00 | 0 | 1 | | 1 | 2022-01-01 01:00:00 | 10 | 1 | | 2 | 2022-01-01 02:00:00 | 0 | 1 | | 3 | 2022-01-01 03:00:00 | 0 | 1 | | 4 | 2022-01-01 04:00:00 | 100 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots Autocorrelation (ACF) and partial autocorrelation (PACF) plots are statistical tools used to analyze time series. ACF charts show the correlation between the values of a time series and their lagged values, while PACF charts show the correlation between the values of a time series and their lagged values, after the effect of previous lagged values has been removed. ACF and PACF charts can be used to identify the structure of a time series, which can be helpful in choosing a suitable model for the time series. For example, if the ACF chart shows a repeating peak and valley pattern, this indicates that the time series is stationary, meaning that it has the same statistical properties over time. If the PACF chart shows a pattern of rapidly decreasing spikes, this indicates that the time series is invertible, meaning it can be reversed to get a stationary time series. The importance of the ACF and PACF charts is that they can help analysts better understand the structure of a time series. This understanding can be helpful in choosing a suitable model for the time series, which can improve the ability to predict future values of the time series. To analyze ACF and PACF charts: * Look for patterns in charts. Common patterns include repeating peaks and valleys, sawtooth patterns, and plateau patterns. * Compare ACF and PACF charts. The PACF chart generally has fewer spikes than the ACF chart. * Consider the length of the time series. ACF and PACF charts for longer time series will have more spikes. * Use a confidence interval. The ACF and PACF plots also show confidence intervals for the autocorrelation values. If an autocorrelation value is outside the confidence interval, it is likely to be significant. ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from plotly.subplots import make_subplots ``` ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose def plotSeasonalDecompose( x, model='additive', filt=None, period=None, two_sided=True, extrapolate_trend=0, title="Seasonal Decomposition"): result = seasonal_decompose( x, model=model, filt=filt, period=period, two_sided=two_sided, extrapolate_trend=extrapolate_trend) fig = make_subplots( rows=4, cols=1, subplot_titles=["Observed", "Trend", "Seasonal", "Residuals"]) for idx, col in enumerate(['observed', 'trend', 'seasonal', 'resid']): fig.add_trace( go.Scatter(x=result.observed.index, y=getattr(result, col), mode='lines'), row=idx+1, col=1, ) return fig ``` ```python theme={null} plotSeasonalDecompose( df["y"], model="additive", period=24, title="Seasonal Decomposition") ``` ## Split the data into training and testing Let’s divide our data into sets Let’s divide our data into sets 1. Data to train our `Croston Optimized Model`. 2. Data to test our model For the test data we will use the last 500 Hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2023-01-31 19:00:00'] test = df[df.ds>'2023-01-31 19:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((9500, 3), (500, 3)) ``` ## Implementation of CrostonOptimized with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import CrostonOptimized ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 24 # Hourly data horizon = len(test) # number of predictions # We call the model that we are going to use models = [CrostonOptimized()] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} # fit the models sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[CrostonOptimized]) ``` Let’s see the results of our `Croston optimized Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'mean': array([27.41841685])} ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 500 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon) Y_hat ``` | | unique\_id | ds | CrostonOptimized | | --- | ---------- | ------------------- | ---------------- | | 0 | 1 | 2023-01-31 20:00:00 | 27.418417 | | 1 | 1 | 2023-01-31 21:00:00 | 27.418417 | | 2 | 1 | 2023-01-31 22:00:00 | 27.418417 | | ... | ... | ... | ... | | 497 | 1 | 2023-02-21 13:00:00 | 27.418417 | | 498 | 1 | 2023-02-21 14:00:00 | 27.418417 | | 499 | 1 | 2023-02-21 15:00:00 | 27.418417 | ```python theme={null} sf.plot(train, Y_hat, max_insample_length=500) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 500 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecast_df = sf.predict(h=horizon) forecast_df ``` | | unique\_id | ds | CrostonOptimized | | --- | ---------- | ------------------- | ---------------- | | 0 | 1 | 2023-01-31 20:00:00 | 27.418417 | | 1 | 1 | 2023-01-31 21:00:00 | 27.418417 | | 2 | 1 | 2023-01-31 22:00:00 | 27.418417 | | ... | ... | ... | ... | | 497 | 1 | 2023-02-21 13:00:00 | 27.418417 | | 498 | 1 | 2023-02-21 14:00:00 | 27.418417 | | 499 | 1 | 2023-02-21 15:00:00 | 27.418417 | ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second months `(step_size=50)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 500 hours ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=50, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | CrostonOptimized | | ---- | ---------- | ------------------- | ------------------- | ---- | ---------------- | | 0 | 1 | 2023-01-23 12:00:00 | 2023-01-23 11:00:00 | 0.0 | 23.655830 | | 1 | 1 | 2023-01-23 13:00:00 | 2023-01-23 11:00:00 | 0.0 | 23.655830 | | 2 | 1 | 2023-01-23 14:00:00 | 2023-01-23 11:00:00 | 0.0 | 23.655830 | | ... | ... | ... | ... | ... | ... | | 2497 | 1 | 2023-02-21 13:00:00 | 2023-01-31 19:00:00 | 60.0 | 27.418417 | | 2498 | 1 | 2023-02-21 14:00:00 | 2023-01-31 19:00:00 | 20.0 | 27.418417 | | 2499 | 1 | 2023-02-21 15:00:00 | 2023-01-31 19:00:00 | 20.0 | 27.418417 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | CrostonOptimized | | - | ---------- | ------ | ---------------- | | 0 | 1 | mae | 33.704756 | | 1 | 1 | mape | 0.632593 | | 2 | 1 | mase | 0.804074 | | 3 | 1 | rmse | 45.262709 | | 4 | 1 | smape | 0.767960 | # References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla CrostonOptimized API](../../src/core/models.html#crostonoptimized) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # CrostonSBA Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/crostonsba.html > Step-by-step guide on using the `CrostonSBA Model` with > `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Croston SBA Model](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of CrostonSBA with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The Croston model is a method used to forecast time series with intermittent demand data, that is, data that has many periods of zero demand and only a few periods of non-zero demand. Croston’s approach was originally proposed by J.D. Croston in 1972. Subsequently, Syntetos and Boylan proposed an improvement to the original model in 2001, known as the Croston-SBA (Syntetos and Boylan Approximation). The Croston-SBA model is based on the assumption that intermittent demand follows a binomial process. Instead of directly modeling demand, the focus is on modeling the intervals between demand periods. The model has two main components: one to model the intervals between demand periods (which are assumed to follow a Poisson distribution), and another to model the demands when they occur. It is important to note that the Croston-SBA model assumes that the intervals between the non-zero demand periods are independent and follow a Poisson distribution. However, this model is an approximation and may not work well in all situations. It is advisable to evaluate its performance on historical data before using it in practice. ## Croston SBA Model The formula of SBA is very similar to the original Croston’s method, however, it apply a correction factor which reduce the error in the final estimate result. if $Z_t=0$ then $Z'_t=Z'_{t-1}$ $P'_t=P'_{t-1}$ Otherwise $Z'_t=\alpha Z_t +(1-\alpha)Z'_{t-1}$ $P'_t=\alpha P_t +(1- \alpha) P'_{t-1}\ where \ 0<\alpha < 1$ $Y'_t=(1-\frac{\alpha}{2}) \frac{Z'_t}{P'_t}$ where * $Y'_t:$ Average demand per period * $Z_t:$ Actual demand at period $t$ * $Z'_t:$ Time between two positive demand * $P:$ Demand size forecast for next period * $P'_t:$ Forecast of demand interval * $\alpha:$ Smoothing constant Note: In Croston’s method, result often will present a considerable positive bias, whereas in SBA the bias is reduced, and sometimes will appear slightly negative bias. ### Principals of the Croston SBA method The Croston SBA (Syntetos and Boylan Approximate) method is a technique used for forecasting time series with intermittent or sporadic data. This methodology is based on the original Croston method, which was developed to forecast inventory demand in situations where data is sparse or not available at regular intervals. The main properties of the Croston SBA method are the following: 1. Suitable for intermittent data: The Croston SBA method is especially useful when the data exhibits intermittent patterns, that is, periods of demand followed by periods of non-demand. Instead of treating the data as zero for non-demand periods, the Croston SBA method estimates demand occurrence rates and conditional demand rates. 2. Separation of frequency and level: One of the key features of the Croston SBA method is that it separates the frequency and level information in the demand data. This allows these two components to be modeled and forecasted separately, which can result in better predictions. 3. Estimation of occurrence and demand rates: The Croston SBA method uses a simple exponential smoothing technique to estimate conditional occurrence and demand rates. These rates are then used to forecast future demand. 4. Does not assume distribution of the data: Unlike some forecasting techniques that assume a specific distribution of the data, the Croston SBA method makes no assumptions about the distribution of demand. This makes it more flexible and applicable to a wide range of situations. 5. Does not require complete historical data: The Croston SBA method can work even when historical data is sparse or not available at regular intervals. This makes it an attractive option when it comes to forecasting intermittent demand with limited data. It is important to note that the Croston SBA method is an approximation and may not be suitable for all cases. It is recommended to evaluate its performance in conjunction with other forecasting techniques and adapt it according to the specific characteristics of the data and the context of the problem. In the Croston SBA method, the data series need not be stationary. The Croston SBA approach is suitable for forecasting time series with intermittent data, where periods of demand are interspersed with periods of non-demand. The Croston SBA method is based on the estimation of occurrence rates and conditional demand rates, using simple exponential smoothing techniques. These rates are used to forecast future demand. In the context of time series, stationarity refers to the property that the statistical properties of the series, such as the mean and variance, are constant over time. However, in the case of intermittent data, it is common for the series not to meet the assumptions of stationarity, since the demand can vary considerably in different periods of time. The Croston SBA method is not based on the assumption of stationarity of the data series. Instead, it focuses on modeling the frequency and level of intermittent demand separately, using simple exponential smoothing techniques. This makes it possible to capture demand occurrence patterns and estimate conditional demand rates, without requiring the stationarity of the series. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/intermittend_demand2") df.head() ``` | | date | sales | | - | ------------------- | ----- | | 0 | 2022-01-01 00:00:00 | 0 | | 1 | 2022-01-01 01:00:00 | 10 | | 2 | 2022-01-01 02:00:00 | 0 | | 3 | 2022-01-01 03:00:00 | 0 | | 4 | 2022-01-01 04:00:00 | 100 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | --- | ---------- | | 0 | 2022-01-01 00:00:00 | 0 | 1 | | 1 | 2022-01-01 01:00:00 | 10 | 1 | | 2 | 2022-01-01 02:00:00 | 0 | 1 | | 3 | 2022-01-01 03:00:00 | 0 | 1 | | 4 | 2022-01-01 04:00:00 | 100 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots Autocorrelation (ACF) and partial autocorrelation (PACF) plots are statistical tools used to analyze time series. ACF charts show the correlation between the values of a time series and their lagged values, while PACF charts show the correlation between the values of a time series and their lagged values, after the effect of previous lagged values has been removed. ACF and PACF charts can be used to identify the structure of a time series, which can be helpful in choosing a suitable model for the time series. For example, if the ACF chart shows a repeating peak and valley pattern, this indicates that the time series is stationary, meaning that it has the same statistical properties over time. If the PACF chart shows a pattern of rapidly decreasing spikes, this indicates that the time series is invertible, meaning it can be reversed to get a stationary time series. The importance of the ACF and PACF charts is that they can help analysts better understand the structure of a time series. This understanding can be helpful in choosing a suitable model for the time series, which can improve the ability to predict future values of the time series. To analyze ACF and PACF charts: * Look for patterns in charts. Common patterns include repeating peaks and valleys, sawtooth patterns, and plateau patterns. * Compare ACF and PACF charts. The PACF chart generally has fewer spikes than the ACF chart. * Consider the length of the time series. ACF and PACF charts for longer time series will have more spikes. * Use a confidence interval. The ACF and PACF plots also show confidence intervals for the autocorrelation values. If an autocorrelation value is outside the confidence interval, it is likely to be significant. ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose from plotly.subplots import make_subplots import plotly.graph_objects as go def plotSeasonalDecompose( x, model='additive', filt=None, period=None, two_sided=True, extrapolate_trend=0, title="Seasonal Decomposition"): result = seasonal_decompose( x, model=model, filt=filt, period=period, two_sided=two_sided, extrapolate_trend=extrapolate_trend) fig = make_subplots( rows=4, cols=1, subplot_titles=["Observed", "Trend", "Seasonal", "Residuals"]) for idx, col in enumerate(['observed', 'trend', 'seasonal', 'resid']): fig.add_trace( go.Scatter(x=result.observed.index, y=getattr(result, col), mode='lines'), row=idx+1, col=1, ) return fig ``` ```python theme={null} plotSeasonalDecompose( df["y"], model="additive", period=24, title="Seasonal Decomposition") ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Croston SBA Model`. 2. Data to test our model For the test data we will use the last 500 Hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2023-01-31 19:00:00'] test = df[df.ds>'2023-01-31 19:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((9500, 3), (500, 3)) ``` ## Implementation of CrostonSBA with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import CrostonSBA ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 24 # Hourly data horizon = len(test) # number of predictions # We call the model that we are going to use models = [CrostonSBA()] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[CrostonSBA]) ``` Let’s see the results of our `Croston SBA Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'mean': array([26.04749601]), 'fitted': array([ nan, 0. , 4.75 , ..., 29.088629, 29.088629, 29.088629], dtype=float32), 'sigma': np.float32(49.512943)} ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 500 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like `ARIMA` and `Theta`) ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon) Y_hat ``` | | unique\_id | ds | CrostonSBA | | --- | ---------- | ------------------- | ---------- | | 0 | 1 | 2023-01-31 20:00:00 | 26.047497 | | 1 | 1 | 2023-01-31 21:00:00 | 26.047497 | | 2 | 1 | 2023-01-31 22:00:00 | 26.047497 | | ... | ... | ... | ... | | 497 | 1 | 2023-02-21 13:00:00 | 26.047497 | | 498 | 1 | 2023-02-21 14:00:00 | 26.047497 | | 499 | 1 | 2023-02-21 15:00:00 | 26.047497 | ```python theme={null} sf.plot(train, Y_hat, max_insample_length=500) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 500 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecast_df = sf.predict(h=horizon) forecast_df ``` | | unique\_id | ds | CrostonSBA | | --- | ---------- | ------------------- | ---------- | | 0 | 1 | 2023-01-31 20:00:00 | 26.047497 | | 1 | 1 | 2023-01-31 21:00:00 | 26.047497 | | 2 | 1 | 2023-01-31 22:00:00 | 26.047497 | | ... | ... | ... | ... | | 497 | 1 | 2023-02-21 13:00:00 | 26.047497 | | 498 | 1 | 2023-02-21 14:00:00 | 26.047497 | | 499 | 1 | 2023-02-21 15:00:00 | 26.047497 | ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second months `(step_size=50)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 500 hours ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=50, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | CrostonSBA | | ---- | ---------- | ------------------- | ------------------- | ---- | ---------- | | 0 | 1 | 2023-01-23 12:00:00 | 2023-01-23 11:00:00 | 0.0 | 22.473040 | | 1 | 1 | 2023-01-23 13:00:00 | 2023-01-23 11:00:00 | 0.0 | 22.473040 | | 2 | 1 | 2023-01-23 14:00:00 | 2023-01-23 11:00:00 | 0.0 | 22.473040 | | ... | ... | ... | ... | ... | ... | | 2497 | 1 | 2023-02-21 13:00:00 | 2023-01-31 19:00:00 | 60.0 | 26.047497 | | 2498 | 1 | 2023-02-21 14:00:00 | 2023-01-31 19:00:00 | 20.0 | 26.047497 | | 2499 | 1 | 2023-02-21 15:00:00 | 2023-01-31 19:00:00 | 20.0 | 26.047497 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | CrostonSBA | | - | ---------- | ------ | ---------- | | 0 | 1 | mae | 33.112519 | | 1 | 1 | mape | 0.626900 | | 2 | 1 | mase | 0.789945 | | 3 | 1 | rmse | 45.203519 | | 4 | 1 | smape | 0.771529 | # References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla CrostonSBA API](../../src/core/models.html#crostonsba) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Dynamic Optimized Theta Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/dynamicoptimizedtheta.html > Step-by-step guide on using the `DynamicOptimizedTheta Model` with > `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). ## Table of Contents * [Introduction](#introduction) * [Dynamic Optimized Theta Model (DOTM)](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of DynamicOptimizedTheta with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The **Dynamic Optimized Theta Model (DOTM)** in `StatsForecast` is a variation of the classic Theta model. It combines key features of two other extensions: the **Optimized Theta Model (OTM)** and the **Dynamic Standard Theta Model (DSTM)**. DOTM introduces two main improvements over the standard Theta model: **optimization** of the theta parameters and **dynamic updating** of model components over time. * **Optimization**: Like OTM, this version automatically searches for the best theta values based on the data, rather than relying on fixed parameters. This flexibility allows the model to better adapt to series with complex seasonal or trend patterns. * **Dynamic updating**: Like DSTM, DOTM continuously updates its internal components as new data becomes available. This makes it well-suited for non-stationary series, where the underlying data structure evolves over time. DOTM also supports **seasonal decomposition**, controlled by the `decomposition_type` parameter. You can choose between: - `'multiplicative'` (default), which assumes that seasonal effects scale with the level of the series, or - `'additive'`, which assumes that seasonal effects remain constant in absolute magnitude. The Dynamic Optimized Theta Model is the most flexible of the Theta family and is particularly effective when forecasting series with changing trends and seasonalities. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} import pandas as pd df = pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/milk_production.csv", usecols=[1,2]) df.head() ``` | | month | production | | - | ---------- | ---------- | | 0 | 1962-01-01 | 589 | | 1 | 1962-02-01 | 561 | | 2 | 1962-03-01 | 640 | | 3 | 1962-04-01 | 656 | | 4 | 1962-05-01 | 727 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ---------- | --- | ---------- | | 0 | 1962-01-01 | 589 | 1 | | 1 | 1962-02-01 | 561 | 1 | | 2 | 1962-03-01 | 640 | 1 | | 3 | 1962-04-01 | 656 | 1 | | 4 | 1962-05-01 | 727 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ### Additive ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "additive", period=12) a.plot(); ``` ### Multiplicative ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "Multiplicative", period=12) a.plot(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Dynamic Optimized Theta Model(DOTM)`. 2. Data to test our model For the test data we will use the last 12 months to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='1974-12-01'] test = df[df.ds>'1974-12-01'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((156, 3), (12, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train", linestyle="--") sns.lineplot(test, x="ds", y="y", label="Test") plt.title("Monthly Milk Production"); plt.show() ``` ## Implementation of DynamicOptimizedTheta with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import DynamicOptimizedTheta ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 12 # Monthly data horizon = len(test) # number of predictions # We call the model that we are going to use models = [DynamicOptimizedTheta(season_length=season_length, decomposition_type="additive")] # multiplicative additive ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='MS') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[DynamicOptimizedTheta]) ``` Let’s see the results of our `Dynamic Optimized Theta Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ print(result.keys()) print(result['fit']) ``` ```text theme={null} dict_keys(['mse', 'amse', 'fit', 'residuals', 'm', 'states', 'par', 'n', 'modeltype', 'mean_y', 'decompose', 'decomposition_type', 'seas_forecast', 'fitted']) results(x=array([250.83206219, 0.75624902, 4.67964777]), fn=10.697554045462667, nit=55, simplex=array([[237.42074763, 0.75306547, 4.46023813], [250.83206219, 0.75624902, 4.67964777], [257.16444246, 0.75229688, 4.42377059], [256.90853867, 0.75757957, 4.43171897]])) ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("residuals"), columns=["residual Model"]) residual ``` | | residual Model | | --- | -------------- | | 0 | -18.247106 | | 1 | -75.757706 | | 2 | 6.001494 | | ... | ... | | 153 | -59.747044 | | 154 | -91.901521 | | 155 | -43.503294 | ```python theme={null} import scipy.stats as stats fig, axs = plt.subplots(nrows=2, ncols=2) residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} # Prediction Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | DynamicOptimizedTheta | | --- | ---------- | ---------- | --------------------- | | 0 | 1 | 1975-01-01 | 839.259705 | | 1 | 1 | 1975-02-01 | 801.399170 | | 2 | 1 | 1975-03-01 | 895.189148 | | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 821.271240 | | 10 | 1 | 1975-11-01 | 792.530518 | | 11 | 1 | 1975-12-01 | 829.854553 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | DynamicOptimizedTheta | | - | ---------- | ---------- | ----- | --------------------- | | 0 | 1 | 1962-01-01 | 589.0 | 607.247131 | | 1 | 1 | 1962-02-01 | 561.0 | 636.757690 | | 2 | 1 | 1962-03-01 | 640.0 | 633.998535 | | 3 | 1 | 1962-04-01 | 656.0 | 608.461243 | | 4 | 1 | 1962-05-01 | 727.0 | 604.808899 | ```python theme={null} StatsForecast.plot(values) ``` Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | DynamicOptimizedTheta | DynamicOptimizedTheta-lo-95 | DynamicOptimizedTheta-hi-95 | | --- | ---------- | ---------- | --------------------- | --------------------------- | --------------------------- | | 0 | 1 | 1975-01-01 | 839.259705 | 741.952332 | 955.151001 | | 1 | 1 | 1975-02-01 | 801.399170 | 641.867920 | 946.045776 | | 2 | 1 | 1975-03-01 | 895.189148 | 707.189087 | 1066.356812 | | ... | ... | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 821.271240 | 546.081726 | 1088.193481 | | 10 | 1 | 1975-11-01 | 792.530518 | 494.623718 | 1037.459839 | | 11 | 1 | 1975-12-01 | 829.854553 | 519.661133 | 1108.213867 | ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | DynamicOptimizedTheta | | --- | ---------- | ---------- | --------------------- | | 0 | 1 | 1975-01-01 | 839.259705 | | 1 | 1 | 1975-02-01 | 801.399170 | | 2 | 1 | 1975-03-01 | 895.189148 | | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 821.271240 | | 10 | 1 | 1975-11-01 | 792.530518 | | 11 | 1 | 1975-12-01 | 829.854553 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[80,95]) forecast_df ``` | | unique\_id | ds | DynamicOptimizedTheta | DynamicOptimizedTheta-lo-80 | DynamicOptimizedTheta-hi-80 | DynamicOptimizedTheta-lo-95 | DynamicOptimizedTheta-hi-95 | | --- | ---------- | ---------- | --------------------- | --------------------------- | --------------------------- | --------------------------- | --------------------------- | | 0 | 1 | 1975-01-01 | 839.259705 | 766.142090 | 928.025513 | 741.952332 | 955.151001 | | 1 | 1 | 1975-02-01 | 801.399170 | 702.981262 | 899.884216 | 641.867920 | 946.045776 | | 2 | 1 | 1975-03-01 | 895.189148 | 760.125916 | 1008.335022 | 707.189087 | 1066.356812 | | ... | ... | ... | ... | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 821.271240 | 617.391724 | 996.698364 | 546.081726 | 1088.193481 | | 10 | 1 | 1975-11-01 | 792.530518 | 568.303162 | 975.070312 | 494.623718 | 1037.459839 | | 11 | 1 | 1975-12-01 | 829.854553 | 598.098267 | 1035.476196 | 519.661133 | 1108.213867 | ```python theme={null} sf.plot(train, test.merge(forecast_df), level=[80, 95]) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=5)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=train, h=horizon, step_size=12, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | DynamicOptimizedTheta | | --- | ---------- | ---------- | ---------- | ----- | --------------------- | | 0 | 1 | 1972-01-01 | 1971-12-01 | 826.0 | 828.692017 | | 1 | 1 | 1972-02-01 | 1971-12-01 | 799.0 | 792.444092 | | 2 | 1 | 1972-03-01 | 1971-12-01 | 890.0 | 883.122620 | | ... | ... | ... | ... | ... | ... | | 33 | 1 | 1974-10-01 | 1973-12-01 | 812.0 | 810.304688 | | 34 | 1 | 1974-11-01 | 1973-12-01 | 773.0 | 781.804688 | | 35 | 1 | 1974-12-01 | 1973-12-01 | 813.0 | 818.811096 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | DynamicOptimizedTheta | | - | ---------- | ------ | --------------------- | | 0 | 1 | mae | 6.861949 | | 1 | 1 | mape | 0.008045 | | 2 | 1 | mase | 0.308595 | | 3 | 1 | rmse | 8.647459 | | 4 | 1 | smape | 0.004010 | ## References 1. [Kostas I. Nikolopoulos, Dimitrios D. Thomakos. Forecasting with the Theta Method-Theory and Applications. 2019 John Wiley & Sons Ltd.](https://onlinelibrary.wiley.com/doi/book/10.1002/9781119320784) 2. [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). 3. [Nixtla DynamicOptimizedTheta API](../../src/core/models.html#dynamicoptimizedtheta) 4. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 5. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, Time series cross-validation”.](https://otexts.com/fpp3/tscv.html). 6. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Dynamic Standard Theta Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/dynamicstandardtheta.html > Step-by-step guide on using the `DynamicStandardTheta Model` with > `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). ## Table of Contents * [Dynamic Standard Theta Model (DOTM)](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of DynamicStandardTheta with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Dynamic Standard Theta Models (DSTM) The Dynamic Standard Theta Model is a case-specific variation of the [Optimized Dynamic Theta Model](./dynamicoptimizedtheta.html). Also, for $\theta=2$, we have a stochastic approach of Theta, which is referred to hereafter as the dynamic standard Theta model (DSTM). ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} import pandas as pd df = pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/milk_production.csv", usecols=[1,2]) df.head() ``` | | month | production | | - | ---------- | ---------- | | 0 | 1962-01-01 | 589 | | 1 | 1962-02-01 | 561 | | 2 | 1962-03-01 | 640 | | 3 | 1962-04-01 | 656 | | 4 | 1962-05-01 | 727 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ---------- | --- | ---------- | | 0 | 1962-01-01 | 589 | 1 | | 1 | 1962-02-01 | 561 | 1 | | 2 | 1962-03-01 | 640 | 1 | | 3 | 1962-04-01 | 656 | 1 | | 4 | 1962-05-01 | 727 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ### Additive ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "additive", period=12) a.plot(); ``` ### Multiplicative ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "Multiplicative", period=12) a.plot(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Dynamic Standard Theta Model` 2. Data to test our model For the test data we will use the last 12 months to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='1974-12-01'] test = df[df.ds>'1974-12-01'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((156, 3), (12, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train", linestyle="--") sns.lineplot(test, x="ds", y="y", label="Test") plt.title("Monthly Milk Production") plt.show() ``` ## Implementation of DynamicStandardTheta with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import DynamicTheta ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 12 # Monthly data horizon = len(test) # number of predictions models = [DynamicTheta(season_length=season_length, decomposition_type="additive")] # multiplicative additive ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='MS') ``` ### Fit Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[DynamicTheta]) ``` Let’s see the results of our `Dynamic Standard Theta model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ print(result.keys()) print(result['fit']) ``` ```text theme={null} dict_keys(['mse', 'amse', 'fit', 'residuals', 'm', 'states', 'par', 'n', 'modeltype', 'mean_y', 'decompose', 'decomposition_type', 'seas_forecast', 'fitted']) results(x=array([393.28739991, 0.76875 ]), fn=10.787112115489622, nit=20, simplex=array([[399.92916541, 0.771875 ], [393.28739991, 0.76875 ], [384.74798713, 0.771875 ]])) ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("residuals"), columns=["residual Model"]) residual ``` | | residual Model | | --- | -------------- | | 0 | -18.247131 | | 1 | -46.247131 | | 2 | 17.140198 | | ... | ... | | 153 | -58.941711 | | 154 | -91.055420 | | 155 | -42.624939 | ```python theme={null} import scipy.stats as stats fig, axs = plt.subplots(nrows=2, ncols=2) residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | DynamicTheta | | --- | ---------- | ---------- | ------------ | | 0 | 1 | 1975-01-01 | 838.531555 | | 1 | 1 | 1975-02-01 | 800.154968 | | 2 | 1 | 1975-03-01 | 893.430786 | | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 815.959351 | | 10 | 1 | 1975-11-01 | 786.716431 | | 11 | 1 | 1975-12-01 | 823.539368 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | DynamicTheta | | - | ---------- | ---------- | ----- | ------------ | | 0 | 1 | 1962-01-01 | 589.0 | 607.247131 | | 1 | 1 | 1962-02-01 | 561.0 | 607.247131 | | 2 | 1 | 1962-03-01 | 640.0 | 622.859802 | | 3 | 1 | 1962-04-01 | 656.0 | 606.987793 | | 4 | 1 | 1962-05-01 | 727.0 | 605.021179 | ```python theme={null} StatsForecast.plot(values) ``` Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | DynamicTheta | DynamicTheta-lo-95 | DynamicTheta-hi-95 | | --- | ---------- | ---------- | ------------ | ------------------ | ------------------ | | 0 | 1 | 1975-01-01 | 838.531555 | 741.237366 | 954.407166 | | 1 | 1 | 1975-02-01 | 800.154968 | 640.697205 | 945.673096 | | 2 | 1 | 1975-03-01 | 893.430786 | 703.900635 | 1065.418701 | | ... | ... | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 815.959351 | 536.422791 | 1086.643433 | | 10 | 1 | 1975-11-01 | 786.716431 | 484.476593 | 1033.687134 | | 11 | 1 | 1975-12-01 | 823.539368 | 509.187256 | 1104.107788 | ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | DynamicTheta | | --- | ---------- | ---------- | ------------ | | 0 | 1 | 1975-01-01 | 838.531555 | | 1 | 1 | 1975-02-01 | 800.154968 | | 2 | 1 | 1975-03-01 | 893.430786 | | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 815.959351 | | 10 | 1 | 1975-11-01 | 786.716431 | | 11 | 1 | 1975-12-01 | 823.539368 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[80,95]) forecast_df ``` | | unique\_id | ds | DynamicTheta | DynamicTheta-lo-80 | DynamicTheta-hi-80 | DynamicTheta-lo-95 | DynamicTheta-hi-95 | | --- | ---------- | ---------- | ------------ | ------------------ | ------------------ | ------------------ | ------------------ | | 0 | 1 | 1975-01-01 | 838.531555 | 765.423828 | 927.285339 | 741.237366 | 954.407166 | | 1 | 1 | 1975-02-01 | 800.154968 | 701.099854 | 899.316162 | 640.697205 | 945.673096 | | 2 | 1 | 1975-03-01 | 893.430786 | 758.326416 | 1007.631165 | 703.900635 | 1065.418701 | | ... | ... | ... | ... | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 815.959351 | 608.699463 | 992.552673 | 536.422791 | 1086.643433 | | 10 | 1 | 1975-11-01 | 786.716431 | 558.429810 | 970.648376 | 484.476593 | 1033.687134 | | 11 | 1 | 1975-12-01 | 823.539368 | 588.706787 | 1031.564941 | 509.187256 | 1104.107788 | ```python theme={null} sf.plot(train, test.merge(forecast_df), level=[80, 95]) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=5)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=train, h=horizon, step_size=12, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` index. If you dont like working with index just run crossvalidation\_df.resetindex() * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | DynamicTheta | | --- | ---------- | ---------- | ---------- | ----- | ------------ | | 0 | 1 | 1972-01-01 | 1971-12-01 | 826.0 | 827.107239 | | 1 | 1 | 1972-02-01 | 1971-12-01 | 799.0 | 789.924194 | | 2 | 1 | 1972-03-01 | 1971-12-01 | 890.0 | 879.664429 | | ... | ... | ... | ... | ... | ... | | 33 | 1 | 1974-10-01 | 1973-12-01 | 812.0 | 804.398560 | | 34 | 1 | 1974-11-01 | 1973-12-01 | 773.0 | 775.329285 | | 35 | 1 | 1974-12-01 | 1973-12-01 | 813.0 | 811.767639 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | DynamicTheta | | - | ---------- | ------ | ------------ | | 0 | 1 | mae | 8.182119 | | 1 | 1 | mape | 0.009736 | | 2 | 1 | mase | 0.367965 | | 3 | 1 | rmse | 9.817624 | | 4 | 1 | smape | 0.004874 | ## References 1. [Kostas I. Nikolopoulos, Dimitrios D. Thomakos. Forecasting with the Theta Method-Theory and Applications. 2019 John Wiley & Sons Ltd.](https://onlinelibrary.wiley.com/doi/book/10.1002/9781119320784) 2. [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). 3. [Nixtla DynamicTheta API](../../src/core/models.html#dynamictheta) 4. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 5. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, Time series cross-validation”.](https://otexts.com/fpp3/tscv.html). 6. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # GARCH Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/garch.html > Step-by-step guide on using the `GARCH Model` with `Statsforecast`. In this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation`. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. [Bollerslev, T. (1986). Generalized autoregressive conditional heteroskedasticity. Journal of econometrics, 31(3), 307-327.](https://citeseerx.ist.psu.edu/document?repid=rep1\&type=pdf\&doi=7da8bfa5295375c1141d797e80065a599153c19d) 3\. [Engle, R. F. (1982). Autoregressive conditional heteroscedasticity with estimates of the variance of United Kingdom inflation. Econometrica: Journal of the econometric society, 987-1007.](http://www.econ.uiuc.edu/~econ508/Papers/engle82.pdf). 4. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) ## Table of Contents * [Introduction](#introduction) * [GARCH Models](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of GARCH with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The Generalized Autoregressive Conditional Heteroskedasticity (GARCH) model is a statistical technique used to model and predict volatility in financial and economic time series. It was developed by Robert Engle in 1982 as an extension of the Autoregressive Conditional Heteroskedasticity (ARCH) model proposed by Andrew Lo and Craig MacKinlay in 1988. The GARCH model allows capturing the presence of conditional heteroscedasticity in time series data, that is, the presence of fluctuations in the variance of a time series as a function of time. This is especially useful in financial data analysis, where volatility can be an important measure of risk. The GARCH model has become a fundamental tool in the analysis of financial time series and has been used in a wide variety of applications, from risk management to forecasting prices of shares and other financial values. ## Definition of GARCH Models **Definition 1.** A $\text{GARCH}(p,q)$ model with order $(p≥1,q≥0)$ is of the form $$ \begin{equation} \begin{cases} X_t = \sigma_t \varepsilon_t\\ \sigma_{t}^2 = \omega + \sum_{i=1}^{p} \alpha_i X_{t-i}^2 + \sum_{j=1}^{q} \beta_j \sigma_{t-j}^2 \end{cases} \end{equation} $$ where $\omega ≥0,\alpha_i ≥0,\beta_j ≥0,\alpha_p >0$ ,and $\beta_q >0$ are constants,$\varepsilon_t \sim iid(0,1)$, and $\varepsilon_t$ is independent of $\{X_k;k ≤ t − 1 \}$. A stochastic process $X_t$ is called a $\text{GARCH}(p, q )$ process if it satisfies Eq. (1). In practice, it has been found that for some time series, the $\text{ARCH}(p)$ model defined by (1) will provide an adequate fit only if the order $p$ is large. By allowing past volatilities to affect the present volatility in (1), a more parsimonious model may result. That is why we need `GARCH` models. Besides, note the condition that the order $p ≥ 1$. The **GARCH model** in Definition 1 has the properties as follows. Proposition 1. If $X_t$ is a $\text{GARCH}(p, q)$ process defined in (1) and $\sum_{i=1}^{p} \alpha_{i} + \sum_{j=1}^{q} \beta_j <1$,then the following propositions hold. * $X_{t}^2$ follows the $\text{ARMA}(m, q )$ model $X_{t}^2=\omega +\sum_{i=1}^{m} (\alpha_i + \beta_i )X_{t-i}^2 + \eta_t − \sum_{j=1}^q \beta_j \eta_{t-j}$ where $\alpha_i =0$ for $i >p,βj =0$ for $j >q,m=max(p,q)$, and $\eta_t =\sigma_{t}^2 (\varepsilon_{t}^2 −1)$. * $X_t$ is a white noise with $E(X)=0, E(X_{t+h} X_t )=0 \ \ \text{for} \ any \ \ h \neq 0, Var(X_t)= \frac{\omega}{1-\sum_{i=1}^{m} (\alpha_i + \beta_i )}$ * $\sigma_{t}^2$ is the conditional variance of $X_t$ , that is, we have $E(X_t|\mathscr{F}_{t−1}) = 0, \sigma_{t}^2 = Var(X_{t}^2|\mathscr{F}_{t−1}).$ * Model (1) reflects the fat tails and volatility clustering. Although an asset return series can usually be seen as a white noise, there exists such a return series so that it may be autocorrelated. What is more, a given original time series is not necessarily a return series, and at the same time, its values may be negative. If a time series is autocorrelated, we must first build an adequate model (e.g., an ARMA model) for the series in order to remove any autocorrelation in it. Then check whether the residual series has an ARCH effect, and if yes then we further model the residuals. In other words, if a time series $Y_t$ is autocorrelated and has ARCH effect, then a GARCH model that can capture the features of $Y_t$t should be of the form where Eq. (2) is referred to as the mean equation (model) and Eq. (3) is known as the volatility (variance) equation (model), and $Z_t$ is a representative of exogenous regressors. If $Y_t$ is a return series, then typically $Y_t = r + X_t$ where $r$ is a constant that means the expected returns is fixed. ### Advantages and disadvantages of the Generalized Autoregressive Conditional Heteroskedasticity (GARCH) Model | Advantages | Disadvantages | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1. 1. Flexible model: The GARCH model is flexible and can fit different types of time series data with different volatility patterns. | 1. Requires a large amount of data: The GARCH model requires a large amount of data to accurately estimate the model parameters. | | 2. Ability to model volatility: The GARCH model is capable of modeling the volatility and heteroscedasticity of a time series, which can improve the accuracy of forecasts. | 2. Sensitive to the model specification: The GARCH model is sensitive to the model specification and can be difficult to estimate if incorrectly specified. | | 3. It incorporates past information: The GARCH model incorporates past information on the volatility of the time series, which makes it useful for predicting future volatility. | 3. It can be computationally expensive: The GARCH model can be computationally expensive, especially if more complex models are used. | | 4. Allows the inclusion of exogenous variables: The GARCH model can be extended to include exogenous variables, which can improve the accuracy of the predictions. | 4. It does not consider extreme events: The GARCH model does not consider extreme or unexpected events in the time series, which can affect the accuracy of the predictions in situations of high volatility. | | 5. The GARCH model makes it possible to model conditional heteroscedasticity, that is, the variation of the variance of a time series as a function of time and of the previous values of the time series itself. | 5. The GARCH model assumes that the time series errors are normally distributed, which may not be true in practice. If the errors are not normally distributed, the model may produce inaccurate estimates of volatility. | | 6. The GARCH model can be used to estimate the value at risk (VaR) and the conditional value at risk (CVaR) of an investment portfolio. | | ### The Generalized Autoregressive Conditional Heteroskedasticity (GARCH) model can be applied in several fields The Generalized Autoregressive Conditional Heteroskedasticity (GARCH) model can be applied in a wide variety of areas where time series volatility is required to be modeled and predicted. Some of the areas in which the GARCH model can be applied are: 1. **Financial markets:** the GARCH model is widely used to model the volatility (risk) of returns on financial assets such as stocks, bonds, currencies, etc. It allows you to capture the changing nature of volatility. 2. **Commodity prices:** the prices of raw materials such as oil, gold, grains, etc. they exhibit conditional volatility that can be modeled with GARCH. 3. **Credit risk:** the risk of non-payment of loans and bonds also presents volatility over time that suits GARCH well. 4. **Economic time series:** macroeconomic indicators such as inflation, GDP, unemployment, etc. they have conditional volatility modelable with GARCH. 5. **Implicit volatility:** the GARCH model allows estimating the implicit volatility in financial options. 6. **Forecasts:** GARCH allows conditional volatility forecasts to be made in any time series. 7. **Risk analysis:** GARCH is useful for measuring and managing the risk of investment portfolios and assets. 8. **Finance:** The GARCH model is widely used in finance to model the price volatility of financial assets, such as stocks, bonds, and currencies. 9. **Economics:** The GARCH model is used in economics to model the volatility of the prices of goods and services, inflation, and other economic indicators. 10. **Environmental sciences:** The GARCH model is applied in environmental sciences to model the volatility of variables such as temperature, precipitation, and air quality. 11. **Social sciences:** The GARCH model is used in the social sciences to model the volatility of variables such as crime, migration, and employment. 12. **Engineering:** The GARCH model is applied in engineering to model the volatility of variables such as the demand for electrical energy, industrial production, and vehicular traffic. 13. **Health sciences:** The GARCH model is used in health sciences to model the volatility of variables such as the number of cases of infectious diseases and the prices of medicines. The GARCH Model is applicable in any context where it is required to model and forecast heterogeneous conditional volatility in time series, especially in finance and economics. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt from statsmodels.graphics.tsaplots import plot_acf from statsmodels.graphics.tsaplots import plot_pacf plt.style.use('fivethirtyeight') plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#212946', 'axes.facecolor': '#212946', 'savefig.facecolor':'#212946', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#2A3459', 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data Let’s pull the S\&P500 stock data from the Yahoo Finance site. ```python theme={null} import datetime import pandas as pd import time import yfinance as yf ticker = '^GSPC' period1 = datetime.datetime(2015, 1, 1) period2 = datetime.datetime(2023, 9, 22) interval = '1d' # 1d, 1m SP_500 = yf.download(ticker, start=period1, end=period2, interval=interval, progress=False) SP_500 = SP_500.reset_index() SP_500.head() ``` | Price | Date | Adj Close | Close | High | Low | Open | Volume | | ------ | ------------------------- | ----------- | ----------- | ----------- | ----------- | ----------- | ---------- | | Ticker | | ^GSPC | ^GSPC | ^GSPC | ^GSPC | ^GSPC | ^GSPC | | 0 | 2015-01-02 00:00:00+00:00 | 2058.199951 | 2058.199951 | 2072.360107 | 2046.040039 | 2058.899902 | 2708700000 | | 1 | 2015-01-05 00:00:00+00:00 | 2020.579956 | 2020.579956 | 2054.439941 | 2017.339966 | 2054.439941 | 3799120000 | | 2 | 2015-01-06 00:00:00+00:00 | 2002.609985 | 2002.609985 | 2030.250000 | 1992.439941 | 2022.150024 | 4460110000 | | 3 | 2015-01-07 00:00:00+00:00 | 2025.900024 | 2025.900024 | 2029.609985 | 2005.550049 | 2005.550049 | 3805480000 | | 4 | 2015-01-08 00:00:00+00:00 | 2062.139893 | 2062.139893 | 2064.080078 | 2030.609985 | 2030.609985 | 3934010000 | ```python theme={null} df=SP_500[["Date","Close"]] ``` The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------------- | ----------- | ---------- | | 0 | 2015-01-02 00:00:00+00:00 | 2058.199951 | 1 | | 1 | 2015-01-05 00:00:00+00:00 | 2020.579956 | 1 | | 2 | 2015-01-06 00:00:00+00:00 | 2002.609985 | 1 | | 3 | 2015-01-07 00:00:00+00:00 | 2025.900024 | 1 | | 4 | 2015-01-08 00:00:00+00:00 | 2062.139893 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds datetime64[ns, UTC] y float64 unique_id object dtype: object ``` ## Explore data with the plot method Plot a series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### The Augmented Dickey-Fuller Test An Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis. Augmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis. Null Hypothesis: Time Series is non-stationary. It gives a time-dependent trend. Alternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time. ADF or t Statistic \< critical values: Reject the null hypothesis, time series is stationary. ADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary. Let’s check if our series that we are analyzing is a stationary series. Let’s create a function to check, using the `Dickey Fuller` test ```python theme={null} from statsmodels.tsa.stattools import adfuller def Augmented_Dickey_Fuller_Test_func(series , column_name): print (f'Dickey-Fuller test results for columns: {column_name}') dftest = adfuller(series, autolag='AIC') dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used']) for key,value in dftest[4].items(): dfoutput['Critical Value (%s)'%key] = value print (dfoutput) if dftest[1] <= 0.05: print("Conclusion:====>") print("Reject the null hypothesis") print("The data is stationary") else: print("Conclusion:====>") print("The null hypothesis cannot be rejected") print("The data is not stationary") ``` ```python theme={null} Augmented_Dickey_Fuller_Test_func(df["y"],'S&P500') ``` ```text theme={null} Dickey-Fuller test results for columns: S&P500 Test Statistic -0.814971 p-value 0.814685 No Lags Used 10.000000 ... Critical Value (1%) -3.433341 Critical Value (5%) -2.862861 Critical Value (10%) -2.567473 Length: 7, dtype: float64 Conclusion:====> The null hypothesis cannot be rejected The data is not stationary ``` In the previous result we can see that the `Augmented_Dickey_Fuller` test gives us a `p-value` of 0.864700, which tells us that the null hypothesis cannot be rejected, and on the other hand the data of our series are not stationary. We need to differentiate our time series, in order to convert the data to stationary. ### Return Series Since the 1970s, the financial industry has been very prosperous with advancement of computer and Internet technology. Trade of financial products (including various derivatives) generates a huge amount of data which form financial time series. For finance, the return on a financial product is most interesting, and so our attention focuses on the return series. If $P_t$ is the closing price at time t for a certain financial product, then the return on this product is $X_t = \frac{(P_t − P_{t−1})}{P_{t−1}} ≈ log(P_t ) − log(P_{t−1}).$ It is return series $\{X_t \}$ that have been much independently studied. And important stylized features which are common across many instruments, markets, and time periods have been summarized. Note that if you purchase the financial product, then it becomes your asset, and its returns become your asset returns. Now let us look at the following examples. We can estimate the series of returns using the [pandas](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pct_change.html), `DataFrame.pct_change()` function. The `pct_change()` function has a periods parameter whose default value is 1. If you want to calculate a 30-day return, you must change the value to 30. ```python theme={null} df['return'] = 100 * df["y"].pct_change() df.dropna(inplace=True, how='any') df.head() ``` | | ds | y | unique\_id | return | | - | ------------------------- | ----------- | ---------- | --------- | | 1 | 2015-01-05 00:00:00+00:00 | 2020.579956 | 1 | -1.827811 | | 2 | 2015-01-06 00:00:00+00:00 | 2002.609985 | 1 | -0.889347 | | 3 | 2015-01-07 00:00:00+00:00 | 2025.900024 | 1 | 1.162984 | | 4 | 2015-01-08 00:00:00+00:00 | 2062.139893 | 1 | 1.788828 | | 5 | 2015-01-09 00:00:00+00:00 | 2044.810059 | 1 | -0.840381 | ```python theme={null} import plotly.express as px fig = px.line(df, x=df["ds"], y="return",title="SP500 Return Chart",template = "plotly_dark") fig.show() ``` ### Creating Squared Returns ```python theme={null} df['sq_return'] = df["return"].mul(df["return"]) df.head() ``` | | ds | y | unique\_id | return | sq\_return | | - | ------------------------- | ----------- | ---------- | --------- | ---------- | | 1 | 2015-01-05 00:00:00+00:00 | 2020.579956 | 1 | -1.827811 | 3.340891 | | 2 | 2015-01-06 00:00:00+00:00 | 2002.609985 | 1 | -0.889347 | 0.790938 | | 3 | 2015-01-07 00:00:00+00:00 | 2025.900024 | 1 | 1.162984 | 1.352532 | | 4 | 2015-01-08 00:00:00+00:00 | 2062.139893 | 1 | 1.788828 | 3.199906 | | 5 | 2015-01-09 00:00:00+00:00 | 2044.810059 | 1 | -0.840381 | 0.706240 | ### Returns vs Squared Returns ```python theme={null} from plotly.subplots import make_subplots import plotly.graph_objects as go fig = make_subplots(rows=1, cols=2) fig.add_trace(go.Scatter(x=df["ds"], y=df["return"], mode='lines', name='return'), row=1, col=1 ) fig.add_trace(go.Scatter(x=df["ds"], y=df["sq_return"], mode='lines', name='sq_return'), row=1, col=2 ) fig.update_layout(height=600, width=800, title_text="Returns vs Squared Returns", template = "plotly_dark") fig.show() ``` ```python theme={null} from scipy.stats import probplot, moment from statsmodels.tsa.stattools import adfuller, q_stat, acf import numpy as np import seaborn as sns def plot_correlogram(x, lags=None, title=None): lags = min(10, int(len(x)/5)) if lags is None else lags fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(14, 8)) x.plot(ax=axes[0][0], title='Return') x.rolling(21).mean().plot(ax=axes[0][0], c='k', lw=1) q_p = np.max(q_stat(acf(x, nlags=lags), len(x))[1]) stats = f'Q-Stat: {np.max(q_p):>8.2f}\nADF: {adfuller(x)[1]:>11.2f}' axes[0][0].text(x=.02, y=.85, s=stats, transform=axes[0][0].transAxes) probplot(x, plot=axes[0][1]) mean, var, skew, kurtosis = moment(x, moment=[1, 2, 3, 4]) s = f'Mean: {mean:>12.2f}\nSD: {np.sqrt(var):>16.2f}\nSkew: {skew:12.2f}\nKurtosis:{kurtosis:9.2f}' axes[0][1].text(x=.02, y=.75, s=s, transform=axes[0][1].transAxes) plot_acf(x=x, lags=lags, zero=False, ax=axes[1][0]) plot_pacf(x, lags=lags, zero=False, ax=axes[1][1]) axes[1][0].set_xlabel('Lag') axes[1][1].set_xlabel('Lag') fig.suptitle(title+ f'Dickey-Fuller: {adfuller(x)[1]:>11.2f}', fontsize=14) sns.despine() fig.tight_layout() fig.subplots_adjust(top=.9) ``` ```python theme={null} plot_correlogram(df["return"], lags=30, title="Time Series Analysis plot \n") ``` ### Ljung-Box Test Ljung-Box is a test for autocorrelation that we can use in tandem with our ACF and PACF plots. The Ljung-Box test takes our data, optionally either lag values to test, or the largest lag value to consider, and whether to compute the Box-Pierce statistic. Ljung-Box and Box-Pierce are two similar test statisitcs, Q , that are compared against a chi-squared distribution to determine if the series is white noise. We might use the Ljung-Box test on the residuals of our model to look for autocorrelation, ideally our residuals would be white noise. * Ho : The data are independently distributed, no autocorrelation. * Ha : The data are not independently distributed; they exhibit serial correlation. The Ljung-Box with the Box-Pierce option will return, for each lag, the Ljung-Box test statistic, Ljung-Box p-values, Box-Pierce test statistic, and Box-Pierce p-values. If $p<\alpha (0.05)$ we reject the null hypothesis. ```python theme={null} from statsmodels.stats.diagnostic import acorr_ljungbox ljung_res = acorr_ljungbox(df["return"], lags= 40, boxpierce=True) ljung_res.head() ``` | | lb\_stat | lb\_pvalue | bp\_stat | bp\_pvalue | | - | --------- | ------------ | --------- | ------------ | | 1 | 49.222273 | 2.285409e-12 | 49.155183 | 2.364927e-12 | | 2 | 62.991348 | 2.097020e-14 | 62.899234 | 2.195861e-14 | | 3 | 63.944944 | 8.433622e-14 | 63.850663 | 8.834380e-14 | | 4 | 74.343652 | 2.742989e-15 | 74.221024 | 2.911751e-15 | | 5 | 80.234862 | 7.494100e-16 | 80.093498 | 8.022242e-16 | ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `GARCH` model 2. Data to test our model For the test data we will use the last 30 day to test and evaluate the performance of our model. ```python theme={null} df=df[["ds","unique_id","return"]] df.columns=["ds", "unique_id", "y"] ``` ```python theme={null} train = df[df.ds<='2023-05-31'] # Let's forecast the last 30 days test = df[df.ds>'2023-05-31'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((2116, 3), (78, 3)) ``` ## Implementation of GARCH with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import GARCH ``` ### Instantiating Models Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful.season\_length. ```python theme={null} season_length = 7 # Dayly data horizon = len(test) # number of predictions biasadj=True, include_drift=True, models = [GARCH(1,1), GARCH(1,2), GARCH(2,2), GARCH(2,1), GARCH(3,1), GARCH(3,2), GARCH(3,3), GARCH(1,3), GARCH(2,3)] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast( models=models, freq='C', # custom business day frequency ) ``` ## Cross-validation We have built different GARCH models, so we need to determine which is the best model to then be able to train it and thus be able to make the predictions. To know which is the best model we go to the Cross Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=train, h=horizon, step_size=6, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | GARCH(1,1) | GARCH(1,2) | GARCH(2,2) | GARCH(2,1) | GARCH(3,1) | GARCH(3,2) | GARCH(3,3) | GARCH(1,3) | GARCH(2,3) | | --- | ---------- | ------------------------- | ------------------------- | --------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | | 0 | 1 | 2023-01-04 00:00:00+00:00 | 2023-01-03 00:00:00+00:00 | 0.753897 | 1.678755 | 1.678412 | 1.680475 | 1.686649 | 1.719494 | 2.210902 | 1.702743 | 1.647114 | 1.637795 | | 1 | 1 | 2023-01-05 00:00:00+00:00 | 2023-01-03 00:00:00+00:00 | -1.164553 | -0.728069 | -0.745487 | -0.730648 | -0.722156 | -0.738119 | -0.824748 | -0.755277 | -0.740976 | -0.744150 | | 2 | 1 | 2023-01-06 00:00:00+00:00 | 2023-01-03 00:00:00+00:00 | 2.284078 | -0.589733 | -0.582982 | -0.590078 | -0.598076 | -0.587109 | -0.866347 | -0.571160 | -0.587807 | -0.584692 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 387 | 1 | 2023-05-26 00:00:00+00:00 | 2023-02-07 00:00:00+00:00 | 1.304909 | -1.697814 | -1.694747 | -1.702537 | -1.735631 | -1.729903 | -1.712997 | -1.663399 | -1.702160 | -1.687723 | | 388 | 1 | 2023-05-30 00:00:00+00:00 | 2023-02-07 00:00:00+00:00 | 0.001660 | -0.326945 | -0.337504 | -0.329686 | -0.330120 | -0.334717 | -0.327583 | -0.330260 | -0.338245 | -0.332412 | | 389 | 1 | 2023-05-31 00:00:00+00:00 | 2023-02-07 00:00:00+00:00 | -0.610862 | 0.807625 | 0.787054 | 0.807819 | 0.841536 | 0.811702 | 0.836159 | 0.772193 | 0.801933 | 0.804526 | ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import rmse ``` ```python theme={null} evals = evaluate(crossvalidation_df.drop(columns='cutoff'), metrics=[rmse], agg_fn='mean') evals ``` | | metric | GARCH(1,1) | GARCH(1,2) | GARCH(2,2) | GARCH(2,1) | GARCH(3,1) | GARCH(3,2) | GARCH(3,3) | GARCH(1,3) | GARCH(2,3) | | - | ------ | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | | 0 | rmse | 1.383143 | 1.526258 | 1.481056 | 1.389969 | 1.453538 | 1.539906 | 1.392352 | 1.515796 | 1.389061 | ```python theme={null} evals.drop(columns='metric').loc[0].idxmin() ``` ```text theme={null} 'GARCH(1,1)' ``` **Note:** This result can vary depending on the data and period you use to train and test the model, and the models you want to test. This is an example, where the objective is to be able to teach a methodology for the use of `StatsForecast`, and in particular the GARCH model and the parameters used in Cross Validation to determine the best model for this example. In the previous result it can be seen that the best model is the model $\text{GARCH}(1,1)$ With this result found using Cross Validation to determine which is the best model, we are going to continue training our model, to then make the predictions. ### Fit the Model ```python theme={null} season_length = 7 # Dayly data horizon = len(test) # number of predictions biasadj=True, include_drift=True, models = [GARCH(1,1)] ``` ```python theme={null} sf = StatsForecast(models=models, freq='C', # custom business day frequency ) ``` ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[GARCH(1,1)]) ``` Let’s see the results of our Theta model. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'p': 1, 'q': 1, 'coeff': array([0.03745049, 0.18399111, 0.7890637 ]), 'message': 'Optimization terminated successfully', 'y_vals': array([-0.61086242]), 'sigma2_vals': array([0.76298402]), 'fitted': array([ nan, 2.14638896, -0.76426268, ..., -0.19747638, 0.76993462, 0.13183178]), 'actual_residuals': array([ nan, -3.03573613, 1.92724695, ..., 1.50238505, -0.7682743 , -0.7426942 ])} ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("actual_residuals"), columns=["residual Model"]) residual ``` | | residual Model | | ---- | -------------- | | 0 | NaN | | 1 | -3.035736 | | 2 | 1.927247 | | ... | ... | | 2113 | 1.502385 | | 2114 | -0.768274 | | 2115 | -0.742694 | ```python theme={null} from scipy import stats fig, axs = plt.subplots(nrows=2, ncols=2) # plot[1,1] residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); # plot sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); # plot stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') # plot plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like `ARIMA` and `Theta`) ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat.head() ``` | | unique\_id | ds | GARCH(1,1) | | - | ---------- | ------------------------- | ---------- | | 0 | 1 | 2023-06-01 00:00:00+00:00 | 1.366914 | | 1 | 1 | 2023-06-02 00:00:00+00:00 | -0.593121 | | 2 | 1 | 2023-06-05 00:00:00+00:00 | -0.485200 | | 3 | 1 | 2023-06-06 00:00:00+00:00 | -0.927145 | | 4 | 1 | 2023-06-07 00:00:00+00:00 | 0.766640 | ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon, fitted=True, level=[95]) Y_hat.head() ``` | | unique\_id | ds | GARCH(1,1) | GARCH(1,1)-lo-95 | GARCH(1,1)-hi-95 | | - | ---------- | ------------------------- | ---------- | ---------------- | ---------------- | | 0 | 1 | 2023-06-01 00:00:00+00:00 | 1.366914 | -0.021035 | 2.754863 | | 1 | 1 | 2023-06-02 00:00:00+00:00 | -0.593121 | -2.435497 | 1.249254 | | 2 | 1 | 2023-06-05 00:00:00+00:00 | -0.485200 | -2.139216 | 1.168815 | | 3 | 1 | 2023-06-06 00:00:00+00:00 | -0.927145 | -2.390566 | 0.536276 | | 4 | 1 | 2023-06-07 00:00:00+00:00 | 0.766640 | -0.771479 | 2.304759 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | GARCH(1,1) | GARCH(1,1)-lo-95 | GARCH(1,1)-hi-95 | | - | ---------- | ------------------------- | --------- | ---------- | ---------------- | ---------------- | | 0 | 1 | 2015-01-05 00:00:00+00:00 | -1.827811 | NaN | NaN | NaN | | 1 | 1 | 2015-01-06 00:00:00+00:00 | -0.889347 | 2.146389 | -0.972874 | 5.265652 | | 2 | 1 | 2015-01-07 00:00:00+00:00 | 1.162984 | -0.764263 | -3.883526 | 2.355000 | | 3 | 1 | 2015-01-08 00:00:00+00:00 | 1.788828 | -0.650707 | -3.769970 | 2.468556 | | 4 | 1 | 2015-01-09 00:00:00+00:00 | -0.840381 | -1.449049 | -4.568312 | 1.670214 | Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | GARCH(1,1) | GARCH(1,1)-lo-95 | GARCH(1,1)-hi-95 | | --- | ---------- | ------------------------- | ---------- | ---------------- | ---------------- | | 0 | 1 | 2023-06-01 00:00:00+00:00 | 1.366914 | -0.021035 | 2.754863 | | 1 | 1 | 2023-06-02 00:00:00+00:00 | -0.593121 | -2.435497 | 1.249254 | | 2 | 1 | 2023-06-05 00:00:00+00:00 | -0.485200 | -2.139216 | 1.168815 | | ... | ... | ... | ... | ... | ... | | 75 | 1 | 2023-09-14 00:00:00+00:00 | -1.686546 | -3.049859 | -0.323233 | | 76 | 1 | 2023-09-15 00:00:00+00:00 | -0.322556 | -2.497448 | 1.852335 | | 77 | 1 | 2023-09-18 00:00:00+00:00 | 0.799407 | -1.027642 | 2.626457 | ```python theme={null} sf.plot(train, Y_hat.merge(test), max_insample_length=200) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 dayly ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | GARCH(1,1) | | --- | ---------- | ------------------------- | ---------- | | 0 | 1 | 2023-06-01 00:00:00+00:00 | 1.366914 | | 1 | 1 | 2023-06-02 00:00:00+00:00 | -0.593121 | | 2 | 1 | 2023-06-05 00:00:00+00:00 | -0.485200 | | ... | ... | ... | ... | | 75 | 1 | 2023-09-14 00:00:00+00:00 | -1.686546 | | 76 | 1 | 2023-09-15 00:00:00+00:00 | -0.322556 | | 77 | 1 | 2023-09-18 00:00:00+00:00 | 0.799407 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[80,95]) forecast_df.head(10) ``` | | unique\_id | ds | GARCH(1,1) | GARCH(1,1)-lo-95 | GARCH(1,1)-lo-80 | GARCH(1,1)-hi-80 | GARCH(1,1)-hi-95 | | --- | ---------- | ------------------------- | ---------- | ---------------- | ---------------- | ---------------- | ---------------- | | 0 | 1 | 2023-06-01 00:00:00+00:00 | 1.366914 | -0.021035 | 0.459383 | 2.274445 | 2.754863 | | 1 | 1 | 2023-06-02 00:00:00+00:00 | -0.593121 | -2.435497 | -1.797786 | 0.611543 | 1.249254 | | 2 | 1 | 2023-06-05 00:00:00+00:00 | -0.485200 | -2.139216 | -1.566703 | 0.596303 | 1.168815 | | ... | ... | ... | ... | ... | ... | ... | ... | | 7 | 1 | 2023-06-12 00:00:00+00:00 | -1.051435 | -4.790880 | -3.496526 | 1.393657 | 2.688010 | | 8 | 1 | 2023-06-13 00:00:00+00:00 | 0.421605 | -3.001123 | -1.816396 | 2.659607 | 3.844333 | | 9 | 1 | 2023-06-14 00:00:00+00:00 | -0.300086 | -3.138338 | -2.155920 | 1.555747 | 2.538166 | ```python theme={null} sf.plot(train, test.merge(forecast_df), level=[80, 95], max_insample_length=200) ``` ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | GARCH(1,1) | | - | ---------- | ------ | ---------- | | 0 | 1 | mae | 0.843296 | | 1 | 1 | mape | 3.703305 | | 2 | 1 | mase | 0.794905 | | 3 | 1 | rmse | 1.048076 | | 4 | 1 | smape | 0.709150 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. [Bollerslev, T. (1986). Generalized autoregressive conditional heteroskedasticity. Journal of econometrics, 31(3), 307-327.](https://citeseerx.ist.psu.edu/document?repid=rep1\&type=pdf\&doi=7da8bfa5295375c1141d797e80065a599153c19d) 3. [Engle, R. F. (1982). Autoregressive conditional heteroscedasticity with estimates of the variance of United Kingdom inflation. Econometrica: Journal of the econometric society, 987-1007.](http://www.econ.uiuc.edu/~econ508/Papers/engle82.pdf). 4. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 5. [Nixtla Garch API](../../src/core/models.html#garch) 6. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 7. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 8. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Holt Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/holt.html > Step-by-step guide on using the `Holt Model` with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Holt Model](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of Holt with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The Holts model, also known as the double exponential smoothing method, is a forecasting technique widely used in time series analysis. It was developed by Charles Holt in 1957 as an improvement on Brown’s simple exponential smoothing method. The Holts model is used to predict future values of a time series that exhibits a trend. The model uses two smoothing parameters, one for estimating the trend and the other for estimating the level or base level of the time series. These parameters are called $\alpha$ and $\beta$, respectively. The Holts model is an extension of Brown’s simple exponential smoothing method, which uses only one smoothing parameter to estimate the trend and base level of the time series. The Holts model improves the accuracy of the forecasts by adding a second smoothing parameter for the trend. One of the main advantages of the Holts model is that it is easy to implement and does not require a large amount of historical data to generate accurate predictions. Furthermore, the model is highly adaptable and can be customized to fit a wide variety of time series. However, Holts’ model has some limitations. For example, the model assumes that the time series is stationary and that the trend is linear. If the time series is not stationary or has a non-linear trend, the Holts model may not be the most appropriate. In general, the Holts model is a useful and widely used technique in time series analysis, especially when the series is expected to exhibit a linear trend. ## Holt Method `Simple exponential smoothing` does not function well when the data has trends. In those cases, we can use *double exponential smoothing*. This is a more reliable method for handling data that consumes trends without seasonality than compared to other methods. This method adds a time *trend* equation in the formulation. Two different weights, or smoothing parameters, are used to update these two components at a time. Holt’s exponential smoothing is also sometimes called *double exponential smoothing*. The main idea here is to use SES and advance it to capture the *trend* component. Holt (1957) extended simple exponential smoothing to allow the forecasting of data with a *trend*. This method involves a forecast equation and two smoothing equations (one for the *level* and one for the *trend*): Assume that a series has the following: * Level * Trend * No seasonality * Noise where $\ell_{t}$ denotes an estimate of the level of the series at time $t, b_t$ denotes an estimate of the trend (slope) of the series at time $t, \alpha$ is the smoothing parameter for the level, $0\le\alpha\le1$, and $\beta^{*}$ is the smoothing parameter for the trend, $0\le\beta^*\le1$. As with simple exponential smoothing, the level equation here shows that $\ell_{t}$ is a weighted average of observation $y_{t}$ and the one-step-ahead training forecast for time $t$, here given by $\ell_{t-1} + b_{t-1}$. The trend equation shows that $b_t$ is a weighted average of the estimated trend at time $t$ based on $\ell_{t} - \ell_{t-1}$ and $b_{t-1}$, the previous estimate of the trend. The forecast function is no longer flat but trending. The $h$-step-ahead forecast is equal to the last estimated level plus $h$ times the last estimated trend value. Hence the forecasts are a linear function of $h$. ### Innovations state space models for exponential smoothing The exponential smoothing methods presented in Table 7.6 are algorithms which generate point forecasts. The statistical models in this tutorial generate the same point forecasts, but can also generate prediction (or forecast) intervals. A statistical model is a stochastic (or random) data generating process that can produce an entire forecast distribution. Each model consists of a measurement equation that describes the observed data, and some state equations that describe how the unobserved components or states (level, trend, seasonal) change over time. Hence, these are referred to as state space models. For each method there exist two models: one with additive errors and one with multiplicative errors. The point forecasts produced by the models are identical if they use the same smoothing parameter values. They will, however, generate different prediction intervals. To distinguish between a model with additive errors and one with multiplicative errors. We label each state space model as ETS( .,.,.) for (Error, Trend, Seasonal). This label can also be thought of as ExponenTial Smoothing. Using the same notation as in Table 7.5, the possibilities for each component are: $Error=\{A,M\}$, $Trend=\{N,A,A_d\}$ and $Seasonal=\{N,A,M\}$ For our case, the linear Holt model with a trend, we are going to see two cases, both for the additive and the multiplicative ### ETS(A,A,N): Holt’s linear method with additive errors For this model, we assume that the one-step-ahead training errors are given by $\varepsilon_t=y_t-\ell_{t-1}-b_{t-1} \sim \text{NID}(0,\sigma^2)$. Substituting this into the error correction equations for Holt’s linear method we obtain where, for simplicity, we have set $\beta=\alpha \beta^*$ ### ETS(M,A,N): Holt’s linear method with multiplicative errors Specifying one-step-ahead training errors as relative errors such that $\varepsilon_t=\frac{y_t-(\ell_{t-1}+b_{t-1})}{(\ell_{t-1}+b_{t-1})}$ and following an approach similar to that used above, the innovations state space model underlying Holt’s linear method with multiplicative errors is specified as where again $\beta=\alpha \beta^*$ and $\varepsilon_t \sim \text{NID}(0,\sigma^2)$. ### A taxonomy of exponential smoothing methods Building on the idea of time series components, we can move to the ETS taxonomy. ETS stands for “Error-Trend-Seasonality” and defines how specifically the components interact with each other. Based on the type of error, trend and seasonality, Pegels (1969) proposed a taxonomy, which was then developed further by Hyndman et al. (2002) and refined by Hyndman et al. (2008). According to this taxonomy, error, trend and seasonality can be: 1. Error: “Additive” (A), or “Multiplicative” (M); 2. Trend: “None” (N), or “Additive” (A), or “Additive damped” (Ad), or “Multiplicative” (M), or “Multiplicative damped” (Md); 3. Seasonality: “None” (N), or “Additive” (A), or “Multiplicative” (M). The components in the ETS taxonomy have clear interpretations: level shows average value per time period, trend reflects the change in the value, while seasonality corresponds to periodic fluctuations (e.g. increase in sales each January). Based on the the types of the components above, it is theoretically possible to devise 30 ETS models with different types of error, trend and seasonality. Figure 1 shows examples of different time series with deterministic (they do not change over time) level, trend, seasonality and with the additive error term. ![“Figure 1: Time series corresponding to the additive error ETS models”](https://openforecast.org/adam/Svetunkov--2023----Forecasting-and-Analytics-with-the-Augmented-Dynamic-Adaptive-Model--ADAM-_files/figure-html/ETSTaxonomyAdditive-1.png) *Figure 4.1: Time series corresponding to the additive error ETS models* Things to note from the plots in Figure.1: 1. When seasonality is multiplicative, its amplitude increases with the increase of the level of the data, while with additive seasonality, the amplitude is constant. Compare, for example, ETS(A,A,A) with ETS(A,A,M): for the former, the distance between the highest and the lowest points in the first year is roughly the same as in the last year. In the case of ETS(A,A,M) the distance increases with the increase in the level of series; 2. When the trend is multiplicative, data exhibits exponential growth/decay; 3. The damped trend slows down both additive and multiplicative trends; 4. It is practically impossible to distinguish additive and multiplicative seasonality if the level of series does not change because the amplitude of seasonality will be constant in both cases (compare ETS(A,N,A) and ETS(A,N,M)). ![](https://openforecast.org/adam/Svetunkov--2023----Forecasting-and-Analytics-with-the-Augmented-Dynamic-Adaptive-Model--ADAM-_files/figure-html/ETSTaxonomyMultiplicative-1.png) *Figure 2: Time series corresponding to the multiplicative error ETS models* The graphs in Figure 2 show approximately the same idea as the additive case, the main difference is that the error variance increases with increasing data level; this becomes clearer in ETS(M,A,N) and ETS(M,M,N) data. This property is called heteroskedasticity in statistics, and Hyndman et al. (2008) argue that the main benefit of multiplicative error models is to capture this characteristic. ### Mathematical models in the ETS taxonomy I hope that it becomes more apparent to the reader how the ETS framework is built upon the idea of time series decomposition. By introducing different components, defining their types, and adding the equations for their update, we can construct models that would work better in capturing the key features of the time series. But we should also consider the potential change in components over time. The “transition” or “state” equations are supposed to reflect this change: they explain how the level, trend or seasonal components evolve. As discussed in Section 2.2, given different types of components and their interactions, we end up with 30 models in the taxonomy. Tables 1 and 2 summarise mathematically all 30 ETS models shown graphically on Figures 1 and 2, presenting formulae for measurement and transition equations. Table 1: Additive error ETS models | | Nonseasonal | Additive | Multiplicative | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | No trend | $\begin{aligned} &y_{t} = l_{t-1} + \epsilon_t \\ &l_t = l_{t-1} + \alpha \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} + s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + \alpha \epsilon_t \\ &s_t = s_{t-m} + \gamma \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + \alpha \frac{\epsilon_t}{s_{t-m}} \\ &s_t = s_{t-m} + \gamma \frac{\epsilon_t}{l_{t-1}} \end{aligned}$ | | Additive | $\begin{aligned} &y_{t} = l_{t-1} + b_{t-1} + \epsilon_t \\ &l_t = l_{t-1} + b_{t-1} + \alpha \epsilon_t \\ &b_t = b_{t-1} + \beta \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} + b_{t-1} + s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + b_{t-1} + \alpha \epsilon_t \\ &b_t = b_{t-1} + \beta \epsilon_t \\ &s_t = s_{t-m} + \gamma \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + b_{t-1}) s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + b_{t-1} + \alpha \frac{\epsilon_t}{s_{t-m}} \\ &b_t = b_{t-1} + \beta \frac{\epsilon_t}{s_{t-m}} \\ &s_t = s_{t-m} + \gamma \frac{\epsilon_t}{l_{t-1} + b_{t-1}} \end{aligned}$ | | Additive damped | $\begin{aligned} &y_{t} = l_{t-1} + \phi b_{t-1} + \epsilon_t \\ &l_t = l_{t-1} + \phi b_{t-1} + \alpha \epsilon_t \\ &b_t = \phi b_{t-1} + \beta \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} + \phi b_{t-1} + s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + \phi b_{t-1} + \alpha \epsilon_t \\ &b_t = \phi b_{t-1} + \beta \epsilon_t \\ &s_t = s_{t-m} + \gamma \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + \phi b_{t-1}) s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + \phi b_{t-1} + \alpha \frac{\epsilon_t}{s_{t-m}} \\ &b_t = \phi b_{t-1} + \beta \frac{\epsilon_t}{s_{t-m}} \\ &s_t = s_{t-m} + \gamma \frac{\epsilon_t}{l_{t-1} + \phi b_{t-1}} \end{aligned}$ | | Multiplicative | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1} + \epsilon_t \\ &l_t = l_{t-1} b_{t-1} + \alpha \epsilon_t \\ &b_t = b_{t-1} + \beta \frac{\epsilon_t}{l_{t-1}} \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1} + s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} b_{t-1} + \alpha \epsilon_t \\ &b_t = b_{t-1} + \beta \frac{\epsilon_t}{l_{t-1}} \\ &s_t = s_{t-m} + \gamma \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1} s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} b_{t-1} + \alpha \frac{\epsilon_t}{s_{t-m}} \\ &b_t = b_{t-1} + \beta \frac{\epsilon_t}{l_{t-1}s_{t-m}} \\ &s_t = s_{t-m} + \gamma \frac{\epsilon_t}{l_{t-1} b_{t-1}} \end{aligned}$ | | Multiplicative damped | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1}^\phi + \epsilon_t \\ &l_t = l_{t-1} b_{t-1}^\phi + \alpha \epsilon_t \\ &b_t = b_{t-1}^\phi + \beta \frac{\epsilon_t}{l_{t-1}} \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1}^\phi + s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} b_{t-1}^\phi + \alpha \epsilon_t \\ &b_t = b_{t-1}^\phi + \beta \frac{\epsilon_t}{l_{t-1}} \\ &s_t = s_{t-m} + \gamma \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1}^\phi s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} b_{t-1}^\phi + \alpha \frac{\epsilon_t}{s_{t-m}} \\ &b_t = b_{t-1}^\phi + \beta \frac{\epsilon_t}{l_{t-1}s_{t-m}} \\ &s_t = s_{t-m} + \gamma \frac{\epsilon_t}{l_{t-1} b_{t-1}} \end{aligned}$ | Table 2: Multiplicative error ETS models | | Nonseasonal | Additive | Multiplicative | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | No trend | $\begin{aligned} &y_{t} = l_{t-1}(1 + \epsilon_t) \\ &l_t = l_{t-1}(1 + \alpha \epsilon_t) \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + s_{t-m})(1 + \epsilon_t) \\ &l_t = l_{t-1} + \alpha \mu_{y,t} \epsilon_t \\ &s_t = s_{t-m} + \gamma \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} s_{t-m}(1 + \epsilon_t) \\ &l_t = l_{t-1}(1 + \alpha \epsilon_t) \\ &s_t = s_{t-m}(1 + \gamma \epsilon_t) \end{aligned}$ | | Additive | $\begin{aligned} &y_{t} = (l_{t-1} + b_{t-1})(1 + \epsilon_t) \\ &l_t = (l_{t-1} + b_{t-1})(1 + \alpha \epsilon_t) \\ &b_t = b_{t-1} + \beta \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + b_{t-1} + s_{t-m})(1 + \epsilon_t) \\ &l_t = l_{t-1} + b_{t-1} + \alpha \mu_{y,t} \epsilon_t \\ &b_t = b_{t-1} + \beta \mu_{y,t} \epsilon_t \\ &s_t = s_{t-m} + \gamma \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + b_{t-1}) s_{t-m}(1 + \epsilon_t) \\ &l_t = (l_{t-1} + b_{t-1})(1 + \alpha \epsilon_t) \\ &b_t = b_{t-1} + \beta (l_{t-1} + b_{t-1}) \epsilon_t \\ &s_t = s_{t-m} (1 + \gamma \epsilon_t) \end{aligned}$ | | Additive damped | $\begin{aligned} &y_{t} = (l_{t-1} + \phi b_{t-1})(1 + \epsilon_t) \\ &l_t = (l_{t-1} + \phi b_{t-1})(1 + \alpha \epsilon_t) \\ &b_t = \phi b_{t-1} + \beta \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + \phi b_{t-1} + s_{t-m})(1 + \epsilon_t) \\ &l_t = l_{t-1} + \phi b_{t-1} + \alpha \mu_{y,t} \epsilon_t \\ &b_t = \phi b_{t-1} + \beta \mu_{y,t} \epsilon_t \\ &s_t = s_{t-m} + \gamma \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + \phi b_{t-1}) s_{t-m}(1 + \epsilon_t) \\ &l_t = l_{t-1} + \phi b_{t-1} (1 + \alpha \epsilon_t) \\ &b_t = \phi b_{t-1} + \beta (l_{t-1} + \phi b_{t-1}) \epsilon_t \\ &s_t = s_{t-m}(1 + \gamma \epsilon_t) \end{aligned}$ | | Multiplicative | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1} (1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1} (1 + \alpha \epsilon_t) \\ &b_t = b_{t-1} (1 + \beta \epsilon_t) \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} b_{t-1} + s_{t-m})(1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1} + \alpha \mu_{y,t} \epsilon_t \\ &b_t = b_{t-1} + \beta \frac{\mu_{y,t}}{l_{t-1}} \epsilon_t \\ &s_t = s_{t-m} + \gamma \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1} s_{t-m} (1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1} (1 + \alpha \epsilon_t) \\ &b_t = b_{t-1} (1 + \beta \epsilon_t) \\ &s_t = s_{t-m} (1 + \gamma \epsilon_t) \end{aligned}$ | | Multiplicative damped | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1}^\phi (1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1}^\phi (1 + \alpha \epsilon_t) \\ &b_t = b_{t-1}^\phi (1 + \beta \epsilon_t) \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} b_{t-1}^\phi + s_{t-m})(1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1}^\phi + \alpha \mu_{y,t} \epsilon_t \\ &b_t = b_{t-1}^\phi + \beta \frac{\mu_{y,t}}{l_{t-1}} \epsilon_t \\ &s_t = s_{t-m} + \gamma \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1}^\phi s_{t-m} (1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1}^\phi \left(1 + \alpha \epsilon_t\right) \\ &b_t = b_{t-1}^\phi \left(1 + \beta \epsilon_t\right) \\ &s_t = s_{t-m} \left(1 + \gamma \epsilon_t\right) \end{aligned}$ | From a statistical point of view, formulae in Tables 1 and 2 correspond to the “true models”, they explain the models underlying potential data, but when it comes to their construction and estimation, the $\epsilon_t$ is substituted by the estimated $e_t$ (which is calculated differently depending on the error type), and time series components and smoothing parameters are also replaced by their estimates (e.g. $\hat \alpha$ instead of $\alpha$). However, if the values of these models’ parameters were known, it would be possible to produce point forecasts and conditional h steps ahead expectations from these models. ### Properties Holt’s linear trend method Holt’s linear trend method is a time series forecasting technique that uses exponential smoothing to estimate the level and trend components of a time series. The method has several properties, including: 1. Additive model: Holt’s linear trend method assumes that the time series can be decomposed into an additive model, where the observed values are the sum of the level, trend, and error components. 2. Smoothing parameters: The method uses two smoothing parameters, α and β, to estimate the level and trend components of the time series. These parameters control the amount of smoothing applied to the level and trend components, respectively. 3. Linear trend: Holt’s linear trend method assumes that the trend component of the time series follows a straight line. This means that the method is suitable for time series data that exhibit a constant linear trend over time. 4. Forecasting: The method uses the estimated level and trend components to forecast future values of the time series. The forecast for the next period is given by the sum of the level and trend components. 5. Optimization: The smoothing parameters α and β are estimated through a process of optimization that minimizes the sum of squared errors between the predicted and observed values. This involves iterating over different values of the smoothing parameters until the optimal values are found. 6. Seasonality: Holt’s linear trend method can be extended to incorporate seasonality components. This involves adding a seasonal component to the model, which captures any systematic variations in the time series that occur on a regular basis. Overall, Holt’s linear trend method is a powerful and widely used forecasting technique that can be used to generate accurate predictions for time series data with a constant linear trend. The method is easy to implement and can be extended to handle time series data with seasonal variations. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/ads.csv") df.head() ``` | | Time | Ads | | - | ------------------- | ------ | | 0 | 2017-09-13T00:00:00 | 80115 | | 1 | 2017-09-13T01:00:00 | 79885 | | 2 | 2017-09-13T02:00:00 | 89325 | | 3 | 2017-09-13T03:00:00 | 101930 | | 4 | 2017-09-13T04:00:00 | 121630 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | ------ | ---------- | | 0 | 2017-09-13T00:00:00 | 80115 | 1 | | 1 | 2017-09-13T01:00:00 | 79885 | 1 | | 2 | 2017-09-13T02:00:00 | 89325 | 1 | | 3 | 2017-09-13T03:00:00 | 101930 | 1 | | 4 | 2017-09-13T04:00:00 | 121630 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### The Augmented Dickey-Fuller Test An Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis. Augmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis. * Null Hypothesis: Time Series is non-stationary. It gives a time-dependent trend. * Alternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time. * ADF or t Statistic \< critical values: Reject the null hypothesis, time series is stationary. * ADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary. ```python theme={null} from statsmodels.tsa.stattools import adfuller def Augmented_Dickey_Fuller_Test_func(series , column_name): print (f'Dickey-Fuller test results for columns: {column_name}') dftest = adfuller(series, autolag='AIC') dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used']) for key,value in dftest[4].items(): dfoutput['Critical Value (%s)'%key] = value print (dfoutput) if dftest[1] <= 0.05: print("Conclusion:====>") print("Reject the null hypothesis") print("The data is stationary") else: print("Conclusion:====>") print("The null hypothesis cannot be rejected") print("The data is not stationary") ``` ```python theme={null} Augmented_Dickey_Fuller_Test_func(df["y"],'Ads') ``` ```text theme={null} Dickey-Fuller test results for columns: Ads Test Statistic -7.089634e+00 p-value 4.444804e-10 No Lags Used 9.000000e+00 ... Critical Value (1%) -3.462499e+00 Critical Value (5%) -2.875675e+00 Critical Value (10%) -2.574304e+00 Length: 7, dtype: float64 Conclusion:====> Reject the null hypothesis The data is stationary ``` ### Autocorrelation plots **Autocorrelation Function** **Definition 1.** Let $\{x_t;1 ≤ t ≤ n\}$ be a time series sample of size n from $\{X_t\}$. 1. $\bar x = \sum_{t=1}^n \frac{x_t}{n}$ is called the sample mean of $\{X_t\}$. 2. $c_k =\sum_{t=1}^{n−k} (x_{t+k}- \bar x)(x_t−\bar x)/n$ is known as the sample autocovariance function of $\{X_t\}$. 3. $r_k = c_k /c_0$ is said to be the sample autocorrelation function of $\{X_t\}$. Note the following remarks about this definition: * Like most literature, this guide uses ACF to denote the sample autocorrelation function as well as the autocorrelation function. What is denoted by ACF can easily be identified in context. * Clearly c0 is the sample variance of $\{X_t\}$. Besides, $r_0 = c_0/c_0 = 1$ and for any integer $k, |r_k| ≤ 1$. * When we compute the ACF of any sample series with a fixed length $n$, we cannot put too much confidence in the values of $r_k$ for large k’s, since fewer pairs of $(x_{t +k }, x_t )$ are available for calculating $r_k$ as $k$ is large. One rule of thumb is not to estimate $r_k$ for $k > n/3$, and another is $n ≥ 50, k ≤ n/4$. In any case, it is always a good idea to be careful. * We also compute the ACF of a nonstationary time series sample by Definition 1. In this case, however, the ACF or $r_k$ very slowly or hardly tapers off as $k$ increases. * Plotting the ACF $(r_k)$ against lag $k$ is easy but very helpful in analyzing time series sample. Such an ACF plot is known as a correlogram. * If $\{X_t\}$ is stationary with $E(X_t)=0$ and $\rho_k =0$ for all $k \neq 0$,thatis,itisa white noise series, then the sampling distribution of $r_k$ is asymptotically normal with the mean 0 and the variance of $1/n$. Hence, there is about 95% chance that $r_k$ falls in the interval $[−1.96/\sqrt{n}, 1.96/\sqrt{n}]$. Now we can give a summary that (1) if the time series plot of a time series clearly shows a trend or/and seasonality, it is surely nonstationary; (2) if the ACF $r_k$ very slowly or hardly tapers off as lag $k$ increases, the time series should also be nonstationary. **Partial autocorrelation** Let $\{X_t\}$ be a stationary time series with $E(X_t) = 0$. Here the assumption $E(X_t ) = 0$ is for conciseness only. If $E(X_t) = \mu \neq 0$, it is okay to replace $\{X_t\}$ by $\{X_t −\mu \}$. Now consider the linear regression (prediction) of $X_t$ on $\{X_{t−k+1:t−1}\}$ for any integer $k ≥ 2$. We use $\hat X_t$ to denote this regression (prediction): $\hat X_t =\alpha_1 X_{t−1}+···+\alpha_{k−1} X_{t−k+1}$ where $\{\alpha_1, · · · , \alpha_{k−1} \}$ satisfy $\{\alpha_1, · · · , \alpha_{k−1} \}=\argmin_{\beta_1,···,\beta{k−1}} E[X_t −(\beta_1 X_{t−1} +···+\beta_{k−1} X_{t−k+1})]^2$ That is, $\{\alpha_1, · · · , \alpha_{k−1} \}$ are chosen by minimizing the mean squared error of prediction. Similarly, let $\hat X_{t −k}$ denote the regression (prediction) of $X_{t −k}$ on $\{X_{t −k+1:t −1}\}$: $\hat X_{t−k} =\eta_1 X_{t−1}+···+\eta_{k−1} X_{t−k+1}$ Note that if $\{X_t\}$ is stationary, then $\{\alpha_{1:k−1} \} = \{\eta_{1:k−1} \}$. Now let $\hat Z_{t−k} = X_{t−k} − \hat X_{t−k}$ and $\hat Z_t = X_t − \hat X_t$. Then $\hat Z_{t−k}$ is the residual of removing the effect of the intervening variables $\{X_{t−k+1:t−1} \}$ from $X_{t−k}$, and $\hat Z_t$ is the residual of removing the effect of $\{X_{t −k+1:t −1} \}$ from $X_t$ . **Definition 2.** The partial autocorrelation function (PACF) at lag $k$ of a stationary time series $\{X_t \}$ with $E(X_t ) = 0$ is $\phi_{11} = Corr(X_{t−1}, X_t ) = \frac{Cov(X_{t−1}, X_t )} {[Var(X_{t−1})Var(X_t)]^{1/2}} = \rho_1$ and $\phi_{kk} = Corr(\hat Z_{t−k},\hat Z_t) = \frac{Cov(\hat Z_{t−k},\hat Z_t)} {[Var(\hat Z_{t −k} )Var(\hat Z_t )]^{1/2}}, \ k ≥ 2$ On the other hand, the following theorem paves the way to estimate the PACF of a stationary time series, and its proof can be seen in Fan and Yao (2003). **Theorem 1.** Let $\{X_t \}$ be a stationary time series with $E(X_t ) = 0$, and $\{a_{1k},··· ,a_{kk}\}$ satisfy $\{a_{1k},··· ,a_{kk}\}= \argmin_{a_1 ,··· ,a_k} E(X_t − a_1 X_{t−1}−···−a_k X_{t−k})^2$ Then $\phi_{kk} =a_{kk}$ for $k≥1$. ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ### Additive ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "additive", period=12) a.plot(); ``` ### Multiplicative ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "Multiplicative", period=12) a.plot(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Holt Model`. 2. Data to test our model For the test data we will use the last 30 hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2017-09-20 17:00:00'] test = df[df.ds>'2017-09-20 17:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((186, 3), (30, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train", linestyle="--") sns.lineplot(test, x="ds", y="y", label="Test") plt.title("Ads watched (hourly data)"); plt.show() ``` ## Implementation of Holt Method with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import Holt ``` ### Instantiate Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 24 # Hourly data horizon = len(test) # number of predictions models = [Holt(season_length=season_length, error_type="A", alias="Add"), Holt(season_length=season_length, error_type="M", alias="Multi")] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[Add,Multi]) ``` Let’s see the results of our `Holt Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ print(result.keys()) print(result['fit']) ``` ```text theme={null} dict_keys(['loglik', 'aic', 'bic', 'aicc', 'mse', 'amse', 'fit', 'residuals', 'components', 'm', 'nstate', 'fitted', 'states', 'par', 'sigma2', 'n_params', 'method', 'actual_residuals']) results(x=array([9.99900000e-01, 1.00000000e-04, 7.97982888e+04, 3.33340440e+02]), fn=4456.295090550272, nit=74, simplex=None) ``` Let us now visualize the fitted values of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("residuals"), columns=["residual Model"]) residual ``` | | residual Model | | --- | -------------- | | 0 | -16.629196 | | 1 | -563.340440 | | 2 | 9106.661223 | | ... | ... | | 183 | -268.370897 | | 184 | -1313.391081 | | 185 | -1428.364244 | ```python theme={null} import scipy.stats as stats fig, axs = plt.subplots(nrows=2, ncols=2) residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | Add | Multi | | --- | ---------- | ------------------- | ------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 139848.234375 | 141089.625000 | | 1 | 1 | 2017-09-20 19:00:00 | 140181.328125 | 142664.000000 | | 2 | 1 | 2017-09-20 20:00:00 | 140514.406250 | 144238.359375 | | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 148841.671875 | 183597.453125 | | 28 | 1 | 2017-09-21 22:00:00 | 149174.750000 | 185171.812500 | | 29 | 1 | 2017-09-21 23:00:00 | 149507.843750 | 186746.187500 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | Add | Multi | | - | ---------- | ------------------- | -------- | ------------- | ------------- | | 0 | 1 | 2017-09-13 00:00:00 | 80115.0 | 80131.632812 | 79287.125000 | | 1 | 1 | 2017-09-13 01:00:00 | 79885.0 | 80448.343750 | 81712.710938 | | 2 | 1 | 2017-09-13 02:00:00 | 89325.0 | 80218.335938 | 81482.796875 | | 3 | 1 | 2017-09-13 03:00:00 | 101930.0 | 89658.281250 | 90922.609375 | | 4 | 1 | 2017-09-13 04:00:00 | 121630.0 | 102264.195312 | 103528.398438 | ```python theme={null} StatsForecast.plot(values) ``` Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | Add | Add-lo-95 | Add-hi-95 | Multi | Multi-lo-95 | Multi-hi-95 | | --- | ---------- | ------------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 139848.234375 | 116559.250000 | 163137.218750 | 141089.625000 | 113501.140625 | 168678.125000 | | 1 | 1 | 2017-09-20 19:00:00 | 140181.328125 | 107245.734375 | 173116.906250 | 142664.000000 | 103333.265625 | 181994.718750 | | 2 | 1 | 2017-09-20 20:00:00 | 140514.406250 | 100175.375000 | 180853.453125 | 144238.359375 | 95679.804688 | 192796.921875 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 148841.671875 | 25453.445312 | 272229.875000 | 183597.453125 | 4082.392090 | 363112.531250 | | 28 | 1 | 2017-09-21 22:00:00 | 149174.750000 | 23596.246094 | 274753.250000 | 185171.812500 | 1151.084961 | 369192.562500 | | 29 | 1 | 2017-09-21 23:00:00 | 149507.843750 | 21776.173828 | 277239.531250 | 186746.187500 | -1776.010254 | 375268.375000 | ```python theme={null} sf.plot(train, Y_hat) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | Add | Multi | | --- | ---------- | ------------------- | ------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 139848.234375 | 141089.625000 | | 1 | 1 | 2017-09-20 19:00:00 | 140181.328125 | 142664.000000 | | 2 | 1 | 2017-09-20 20:00:00 | 140514.406250 | 144238.359375 | | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 148841.671875 | 183597.453125 | | 28 | 1 | 2017-09-21 22:00:00 | 149174.750000 | 185171.812500 | | 29 | 1 | 2017-09-21 23:00:00 | 149507.843750 | 186746.187500 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[80,95]) forecast_df ``` | | unique\_id | ds | Add | Add-lo-95 | Add-lo-80 | Add-hi-80 | Add-hi-95 | Multi | Multi-lo-95 | Multi-lo-80 | Multi-hi-80 | Multi-hi-95 | | --- | ---------- | ------------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 139848.234375 | 116559.250000 | 124620.390625 | 155076.078125 | 163137.218750 | 141089.625000 | 113501.140625 | 123050.484375 | 159128.781250 | 168678.125000 | | 1 | 1 | 2017-09-20 19:00:00 | 140181.328125 | 107245.734375 | 118645.898438 | 161716.750000 | 173116.906250 | 142664.000000 | 103333.265625 | 116947.015625 | 168380.984375 | 181994.718750 | | 2 | 1 | 2017-09-20 20:00:00 | 140514.406250 | 100175.375000 | 114138.132812 | 166890.687500 | 180853.453125 | 144238.359375 | 95679.804688 | 112487.625000 | 175989.093750 | 192796.921875 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 148841.671875 | 25453.445312 | 68162.445312 | 229520.890625 | 272229.875000 | 183597.453125 | 4082.392090 | 66218.867188 | 300976.031250 | 363112.531250 | | 28 | 1 | 2017-09-21 22:00:00 | 149174.750000 | 23596.246094 | 67063.382812 | 231286.125000 | 274753.250000 | 185171.812500 | 1151.084961 | 64847.128906 | 305496.500000 | 369192.562500 | | 29 | 1 | 2017-09-21 23:00:00 | 149507.843750 | 21776.173828 | 65988.593750 | 233027.093750 | 277239.531250 | 186746.187500 | -1776.010254 | 63478.144531 | 310014.218750 | 375268.375000 | ```python theme={null} sf.plot(train, forecast_df, level=[80, 95]) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 30 hours ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=30, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier. * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | Add | Multi | | --- | ---------- | ------------------- | ------------------- | -------- | ------------- | ------------- | | 0 | 1 | 2017-09-18 06:00:00 | 2017-09-18 05:00:00 | 99440.0 | 111573.328125 | 112874.039062 | | 1 | 1 | 2017-09-18 07:00:00 | 2017-09-18 05:00:00 | 97655.0 | 111820.390625 | 114421.679688 | | 2 | 1 | 2017-09-18 08:00:00 | 2017-09-18 05:00:00 | 97655.0 | 112067.453125 | 115969.320312 | | ... | ... | ... | ... | ... | ... | ... | | 87 | 1 | 2017-09-21 21:00:00 | 2017-09-20 17:00:00 | 103080.0 | 148841.671875 | 183597.453125 | | 88 | 1 | 2017-09-21 22:00:00 | 2017-09-20 17:00:00 | 95155.0 | 149174.750000 | 185171.812500 | | 89 | 1 | 2017-09-21 23:00:00 | 2017-09-20 17:00:00 | 80285.0 | 149507.843750 | 186746.187500 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | Add | Multi | | - | ---------- | ------ | ------------ | ------------ | | 0 | 1 | mae | 30905.751042 | 48210.098958 | | 1 | 1 | mape | 0.336201 | 0.491980 | | 2 | 1 | mase | 3.818464 | 5.956449 | | 3 | 1 | rmse | 38929.522482 | 54653.132768 | | 4 | 1 | smape | 0.129755 | 0.182024 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla Holt API](../../src/core/models.html#holt) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Holt Winters Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/holtwinters.html > Step-by-step guide on using the `Holt Winters Model` with > `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Holt-Winters Model](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of Holt-Winters with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The Holt-Winter model, also known as the triple exponential smoothing method, is a forecasting technique widely used in time series analysis. It was developed by Charles Holt and Peter Winters in 1960 as an improvement on Holt’s double exponential smoothing method. The Holt-Winter model is used to predict future values of a time series that exhibits a trend and seasonality. The model uses three smoothing parameters, one for estimating the trend, another for estimating the level or base level of the time series, and another for estimating seasonality. These parameters are called α, β and γ, respectively. The Holt-Winter model is an extension of Holt’s double exponential smoothing method, which uses only two smoothing parameters to estimate the trend and base level of the time series. The Holt-Winter model improves the accuracy of the forecasts by adding a third smoothing parameter for seasonality. One of the main advantages of the Holt-Winter model is that it is easy to implement and does not require a large amount of historical data to generate accurate predictions. Furthermore, the model is highly adaptable and can be customized to fit a wide variety of time series with seasonality. However, the Holt-Winter model has some limitations. For example, the model assumes that the time series is stationary and that seasonality is constant. If the time series is not stationary or has non-constant seasonality, the Holt-Winter model may not be the most appropriate. In general, the Holt-Winter model is a useful and widely used technique in time series analysis, especially when the series is expected to exhibit a constant trend and seasonality. ## Holt-Winters Method The Holt-Winters seasonal method comprises the forecast equation and three smoothing equations — one for the level $\ell_{t}$, one for the trend $b_t$, and one for the seasonal component $s_t$ , with corresponding smoothing parameters $\alpha$ , $\beta^*$ and $\gamma$. We use $m$ to denote the period of the seasonality, i.e., the number of seasons in a year. For example, for quarterly data $m=4$, and for monthly data $m=12$. There are two variations to this method that differ in the nature of the seasonal component. The additive method is preferred when the seasonal variations are roughly constant through the series, while the multiplicative method is preferred when the seasonal variations are changing proportional to the level of the series. With the additive method, the seasonal component is expressed in absolute terms in the scale of the observed series, and in the level equation the series is seasonally adjusted by subtracting the seasonal component. Within each year, the seasonal component will add up to approximately zero. With the multiplicative method, the seasonal component is expressed in relative terms (percentages), and the series is seasonally adjusted by dividing through by the seasonal component. Within each year, the seasonal component will sum up to approximately $m$. ### Holt-Winters’ additive method Holt-Winters’ additive method is a time series forecasting technique that extends the Holt-Winters’ method by incorporating an additive seasonality component. It is suitable for time series data that exhibit a seasonal pattern that changes over time. The Holt-Winters’ additive method uses three smoothing parameters - alpha (α), beta (β), and gamma (γ) - to estimate the level, trend, and seasonal components of the time series. The alpha parameter controls the smoothing of the level component, the beta parameter controls the smoothing of the trend component, and the gamma parameter controls the smoothing of the additive seasonal component. The forecasting process involves three steps: first, the level, trend, and seasonal components are estimated using the smoothing parameters and the historical data; second, these components are used to forecast future values of the time series; and third, the forecasted values are adjusted for the seasonal component using an additive factor. One of the advantages of Holt-Winters’ additive method is that it can handle time series data with an additive seasonality component, which is common in many real-world applications. The method is also easy to implement and can be extended to handle time series data with changing seasonal patterns. However, the method has some limitations. It assumes that the seasonality pattern is additive, which may not be the case for all time series. Additionally, the method requires a sufficient amount of historical data to accurately estimate the smoothing parameters and the seasonal component. Overall, Holt-Winters’ additive method is a powerful and widely used forecasting technique that can be used to generate accurate predictions for time series data with an additive seasonality component. The method is easy to implement and can be extended to handle time series data with changing seasonal patterns. The component form for the additive method is: where $k$ is the integer part of $(h-1)/m$, which ensures that the estimates of the seasonal indices used for forecasting come from the final year of the sample. The level equation shows a weighted average between the seasonally adjusted observation $(y_{t} - s_{t-m})$ and the non-seasonal forecast $(\ell_{t-1}+b_{t-1})$ for time $t$. The trend equation is identical to Holt’s linear method. The seasonal equation shows a weighted average between the current seasonal index, $(y_{t}-\ell_{t-1}-b_{t-1})$, and the seasonal index of the same season last year (i.e., $m$ time periods ago). The equation for the seasonal component is often expressed as $s_{t} = \gamma^* (y_{t}-\ell_{t})+ (1-\gamma^*)s_{t-m}.$ If we substitute $\ell_{t}$ from the smoothing equation for the level of the component form above, we get $s_{t} = \gamma^*(1-\alpha) (y_{t}-\ell_{t-1}-b_{t-1})+ [1-\gamma^*(1-\alpha)]s_{t-m},$ which is identical to the smoothing equation for the seasonal component we specify here, with $\gamma=\gamma^*(1-\alpha)$. The usual parameter restriction is $0\le\gamma^*\le1$, which translates to $0\le\gamma\le 1-\alpha$. ### Holt-Winters’ multiplicative method The Holt-Winters’ multiplicative method uses three smoothing parameters - alpha (α), beta (β), and gamma (γ) - to estimate the level, trend, and seasonal components of the time series. The alpha parameter controls the smoothing of the level component, the beta parameter controls the smoothing of the trend component, and the gamma parameter controls the smoothing of the multiplicative seasonal component. The forecasting process involves three steps: first, the level, trend, and seasonal components are estimated using the smoothing parameters and the historical data; second, these components are used to forecast future values of the time series; and third, the forecasted values are adjusted for the seasonal component using a multiplicative factor. One of the advantages of Holt-Winters’ multiplicative method is that it can handle time series data with a multiplicative seasonality component, which is common in many real-world applications. The method is also easy to implement and can be extended to handle time series data with changing seasonal patterns. However, the method has some limitations. It assumes that the seasonality pattern is multiplicative, which may not be the case for all time series. Additionally, the method requires a sufficient amount of historical data to accurately estimate the smoothing parameters and the seasonal component. Overall, Holt-Winters’ multiplicative method is a powerful and widely used forecasting technique that can be used to generate accurate predictions for time series data with a multiplicative seasonality component. The method is easy to implement and can be extended to handle time series data with changing seasonal patterns. In the multiplicative version, the seasonality averages to one. Use the multiplicative method if the seasonal variation increases with the level. ### Mathematical models in the ETS taxonomy I hope that it becomes more apparent to the reader how the ETS framework is built upon the idea of time series decomposition. By introducing different components, defining their types, and adding the equations for their update, we can construct models that would work better in capturing the key features of the time series. But we should also consider the potential change in components over time. The “transition” or “state” equations are supposed to reflect this change: they explain how the level, trend or seasonal components evolve. As discussed in Section 2.2, given different types of components and their interactions, we end up with 30 models in the taxonomy. Tables 1 and 2 summarise mathematically all 30 ETS models shown graphically on Figures 1 and 2, presenting formulae for measurement and transition equations. Table 1: Additive error ETS models | | Nonseasonal | Additive | Multiplicative | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | No trend | $\begin{aligned} &y_{t} = l_{t-1} + \epsilon_t \\ &l_t = l_{t-1} + \alpha \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} + s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + \alpha \epsilon_t \\ &s_t = s_{t-m} + \gamma \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + \alpha \frac{\epsilon_t}{s_{t-m}} \\ &s_t = s_{t-m} + \gamma \frac{\epsilon_t}{l_{t-1}} \end{aligned}$ | | Additive | $\begin{aligned} &y_{t} = l_{t-1} + b_{t-1} + \epsilon_t \\ &l_t = l_{t-1} + b_{t-1} + \alpha \epsilon_t \\ &b_t = b_{t-1} + \beta \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} + b_{t-1} + s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + b_{t-1} + \alpha \epsilon_t \\ &b_t = b_{t-1} + \beta \epsilon_t \\ &s_t = s_{t-m} + \gamma \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + b_{t-1}) s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + b_{t-1} + \alpha \frac{\epsilon_t}{s_{t-m}} \\ &b_t = b_{t-1} + \beta \frac{\epsilon_t}{s_{t-m}} \\ &s_t = s_{t-m} + \gamma \frac{\epsilon_t}{l_{t-1} + b_{t-1}} \end{aligned}$ | | Additive damped | $\begin{aligned} &y_{t} = l_{t-1} + \phi b_{t-1} + \epsilon_t \\ &l_t = l_{t-1} + \phi b_{t-1} + \alpha \epsilon_t \\ &b_t = \phi b_{t-1} + \beta \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} + \phi b_{t-1} + s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + \phi b_{t-1} + \alpha \epsilon_t \\ &b_t = \phi b_{t-1} + \beta \epsilon_t \\ &s_t = s_{t-m} + \gamma \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + \phi b_{t-1}) s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} + \phi b_{t-1} + \alpha \frac{\epsilon_t}{s_{t-m}} \\ &b_t = \phi b_{t-1} + \beta \frac{\epsilon_t}{s_{t-m}} \\ &s_t = s_{t-m} + \gamma \frac{\epsilon_t}{l_{t-1} + \phi b_{t-1}} \end{aligned}$ | | Multiplicative | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1} + \epsilon_t \\ &l_t = l_{t-1} b_{t-1} + \alpha \epsilon_t \\ &b_t = b_{t-1} + \beta \frac{\epsilon_t}{l_{t-1}} \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1} + s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} b_{t-1} + \alpha \epsilon_t \\ &b_t = b_{t-1} + \beta \frac{\epsilon_t}{l_{t-1}} \\ &s_t = s_{t-m} + \gamma \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1} s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} b_{t-1} + \alpha \frac{\epsilon_t}{s_{t-m}} \\ &b_t = b_{t-1} + \beta \frac{\epsilon_t}{l_{t-1}s_{t-m}} \\ &s_t = s_{t-m} + \gamma \frac{\epsilon_t}{l_{t-1} b_{t-1}} \end{aligned}$ | | Multiplicative damped | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1}^\phi + \epsilon_t \\ &l_t = l_{t-1} b_{t-1}^\phi + \alpha \epsilon_t \\ &b_t = b_{t-1}^\phi + \beta \frac{\epsilon_t}{l_{t-1}} \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1}^\phi + s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} b_{t-1}^\phi + \alpha \epsilon_t \\ &b_t = b_{t-1}^\phi + \beta \frac{\epsilon_t}{l_{t-1}} \\ &s_t = s_{t-m} + \gamma \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1}^\phi s_{t-m} + \epsilon_t \\ &l_t = l_{t-1} b_{t-1}^\phi + \alpha \frac{\epsilon_t}{s_{t-m}} \\ &b_t = b_{t-1}^\phi + \beta \frac{\epsilon_t}{l_{t-1}s_{t-m}} \\ &s_t = s_{t-m} + \gamma \frac{\epsilon_t}{l_{t-1} b_{t-1}} \end{aligned}$ | Table 2: Multiplicative error ETS models | | Nonseasonal | Additive | Multiplicative | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | No trend | $\begin{aligned} &y_{t} = l_{t-1}(1 + \epsilon_t) \\ &l_t = l_{t-1}(1 + \alpha \epsilon_t) \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + s_{t-m})(1 + \epsilon_t) \\ &l_t = l_{t-1} + \alpha \mu_{y,t} \epsilon_t \\ &s_t = s_{t-m} + \gamma \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} s_{t-m}(1 + \epsilon_t) \\ &l_t = l_{t-1}(1 + \alpha \epsilon_t) \\ &s_t = s_{t-m}(1 + \gamma \epsilon_t) \end{aligned}$ | | Additive | $\begin{aligned} &y_{t} = (l_{t-1} + b_{t-1})(1 + \epsilon_t) \\ &l_t = (l_{t-1} + b_{t-1})(1 + \alpha \epsilon_t) \\ &b_t = b_{t-1} + \beta \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + b_{t-1} + s_{t-m})(1 + \epsilon_t) \\ &l_t = l_{t-1} + b_{t-1} + \alpha \mu_{y,t} \epsilon_t \\ &b_t = b_{t-1} + \beta \mu_{y,t} \epsilon_t \\ &s_t = s_{t-m} + \gamma \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + b_{t-1}) s_{t-m}(1 + \epsilon_t) \\ &l_t = (l_{t-1} + b_{t-1})(1 + \alpha \epsilon_t) \\ &b_t = b_{t-1} + \beta (l_{t-1} + b_{t-1}) \epsilon_t \\ &s_t = s_{t-m} (1 + \gamma \epsilon_t) \end{aligned}$ | | Additive damped | $\begin{aligned} &y_{t} = (l_{t-1} + \phi b_{t-1})(1 + \epsilon_t) \\ &l_t = (l_{t-1} + \phi b_{t-1})(1 + \alpha \epsilon_t) \\ &b_t = \phi b_{t-1} + \beta \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + \phi b_{t-1} + s_{t-m})(1 + \epsilon_t) \\ &l_t = l_{t-1} + \phi b_{t-1} + \alpha \mu_{y,t} \epsilon_t \\ &b_t = \phi b_{t-1} + \beta \mu_{y,t} \epsilon_t \\ &s_t = s_{t-m} + \gamma \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} + \phi b_{t-1}) s_{t-m}(1 + \epsilon_t) \\ &l_t = l_{t-1} + \phi b_{t-1} (1 + \alpha \epsilon_t) \\ &b_t = \phi b_{t-1} + \beta (l_{t-1} + \phi b_{t-1}) \epsilon_t \\ &s_t = s_{t-m}(1 + \gamma \epsilon_t) \end{aligned}$ | | Multiplicative | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1} (1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1} (1 + \alpha \epsilon_t) \\ &b_t = b_{t-1} (1 + \beta \epsilon_t) \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} b_{t-1} + s_{t-m})(1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1} + \alpha \mu_{y,t} \epsilon_t \\ &b_t = b_{t-1} + \beta \frac{\mu_{y,t}}{l_{t-1}} \epsilon_t \\ &s_t = s_{t-m} + \gamma \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1} s_{t-m} (1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1} (1 + \alpha \epsilon_t) \\ &b_t = b_{t-1} (1 + \beta \epsilon_t) \\ &s_t = s_{t-m} (1 + \gamma \epsilon_t) \end{aligned}$ | | Multiplicative damped | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1}^\phi (1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1}^\phi (1 + \alpha \epsilon_t) \\ &b_t = b_{t-1}^\phi (1 + \beta \epsilon_t) \end{aligned}$ | $\begin{aligned} &y_{t} = (l_{t-1} b_{t-1}^\phi + s_{t-m})(1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1}^\phi + \alpha \mu_{y,t} \epsilon_t \\ &b_t = b_{t-1}^\phi + \beta \frac{\mu_{y,t}}{l_{t-1}} \epsilon_t \\ &s_t = s_{t-m} + \gamma \mu_{y,t} \epsilon_t \end{aligned}$ | $\begin{aligned} &y_{t} = l_{t-1} b_{t-1}^\phi s_{t-m} (1 + \epsilon_t) \\ &l_t = l_{t-1} b_{t-1}^\phi \left(1 + \alpha \epsilon_t\right) \\ &b_t = b_{t-1}^\phi \left(1 + \beta \epsilon_t\right) \\ &s_t = s_{t-m} \left(1 + \gamma \epsilon_t\right) \end{aligned}$ | From a statistical point of view, formulae in Tables 1 and 2 correspond to the “true models”, they explain the models underlying potential data, but when it comes to their construction and estimation, the $\epsilon_t$ is substituted by the estimated $e_t$ (which is calculated differently depending on the error type), and time series components and smoothing parameters are also replaced by their estimates (e.g. $\hat \alpha$ instead of $\alpha$). However, if the values of these models’ parameters were known, it would be possible to produce point forecasts and conditional h steps ahead expectations from these models. ### Model selection A great advantage of the `Holt Winters` statistical framework is that information criteria can be used for model selection. The `AIC, AIC_c` and `BIC`, can be used here to determine which of the `Holt Winters` models is most appropriate for a given time series. For `Holt Winters` models, Akaike’s Information Criterion (`AIC)` is defined as $\text{AIC} = -2\log(L) + 2k,$ where $L$ is the likelihood of the model and $k$ is the total number of parameters and initial states that have been estimated (including the residual variance). The `AIC` corrected for small sample bias `(AIC_c)` is defined as $AIC_c = AIC + \frac{2k(k+1)}{T-k-1}$ and the Bayesian Information Criterion `(BIC)` is $\text{BIC} = \text{AIC} + k[\log(T)-2]$ Three of the combinations of (Error, Trend, Seasonal) can lead to numerical difficulties. Specifically, the models that can cause such instabilities are `ETS(A,N,M), ETS(A,A,M)`, and `ETS(A,Ad,M)`, due to division by values potentially close to zero in the state equations. We normally do not consider these particular combinations when selecting a model. Models with multiplicative errors are useful when the data are strictly positive, but are not numerically stable when the data contain zeros or negative values. Therefore, multiplicative error models will not be considered if the time series is not strictly positive. In that case, only the six fully additive models will be applied. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/ads.csv") df.head() ``` | | Time | Ads | | - | ------------------- | ------ | | 0 | 2017-09-13T00:00:00 | 80115 | | 1 | 2017-09-13T01:00:00 | 79885 | | 2 | 2017-09-13T02:00:00 | 89325 | | 3 | 2017-09-13T03:00:00 | 101930 | | 4 | 2017-09-13T04:00:00 | 121630 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | ------ | ---------- | | 0 | 2017-09-13T00:00:00 | 80115 | 1 | | 1 | 2017-09-13T01:00:00 | 79885 | 1 | | 2 | 2017-09-13T02:00:00 | 89325 | 1 | | 3 | 2017-09-13T03:00:00 | 101930 | 1 | | 4 | 2017-09-13T04:00:00 | 121630 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### The Augmented Dickey-Fuller Test An Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis. Augmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis. * Null Hypothesis: Time Series is non-stationary. It gives a time-dependent trend. * Alternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time. * ADF or t Statistic \< critical values: Reject the null hypothesis, time series is stationary. * ADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary. ```python theme={null} from statsmodels.tsa.stattools import adfuller def Augmented_Dickey_Fuller_Test_func(series , column_name): print (f'Dickey-Fuller test results for columns: {column_name}') dftest = adfuller(series, autolag='AIC') dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used']) for key,value in dftest[4].items(): dfoutput['Critical Value (%s)'%key] = value print (dfoutput) if dftest[1] <= 0.05: print("Conclusion:====>") print("Reject the null hypothesis") print("The data is stationary") else: print("Conclusion:====>") print("The null hypothesis cannot be rejected") print("The data is not stationary") ``` ```python theme={null} Augmented_Dickey_Fuller_Test_func(df["y"],'Ads') ``` ```text theme={null} Dickey-Fuller test results for columns: Ads Test Statistic -7.089634e+00 p-value 4.444804e-10 No Lags Used 9.000000e+00 ... Critical Value (1%) -3.462499e+00 Critical Value (5%) -2.875675e+00 Critical Value (10%) -2.574304e+00 Length: 7, dtype: float64 Conclusion:====> Reject the null hypothesis The data is stationary ``` ### Autocorrelation plots **Autocorrelation Function** **Definition 1.** Let $\{x_t;1 ≤ t ≤ n\}$ be a time series sample of size n from $\{X_t\}$. 1. $\bar x = \sum_{t=1}^n \frac{x_t}{n}$ is called the sample mean of $\{X_t\}$. 2. $c_k =\sum_{t=1}^{n−k} (x_{t+k}- \bar x)(x_t−\bar x)/n$ is known as the sample autocovariance function of $\{X_t\}$. 3. $r_k = c_k /c_0$ is said to be the sample autocorrelation function of $\{X_t\}$. Note the following remarks about this definition: * Like most literature, this guide uses ACF to denote the sample autocorrelation function as well as the autocorrelation function. What is denoted by ACF can easily be identified in context. * Clearly c0 is the sample variance of $\{X_t\}$. Besides, $r_0 = c_0/c_0 = 1$ and for any integer $k, |r_k| ≤ 1$. * When we compute the ACF of any sample series with a fixed length $n$, we cannot put too much confidence in the values of $r_k$ for large k’s, since fewer pairs of $(x_{t +k }, x_t )$ are available for calculating $r_k$ as $k$ is large. One rule of thumb is not to estimate $r_k$ for $k > n/3$, and another is $n ≥ 50, k ≤ n/4$. In any case, it is always a good idea to be careful. * We also compute the ACF of a nonstationary time series sample by Definition 1. In this case, however, the ACF or $r_k$ very slowly or hardly tapers off as $k$ increases. * Plotting the ACF $(r_k)$ against lag $k$ is easy but very helpful in analyzing time series sample. Such an ACF plot is known as a correlogram. * If $\{X_t\}$ is stationary with $E(X_t)=0$ and $\rho_k =0$ for all $k \neq 0$,thatis,itisa white noise series, then the sampling distribution of $r_k$ is asymptotically normal with the mean 0 and the variance of $1/n$. Hence, there is about 95% chance that $r_k$ falls in the interval $[−1.96/\sqrt{n}, 1.96/\sqrt{n}]$. Now we can give a summary that (1) if the time series plot of a time series clearly shows a trend or/and seasonality, it is surely nonstationary; (2) if the ACF $r_k$ very slowly or hardly tapers off as lag $k$ increases, the time series should also be nonstationary. ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ### Additive ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "additive", period=24) a.plot(); ``` ### Multiplicative ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "Multiplicative", period=24) a.plot(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Holt Winters Model`. 2. Data to test our model For the test data we will use the last 30 hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2017-09-20 17:00:00'] test = df[df.ds>'2017-09-20 17:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((186, 3), (30, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train", linestyle="--",linewidth=2) sns.lineplot(test, x="ds", y="y", label="Test", linewidth=2, color="yellow") plt.title("Ads watched (hourly data)"); plt.show() ``` ## Implementation of Holt-Winters Method with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import HoltWinters ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. In this case we are going to test two alternatives of the model, one additive and one multiplicative. ```python theme={null} season_length = 24 # Hourly data horizon = len(test) # number of predictions models = [HoltWinters(season_length=season_length, error_type="A", alias="Add"), HoltWinters(season_length=season_length, error_type="M", alias="Multi")] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[Add,Multi]) ``` Let’s see the results of our `Holt Winters Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ print(result.keys()) print(result['fit']) ``` ```text theme={null} dict_keys(['loglik', 'aic', 'bic', 'aicc', 'mse', 'amse', 'fit', 'residuals', 'components', 'm', 'nstate', 'fitted', 'states', 'par', 'sigma2', 'n_params', 'method', 'actual_residuals']) results(x=array([ 2.60632491e-02, 1.53030002e-03, 3.22298668e-02, 9.00958233e-01, 1.23628350e+05, -5.12405452e+01, -3.96677340e+04, -2.83800237e+04, -1.49514829e+04, 1.05413201e+04, 3.65409126e+04, 3.58433030e+04, 2.93235036e+04, 2.66607410e+04, 2.55392078e+04, 2.60970444e+04, 2.63155973e+04, 2.83192738e+04, 2.16640268e+04, 5.19120023e+03, -6.15595960e+03, -8.84863887e+03, -9.28320586e+03, -8.09549672e+03, -3.83755898e+03, -3.33456554e+03, -2.56333963e+04, -3.72181618e+04, -4.42497509e+04]), fn=4363.098387651742, nit=1001, simplex=None) ``` Let us now visualize the fitted values of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("residuals"), columns=["residual Model"]) residual ``` | | residual Model | | --- | -------------- | | 0 | -1087.029091 | | 1 | 623.989786 | | 2 | 3054.101324 | | ... | ... | | 183 | -2783.032921 | | 184 | -4618.147123 | | 185 | -8194.063498 | ```python theme={null} import scipy.stats as stats fig, axs = plt.subplots(nrows=2, ncols=2) residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 hours ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | Add | Multi | | --- | ---------- | ------------------- | ------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 154164.609375 | 151414.984375 | | 1 | 1 | 2017-09-20 19:00:00 | 154547.171875 | 152352.640625 | | 2 | 1 | 2017-09-20 20:00:00 | 128790.359375 | 128274.789062 | | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 103021.726562 | 103086.851562 | | 28 | 1 | 2017-09-21 22:00:00 | 89544.054688 | 90028.406250 | | 29 | 1 | 2017-09-21 23:00:00 | 78090.210938 | 78823.953125 | With the forecast method we can also extract the fitted values from the model and visualize it graphically, with the following instruction we can do it. ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | Add | Multi | | - | ---------- | ------------------- | -------- | ------------- | ------------- | | 0 | 1 | 2017-09-13 00:00:00 | 80115.0 | 81202.031250 | 79892.687500 | | 1 | 1 | 2017-09-13 01:00:00 | 79885.0 | 79261.007812 | 78792.476562 | | 2 | 1 | 2017-09-13 02:00:00 | 89325.0 | 86270.898438 | 85444.117188 | | 3 | 1 | 2017-09-13 03:00:00 | 101930.0 | 97905.273438 | 97286.796875 | | 4 | 1 | 2017-09-13 04:00:00 | 121630.0 | 120287.523438 | 118195.570312 | ```python theme={null} StatsForecast.plot(values) ``` Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | Add | Add-lo-95 | Add-hi-95 | Multi | Multi-lo-95 | Multi-hi-95 | | --- | ---------- | ------------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 154164.609375 | 134594.859375 | 173734.375000 | 151414.984375 | 125296.867188 | 177533.109375 | | 1 | 1 | 2017-09-20 19:00:00 | 154547.171875 | 134970.062500 | 174124.265625 | 152352.640625 | 126234.515625 | 178470.765625 | | 2 | 1 | 2017-09-20 20:00:00 | 128790.359375 | 109205.242188 | 148375.484375 | 128274.789062 | 102156.671875 | 154392.906250 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 103021.726562 | 83118.632812 | 122924.812500 | 103086.851562 | 76659.867188 | 129513.835938 | | 28 | 1 | 2017-09-21 22:00:00 | 89544.054688 | 69626.210938 | 109461.890625 | 90028.406250 | 63601.425781 | 116455.390625 | | 29 | 1 | 2017-09-21 23:00:00 | 78090.210938 | 58157.574219 | 98022.843750 | 78823.953125 | 52396.972656 | 105250.937500 | ```python theme={null} sf.plot(train, Y_hat) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 hours ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | Add | Multi | | --- | ---------- | ------------------- | ------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 154164.609375 | 151414.984375 | | 1 | 1 | 2017-09-20 19:00:00 | 154547.171875 | 152352.640625 | | 2 | 1 | 2017-09-20 20:00:00 | 128790.359375 | 128274.789062 | | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 103021.726562 | 103086.851562 | | 28 | 1 | 2017-09-21 22:00:00 | 89544.054688 | 90028.406250 | | 29 | 1 | 2017-09-21 23:00:00 | 78090.210938 | 78823.953125 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[80,95]) forecast_df ``` | | unique\_id | ds | Add | Add-lo-95 | Add-lo-80 | Add-hi-80 | Add-hi-95 | Multi | Multi-lo-95 | Multi-lo-80 | Multi-hi-80 | Multi-hi-95 | | --- | ---------- | ------------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 154164.609375 | 134594.859375 | 141368.640625 | 166960.593750 | 173734.375000 | 151414.984375 | 125296.867188 | 134337.265625 | 168492.703125 | 177533.109375 | | 1 | 1 | 2017-09-20 19:00:00 | 154547.171875 | 134970.062500 | 141746.390625 | 167347.953125 | 174124.265625 | 152352.640625 | 126234.515625 | 135274.921875 | 169430.359375 | 178470.765625 | | 2 | 1 | 2017-09-20 20:00:00 | 128790.359375 | 109205.242188 | 115984.335938 | 141596.375000 | 148375.484375 | 128274.789062 | 102156.671875 | 111197.070312 | 145352.515625 | 154392.906250 | | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 103021.726562 | 83118.632812 | 90007.796875 | 116035.656250 | 122924.812500 | 103086.851562 | 76659.867188 | 85807.171875 | 120366.523438 | 129513.835938 | | 28 | 1 | 2017-09-21 22:00:00 | 89544.054688 | 69626.210938 | 76520.476562 | 102567.632812 | 109461.890625 | 90028.406250 | 63601.425781 | 72748.734375 | 107308.085938 | 116455.390625 | | 29 | 1 | 2017-09-21 23:00:00 | 78090.210938 | 58157.574219 | 65056.960938 | 91123.460938 | 98022.843750 | 78823.953125 | 52396.972656 | 61544.281250 | 96103.632812 | 105250.937500 | ```python theme={null} sf.plot(train, forecast_df, level=[80, 95]) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=30, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier. * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | Add | Multi | | --- | ---------- | ------------------- | ------------------- | -------- | ------------- | ------------- | | 0 | 1 | 2017-09-18 06:00:00 | 2017-09-18 05:00:00 | 99440.0 | 134578.328125 | 133820.109375 | | 1 | 1 | 2017-09-18 07:00:00 | 2017-09-18 05:00:00 | 97655.0 | 133548.781250 | 133734.000000 | | 2 | 1 | 2017-09-18 08:00:00 | 2017-09-18 05:00:00 | 97655.0 | 134798.656250 | 135216.046875 | | ... | ... | ... | ... | ... | ... | ... | | 87 | 1 | 2017-09-21 21:00:00 | 2017-09-20 17:00:00 | 103080.0 | 103021.726562 | 103086.851562 | | 88 | 1 | 2017-09-21 22:00:00 | 2017-09-20 17:00:00 | 95155.0 | 89544.054688 | 90028.406250 | | 89 | 1 | 2017-09-21 23:00:00 | 2017-09-20 17:00:00 | 80285.0 | 78090.210938 | 78823.953125 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | Add | Multi | | - | ---------- | ------ | ----------- | ----------- | | 0 | 1 | mae | 4306.244531 | 4886.992188 | | 1 | 1 | mape | 0.038087 | 0.043549 | | 2 | 1 | mase | 0.532045 | 0.603797 | | 3 | 1 | rmse | 5415.015573 | 5862.473702 | | 4 | 1 | smape | 0.018708 | 0.021433 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla HoltWinters API](../../src/core/models.html#holtwinters) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # IMAPA Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/imapa.html > Step-by-step guide on using the `IMAPA Model` with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [IMAPA Model](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of IMAPA with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction IMAPA is an algorithm that uses multiple models to forecast the future values of an intermittent time series. The algorithm starts by adding the time series values at regular intervals. It then uses a forecast model to forecast the added values. IMAPA is a good choice for intermittent time series because it is robust to missing values and is computationally efficient. IMAPA is also easy to implement. IMAPA has been tested on a variety of intermittent time series and has been shown to be effective in forecasting future values. ## IMAPA Method The Intermittent Multiple Aggregation Prediction Algorithm (IMAPA) model is a time series model for forecasting future values for time series that are intermittent. The IMAPA model is based on the idea of aggregating the time series values at regular intervals and then using a forecast model to forecast the aggregated values. The aggregated values can be forecast using any forecast model. It uses the optimized SES to generate the forecasts at the new levels and then combines them using a simple average. The IMAPA model can be defined mathematically as follows: $\hat{y}_{t+1} = f(\hat{y}_{t-\tau}, \hat{y}_{t-2\tau}, ..., \hat{ y}_{t-m\tau})$ where $\hat{y}_{t+1}$ is the forecast time value $t+1$, $f$ is the forecast model, $\hat{y}_{t-\tau} , \hat{y}_{t-2\tau}, ..., \hat{y}_{t-m\tau}$ are the forecasts of the added values at times $t-\tau, t-2 \tau, ..., t-m\tau$, and $\tau$ is the time interval over which the time series values are aggregated. IMAPA is a good choice for intermittent time series because it is robust to missing values and is computationally efficient. IMAPA is also easy to implement. IMAPA has been tested on a variety of intermittent time series and has been shown to be effective in forecasting future values. ### IMAPA General Properties * Multiple Aggregation: IMAPA uses multiple levels of aggregation to analyze and predict intermittent time series. This involves decomposing the original series into components of different time scales. * Intermittency: IMAPA focuses on handling intermittent time series, which are those that exhibit irregular and non-stationary patterns with periods of activity and periods of inactivity. * Adaptive Prediction: IMAPA uses an adaptive approach to adjust prediction models as new data is collected. This allows the algorithm to adapt to changes in the time series behavior over time. * Robust to Missing Values: IMAPA can handle missing values in the data without sacrificing accuracy. This is important for intermittent time series, which often have missing values. * Computationally Efficient: IMAPA is computationally efficient, meaning it can forecast future values quickly. This is important for large time series, which can take a long time to forecast using other methods. * Decomposition Property: Time series can be decomposed into components such as trend, seasonality, and residual components. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/intermittend_demand2") df.head() ``` | | date | sales | | - | ------------------- | ----- | | 0 | 2022-01-01 00:00:00 | 0 | | 1 | 2022-01-01 01:00:00 | 10 | | 2 | 2022-01-01 02:00:00 | 0 | | 3 | 2022-01-01 03:00:00 | 0 | | 4 | 2022-01-01 04:00:00 | 100 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | --- | ---------- | | 0 | 2022-01-01 00:00:00 | 0 | 1 | | 1 | 2022-01-01 01:00:00 | 10 | 1 | | 2 | 2022-01-01 02:00:00 | 0 | 1 | | 3 | 2022-01-01 03:00:00 | 0 | 1 | | 4 | 2022-01-01 04:00:00 | 100 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots Autocorrelation (ACF) and partial autocorrelation (PACF) plots are statistical tools used to analyze time series. ACF charts show the correlation between the values of a time series and their lagged values, while PACF charts show the correlation between the values of a time series and their lagged values, after the effect of previous lagged values has been removed. ACF and PACF charts can be used to identify the structure of a time series, which can be helpful in choosing a suitable model for the time series. For example, if the ACF chart shows a repeating peak and valley pattern, this indicates that the time series is stationary, meaning that it has the same statistical properties over time. If the PACF chart shows a pattern of rapidly decreasing spikes, this indicates that the time series is invertible, meaning it can be reversed to get a stationary time series. The importance of the ACF and PACF charts is that they can help analysts better understand the structure of a time series. This understanding can be helpful in choosing a suitable model for the time series, which can improve the ability to predict future values of the time series. To analyze ACF and PACF charts: * Look for patterns in charts. Common patterns include repeating peaks and valleys, sawtooth patterns, and plateau patterns. * Compare ACF and PACF charts. The PACF chart generally has fewer spikes than the ACF chart. * Consider the length of the time series. ACF and PACF charts for longer time series will have more spikes. * Use a confidence interval. The ACF and PACF plots also show confidence intervals for the autocorrelation values. If an autocorrelation value is outside the confidence interval, it is likely to be significant. ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); # Grafico plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose from plotly.subplots import make_subplots import plotly.graph_objects as go def plotSeasonalDecompose( x, model='additive', filt=None, period=None, two_sided=True, extrapolate_trend=0, title="Seasonal Decomposition"): result = seasonal_decompose( x, model=model, filt=filt, period=period, two_sided=two_sided, extrapolate_trend=extrapolate_trend) fig = make_subplots( rows=4, cols=1, subplot_titles=["Observed", "Trend", "Seasonal", "Residuals"]) for idx, col in enumerate(['observed', 'trend', 'seasonal', 'resid']): fig.add_trace( go.Scatter(x=result.observed.index, y=getattr(result, col), mode='lines'), row=idx+1, col=1, ) return fig ``` ```python theme={null} plotSeasonalDecompose( df["y"], model="additive", period=24, title="Seasonal Decomposition") ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `IMAPA Model`. 2. Data to test our model For the test data we will use the last 500 Hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2023-01-31 19:00:00'] test = df[df.ds>'2023-01-31 19:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((9500, 3), (500, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train", linestyle="--",linewidth=2) sns.lineplot(test, x="ds", y="y", label="Test", linewidth=2, color="yellow") plt.title("Store visit"); plt.xlabel("Hours") plt.show() ``` ## Implementation of IMAPA Method with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import IMAPA ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 24 # Hourly data horizon = len(test) # number of predictions models = [IMAPA()] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[IMAPA]) ``` Let’s see the results of our `IMAPA Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'mean': array([28.579695], dtype=float32)} ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 500 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon) Y_hat ``` | | unique\_id | ds | IMAPA | | --- | ---------- | ------------------- | --------- | | 0 | 1 | 2023-01-31 20:00:00 | 28.579695 | | 1 | 1 | 2023-01-31 21:00:00 | 28.579695 | | 2 | 1 | 2023-01-31 22:00:00 | 28.579695 | | ... | ... | ... | ... | | 497 | 1 | 2023-02-21 13:00:00 | 28.579695 | | 498 | 1 | 2023-02-21 14:00:00 | 28.579695 | | 499 | 1 | 2023-02-21 15:00:00 | 28.579695 | ```python theme={null} sf.plot(train, Y_hat) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 500 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecast_df = sf.predict(h=horizon) forecast_df ``` | | unique\_id | ds | IMAPA | | --- | ---------- | ------------------- | --------- | | 0 | 1 | 2023-01-31 20:00:00 | 28.579695 | | 1 | 1 | 2023-01-31 21:00:00 | 28.579695 | | 2 | 1 | 2023-01-31 22:00:00 | 28.579695 | | ... | ... | ... | ... | | 497 | 1 | 2023-02-21 13:00:00 | 28.579695 | | 498 | 1 | 2023-02-21 14:00:00 | 28.579695 | | 499 | 1 | 2023-02-21 15:00:00 | 28.579695 | ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second months `(step_size=50)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 500 hours ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=50, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` index. If you dont like working with index just run `crossvalidation_df.resetindex()`. * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | IMAPA | | ---- | ---------- | ------------------- | ------------------- | ---- | --------- | | 0 | 1 | 2023-01-23 12:00:00 | 2023-01-23 11:00:00 | 0.0 | 15.134251 | | 1 | 1 | 2023-01-23 13:00:00 | 2023-01-23 11:00:00 | 0.0 | 15.134251 | | 2 | 1 | 2023-01-23 14:00:00 | 2023-01-23 11:00:00 | 0.0 | 15.134251 | | ... | ... | ... | ... | ... | ... | | 2497 | 1 | 2023-02-21 13:00:00 | 2023-01-31 19:00:00 | 60.0 | 28.579695 | | 2498 | 1 | 2023-02-21 14:00:00 | 2023-01-31 19:00:00 | 20.0 | 28.579695 | | 2499 | 1 | 2023-02-21 15:00:00 | 2023-01-31 19:00:00 | 20.0 | 28.579695 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | IMAPA | | - | ---------- | ------ | --------- | | 0 | 1 | mae | 34.206428 | | 1 | 1 | mape | 0.637417 | | 2 | 1 | mase | 0.816042 | | 3 | 1 | rmse | 45.345223 | | 4 | 1 | smape | 0.764973 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla IMAPA API](../../src/core/models.html#imapa) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # MFLES Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/mfles.html > MFLES is a simple time series method based on gradient boosting time > series decomposition. There are numerous methods that can enter the boosting loop depending on user-provided parameters or some quick logic MFLES does automatically that seems to work ok. Some of these methods are: 1. SES Ensemble 2. Simple Moving Average 3. Piecewise Linear Trend 4. Fourier Basis function regression for seasonality 5. Simple Median 6. A Robust Linear Method for trend # **Gradient Boosted Decomposition** This approach aims to view a time series decomposition (trend, seasonality, and exogenous) as the ‘weak’ estimator in a gradient boosting procedure. The major relevant changes to note are: 1. The trend estimator will always go from simple to complex. Beginning with a median, then to a linear/piecewise linear, then to some sort of smoother. 2. Multiple seasonality is fit one seasonality per boosting round rather than simultaneously. This means you should organize your seasonality in order of perceived importance. Also, theoretically, you can have up to 50 seasonalities present by default, but after 3 you should expect degraded performance. 3. Learning rates are now estimator specific rather than a single parameter like you would see in something like XGBoost. This is useful if you have exogenous signals that are also seasonal, you (this will not be done automatically) can optimize for the combination of the seasonal signal and the exogenous signal. # **Let’s forecast** ```python theme={null} # %pip install statsforecast ``` Here, we will use the specific model object in Statsforecast and the infamous airline passengers dataset 😀: ```python theme={null} import pandas as pd import numpy as np from statsforecast.models import AutoMFLES import matplotlib.pyplot as plt df = pd.read_csv(r'https://raw.githubusercontent.com/jbrownlee/Datasets/master/airline-passengers.csv') y = df['Passengers'].values # make array mfles_model = AutoMFLES( season_length = [12], test_size = 12, n_windows=2, metric = 'smape') mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] ``` ```python theme={null} plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` Let’s take a look at some of the key parameters for a standard experience. * **season\_length**: a list of seasonal periods, in order of perceived importance preferably. * **test\_size**: AutoMFLES is optimized via time series cross validation. The test size dictates how many periods to use in each test fold. **This is probably the most important parameter when it comes to optimizing and you should weigh the season length, forecast horizon, and general data length when setting this. But a good rule of thumb is either the most important season length or half that to allow MFLES to pick up on seasonality.** * **n\_windows**: how many test sets are used in optimizing parameters. In this example, 2 means that we, in total, use 24 months (12 \* 2) split between the 2 windows. * **metric**: this one is easy, it is simply the metric we want to optimize for with our parameters. Here we use the default which is smape that is defaulted to reproduce experiment results on M4. You can also pass ‘rmse’, ‘mape’, or ‘mae’ to optimize for another metric. # **A deeper look at a more customized model** The previous fit is done with 99% automated logic checks and grid searched parameters. But we can manipulate the fit greatly (maybe too much). This section will overview some very important parameters and how they effect the output. ## **The parameter grid search** First, let’s take a look at the default grid of parameters AutoMFLES will try: ```python theme={null} config = { 'seasonality_weights': [True, False], 'smoother': [True, False], 'ma': [int(min(seasonal_period)), int(min(seasonal_period)/2),None], 'seasonal_period': [None, seasonal_period], } ``` * **seasonality\_weights**: If True, we will weigh more recent observations more when calculating seasonality. The allows a deterministic seasonality to reflect more recent changes. * **smoother**: True means we will use a simple exponential smoother to fit on residuals after a few rounds of boosting. If the parameter is False then we use a simple moving average * **ma**: This parameter is the number of past observations to include when using a moving average, None indicates it will be semi-auto set or disregarded in the case of ‘smoother’ being True. For optimizing we search for the minimum season length provided by you or that number divided by 2. * **seasonal\_period**: this is the list of season\_length provided by the you Now let’s see how to pass the config to AutoMFLES, since this is what we use under-the-hood the results will be the same! ```python theme={null} season_length = [12] config = { 'seasonality_weights': [True, False], 'smoother': [True, False], 'ma': [int(min(season_length)), int(min(season_length)/2),None], 'seasonal_period': [None, season_length], } mfles_model = AutoMFLES( season_length = season_length, test_size = 12, n_windows=2, metric = 'smape', config=config) # adding the config dictionary manually mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` ### What if you want to force a less reactive forecast? Just pass False for the smoother and adjust ma to be larger relative to your seasonality ```python theme={null} season_length = [12] config = { 'seasonality_weights': [True, False], 'smoother': [False], 'ma': [30], 'seasonal_period': [None, season_length], } mfles_model = AutoMFLES( season_length = season_length, test_size = 12, n_windows=2, metric = 'smape', config=config) # adding the config dictionary manually mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` ### **Forcing** **Seasonality** Sometimes a seasonal series is auto-fit with a nonseasonal setting, to adjust this and force seasonality, jsut remove the ‘None’ setting in the seasonal\_period list. This also reduces the number of configurations MFLES tries and therefore speeds up the fitting. ```python theme={null} season_length = [12] config = { 'seasonality_weights': [True, False], 'smoother': [False], 'ma': [30], 'seasonal_period': [season_length], } mfles_model = AutoMFLES( season_length = season_length, test_size = 12, n_windows=2, metric = 'smape', config=config) # adding the config dictionary manually mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` ## **Controlling the Complexity** One of the best ways to control for complexity is with the max\_rounds parameter. By default this is set to 50 but most of the time the model converges much quicker than that. At round 4 we start implementing smoothers as the trend piece so if you do not want that then set the max\_rounds to 3! But, you probably want the smoothers! ```python theme={null} season_length = [12] config = { 'seasonality_weights': [True, False], 'smoother': [True, False], 'ma': [int(min(season_length)), int(min(season_length)/2),None], 'seasonal_period': [None, season_length], 'max_rounds': [3], } mfles_model = AutoMFLES( season_length = season_length, test_size = 12, n_windows=2, metric = 'smape', config=config) # adding the config dictionary manually mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` You can also leverage estimator specific learning rates which are applied to individual estimators rather than the entire boosting round. Useful if you notice that the residual smoother is eating too much signal too quickly: ```python theme={null} season_length = [12] config = { 'seasonality_weights': [True, False], 'smoother': [True, False], 'ma': [int(min(season_length)), int(min(season_length)/2),None], 'seasonal_period': [None, season_length], 'rs_lr': [.2], } mfles_model = AutoMFLES( season_length = season_length, test_size = 12, n_windows=2, metric = 'smape', config=config) # adding the config dictionary manually mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` ## **Tips and Tricks** Since most settings are optimized for during cross validation there is always a trade-off between accuracy and computation. The default settings were done after extensive testing to give you a balanced approach. Hopefully, it delivers good accuracy in a short amount of time. But, there are ways to give you generally more accuracy (not life changing but a slight boost) or a dramatic decrease in runtime (without sacrificing too much accuracy). The next section will review some of those settings! ## **Number of Testing Windows** When optimizing using time series cross validation the number of windows directly effects the number of times we have to fit the model for each parameter. The default here is 2, but going up to 3 (if your data allows it) should give you more consistent results. Obviously, the more the better to a certain point but this will depend on your data. Conversely, decreasing this to 1 means you are choosing parameters based on a single holdout set which may decrease accuracy. ```python theme={null} season_length = [12] mfles_model = AutoMFLES( season_length = season_length, test_size = 12, n_windows = 1, # Trying just 1 window here metric = 'smape') mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` And now trying with 3, notice the fit is different! ```python theme={null} season_length = [12] mfles_model = AutoMFLES( season_length = season_length, test_size = 12, n_windows = 3, # Trying just 1 window here metric = 'smape') mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` ## **The Moving Average Parameter** By default, we will try the min of your season lengths and half that for the ‘ma’ parameter. This works well in the wild but you may want to deepen this search greatly. **This is one of the best parameters to tweak if you need more accuracy out of MFLES**. Simply pass more parameters to the list, ideally these numbers are informed by the seasonality, forecast horizon, or some other bit of information. In our case, I will also pass 3 and 4 due to it being monthly data. Since this increases the number of parameters to try, it will also increase the computation time. ```python theme={null} season_length = [12] config = { 'seasonality_weights': [True, False], 'smoother': [True, False], 'ma': [3, 4, int(min(season_length)), int(min(season_length)/2),None], 'seasonal_period': [None, season_length], } mfles_model = AutoMFLES( season_length = season_length, test_size = 12, n_windows=2, metric = 'smape', config=config) # adding the config dictionary manually mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` ### **Changepoints** By default, MFLES will auto-detect if it should use changepoints. This has some accuracy benefits but massive computation expenses. You can disable changepoints and generally see close accuracy but great speed gains: ```python theme={null} season_length = [12] config = { 'changepoints': [False], 'seasonality_weights': [True, False], 'smoother': [True, False], 'ma': [int(min(season_length)), int(min(season_length)/2),None], 'seasonal_period': [None, season_length], } mfles_model = AutoMFLES( season_length = season_length, test_size = 12, n_windows=2, metric = 'smape', config=config) # adding the config dictionary manually mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` ### **Seasonality Weights** Most time series will not have a significant shift in the seasonal signal, or at least not one that is worth the extra computation needed to fit for it. To speed things up a bit, you can disable this. Although, sometimes, disabling this will cause large degradation in accuracy. ```python theme={null} season_length = [12] config = { 'seasonality_weights': [False], 'smoother': [True, False], 'ma': [int(min(season_length)), int(min(season_length)/2),None], 'seasonal_period': [None, season_length], } mfles_model = AutoMFLES( season_length = season_length, test_size = 12, n_windows=2, metric = 'smape', config=config) # adding the config dictionary manually mfles_model.fit(y=y) predicted = mfles_model.predict(12)['mean'] fitted = mfles_model.predict_in_sample()['fitted'] plt.plot(np.append(fitted, predicted), linestyle='dashed', color='red') plt.plot(y) plt.show() ``` # Multiple Seasonal Trend (MSTL) Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/multipleseasonaltrend.html > Step-by-step guide on using the `MSTL Model` with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [IMAPA Model](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of IMAPA with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The MSTL model (Multiple Seasonal-Trend decomposition using LOESS) is a method used to decompose a time series into its seasonal, trend and residual components. This approach is based on the use of LOESS (Local Regression Smoothing) to estimate the components of the time series. The MSTL decomposition is an extension of the classic seasonal-trend decomposition method (also known as Holt-Winters decomposition), which is designed to handle situations where multiple seasonal patterns exist in the data. This can occur, for example, when a time series exhibits daily, weekly, and yearly patterns simultaneously. The MSTL decomposition process is performed in several stages: 1. Trend estimation: LOESS is used to estimate the trend component of the time series. LOESS is a non-parametric smoothing method that locally fits data and allows complex trend patterns to be captured. 2. Estimation of seasonal components: Seasonal decomposition techniques are applied to identify and model the different seasonal patterns present in the data. This involves extracting and modeling seasonal components, such as daily, weekly, or yearly patterns. 3. Estimation of the residuals: The residuals are calculated as the difference between the original time series and the sum of the estimates of trend and seasonal components. Residuals represent variation not explained by trend and seasonal patterns and may contain additional information or noise. MSTL decomposition allows you to analyze and understand the different components of a time series in more detail, which can make it easier to forecast and detect patterns or anomalies. Furthermore, the use of LOESS provides flexibility to adapt to different trend and seasonal patterns present in the data. It is important to note that the MSTL model is only one of the available approaches for time series decomposition and that its choice will depend on the specific characteristics of the data and the application context. ## MSTL An important objective in time series analysis is the decomposition of a series into a set of non-observable (latent) components that can be associated with different types of temporal variations. The idea of time series decomposition is very old and was used for the calculation of planetary orbits by seventeenth century astronomers. Persons was the first to state explicitly the assumptions of unobserved components. As Persons saw it, time series was composed of four types of fluctuations: 1. a long-term tendency or secular trend; 2. cyclical movements superimposed upon the long-term trend. These cycles appear to reach their peaks during periods of industrial prosperity and their troughs during periods of depressions, their rise and fall constituting the business cycle; 3. a seasonal movement within each year, the shape of which depends on the nature of the series; 4. residual variations due to changes impacting individual variables or other major events, such as wars and national catastrophes affecting a number of variables. Traditionally, the four variations have been assumed to be mutually independent from one another and specified by means of an additive decomposition model: $$ \begin{equation} y_t= T_t +C_t +S_t +I_t, t=1,\ \cdots, n \tag 1 \end{equation} $$ where $y_t$ denotes the observed series at time $t$, $T_t$ the long-term trend, $C_t$ the business cycle, $S_t$ seasonality, and $I_t$ the irregulars. If there is dependence among the latent components, this relationship is specified through a multiplicative model $$ \begin{equation} y_t= T_t \times C_t \times S_t \times I_t, t=1,\ \cdots, n \tag 2 \end{equation} $$ where now $S_t$ and $I_t$ are expressed in proportion to the trend-cycle $T_t \times C_t$ . In some cases, mixed additive-multiplicative models are used. ### LOESS (Local Regression Smoothing) LOESS is a nonparametric smoothing method used to estimate a smooth function that locally fits the data. For each point in the time series, LOESS performs a weighted regression using nearest neighbors. The LOESS calculation involves the following steps: * For each point t in the time series, a nearest neighbor window is selected. * Weights are assigned to neighbors based on their proximity to t, using a weighting function, such as the Gaussian kernel. * A weighted regression is performed using the neighbors and their assigned weights. * The fitted value for point t is obtained based on local regression. * The process is repeated for all points in the time series, thus obtaining a smoothed estimate of the trend. ### MSTL General Properties The MSTL model (Multiple Seasonal-Trend decomposition using LOESS) has several properties that make it useful in time series analysis. Here is a list of some of its properties: 1. Decomposition of multiple seasonal components: The MSTL model is capable of handling time series that exhibit multiple seasonal patterns simultaneously. You can effectively identify and model different seasonal components present in the data. 2. Flexibility in detecting complex trends: Thanks to the use of LOESS, the MSTL model can capture complex trend patterns in the data. This includes non-linear trends and abrupt changes in the time series. 3. Adaptability to different seasonal frequencies: The MSTL model is capable of handling data with different seasonal frequencies, such as daily, weekly, monthly, or even yearly patterns. You can identify and model seasonal patterns of different cycle lengths. (see) [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) | Frecuencia | | ---------- | | Data | Minute | Hour | Day | Week | Year | | ----------- | ------ | ---- | ----- | ------ | -------- | | Daily | | | | 7 | 365.25 | | Hourly | | | 24 | 168 | 8766 | | Half-hourly | | | 48 | 336 | 17532 | | Minutes | | 60 | 1440 | 10080 | 525960 | | Seconds | 60 | 3600 | 86400 | 604800 | 31557600 | 1. Ability to smooth noise and outliers: The smoothing process used in LOESS allows to reduce the impact of noise and outliers in the time series. This can improve detection of underlying patterns and make it easier to analyze trend and seasonality. 2. Improved forecasting: By decomposing the time series into seasonal, trend, and residual components, the MSTL model can provide more accurate forecasts. Forecasts can be generated by extrapolating trend and seasonal patterns into the future, and adding the stochastic residuals. 3. More detailed interpretation and analysis: The MSTL decomposition allows you to analyze and understand the different components of the time series in a more detailed way. This facilitates the identification of seasonal patterns, changes in trend, and the evaluation of residual variability. 4. Efficient Implementation: Although the specific implementation may vary, the MSTL model can be calculated efficiently, especially when LOESS is used in combination with optimized calculation algorithms. These properties make the MSTL model a useful tool for exploratory time series analysis, data forecasting, and pattern detection in the presence of multiple seasonal components and complex trends. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/ads.csv") df.head() ``` | | Time | Ads | | - | ------------------- | ------ | | 0 | 2017-09-13T00:00:00 | 80115 | | 1 | 2017-09-13T01:00:00 | 79885 | | 2 | 2017-09-13T02:00:00 | 89325 | | 3 | 2017-09-13T03:00:00 | 101930 | | 4 | 2017-09-13T04:00:00 | 121630 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | ------ | ---------- | | 0 | 2017-09-13T00:00:00 | 80115 | 1 | | 1 | 2017-09-13T01:00:00 | 79885 | 1 | | 2 | 2017-09-13T02:00:00 | 89325 | 1 | | 3 | 2017-09-13T03:00:00 | 101930 | 1 | | 4 | 2017-09-13T04:00:00 | 121630 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots Autocorrelation (ACF) and partial autocorrelation (PACF) plots are statistical tools used to analyze time series. ACF charts show the correlation between the values of a time series and their lagged values, while PACF charts show the correlation between the values of a time series and their lagged values, after the effect of previous lagged values has been removed. ACF and PACF charts can be used to identify the structure of a time series, which can be helpful in choosing a suitable model for the time series. For example, if the ACF chart shows a repeating peak and valley pattern, this indicates that the time series is stationary, meaning that it has the same statistical properties over time. If the PACF chart shows a pattern of rapidly decreasing spikes, this indicates that the time series is invertible, meaning it can be reversed to get a stationary time series. The importance of the ACF and PACF charts is that they can help analysts better understand the structure of a time series. This understanding can be helpful in choosing a suitable model for the time series, which can improve the ability to predict future values of the time series. To analyze ACF and PACF charts: * Look for patterns in charts. Common patterns include repeating peaks and valleys, sawtooth patterns, and plateau patterns. * Compare ACF and PACF charts. The PACF chart generally has fewer spikes than the ACF chart. * Consider the length of the time series. ACF and PACF charts for longer time series will have more spikes. * Use a confidence interval. The ACF and PACF plots also show confidence intervals for the autocorrelation values. If an autocorrelation value is outside the confidence interval, it is likely to be significant. ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); # Grafico plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose from plotly.subplots import make_subplots import plotly.graph_objects as go def plotSeasonalDecompose( x, model='additive', filt=None, period=None, two_sided=True, extrapolate_trend=0, title="Seasonal Decomposition"): result = seasonal_decompose( x, model=model, filt=filt, period=period, two_sided=two_sided, extrapolate_trend=extrapolate_trend) fig = make_subplots( rows=4, cols=1, subplot_titles=["Observed", "Trend", "Seasonal", "Residuals"]) for idx, col in enumerate(['observed', 'trend', 'seasonal', 'resid']): fig.add_trace( go.Scatter(x=result.observed.index, y=getattr(result, col), mode='lines'), row=idx+1, col=1, ) return fig ``` ```python theme={null} plotSeasonalDecompose( df["y"], model="additive", period=24, title="Seasonal Decomposition") ``` ```text theme={null} Unable to display output for mime type(s): application/vnd.plotly.v1+json ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `MSTL Model`. 2. Data to test our model For the test data we will use the last 30 Hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2017-09-20 17:00:00'] test = df[df.ds>'2017-09-20 17:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((186, 3), (30, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train", linestyle="--",linewidth=2) sns.lineplot(test, x="ds", y="y", label="Test", linewidth=2, color="yellow") plt.title("Ads watched (hourly data)"); plt.xlabel("Hours") plt.show() ``` ## Implementation of MSTL Method with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import MSTL, AutoARIMA ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. First, we must define the model parameters. As mentioned before, the Candy production load presents seasonalities every 24 hours (Hourly) and every 24 \* 7 (Daily) hours. Therefore, we will use `[24, 24 * 7]` for season length. The trend component will be forecasted with an `AutoARIMA` model. (You can also try with: `AutoTheta`, `AutoCES`, and `AutoETS`) ```python theme={null} from statsforecast.utils import ConformalIntervals horizon = len(test) # number of predictions models = [MSTL(season_length=[24, 168], # seasonalities of the time series trend_forecaster=AutoARIMA(prediction_intervals=ConformalIntervals(n_windows=3, h=horizon)))] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[MSTL]) ``` Let’s see the results of our `MSTL Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` | | data | trend | seasonal24 | seasonal168 | remainder | | --- | -------- | ------------- | ------------- | ------------ | ------------ | | 0 | 80115.0 | 126222.558267 | -42511.086107 | -1524.379074 | -2072.093085 | | 1 | 79885.0 | 126191.340644 | -43585.928105 | -1315.292640 | -1405.119899 | | 2 | 89325.0 | 126160.117727 | -36756.458517 | 659.187427 | -737.846637 | | ... | ... | ... | ... | ... | ... | | 183 | 141590.0 | 120314.325647 | 25363.015190 | -2808.715638 | -1278.625199 | | 184 | 140610.0 | 120280.850692 | 26306.688690 | -6221.712712 | 244.173330 | | 185 | 139515.0 | 120247.361703 | 27571.777796 | -5745.053631 | -2559.085868 | ```python theme={null} sf.fitted_[0, 0].model_.tail(24 * 28).plot(subplots=True, grid=True) plt.tight_layout() plt.show() ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 hours ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like `ARIMA` and `Theta`) ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | MSTL | | --- | ---------- | ------------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 157848.500000 | | 1 | 1 | 2017-09-20 19:00:00 | 159790.328125 | | 2 | 1 | 2017-09-20 20:00:00 | 133002.281250 | | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 98109.875000 | | 28 | 1 | 2017-09-21 22:00:00 | 86342.015625 | | 29 | 1 | 2017-09-21 23:00:00 | 76815.976562 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | MSTL | | - | ---------- | ------------------- | -------- | ------------- | | 0 | 1 | 2017-09-13 00:00:00 | 80115.0 | 79990.851562 | | 1 | 1 | 2017-09-13 01:00:00 | 79885.0 | 79329.132812 | | 2 | 1 | 2017-09-13 02:00:00 | 89325.0 | 88401.179688 | | 3 | 1 | 2017-09-13 03:00:00 | 101930.0 | 102109.929688 | | 4 | 1 | 2017-09-13 04:00:00 | 121630.0 | 123543.671875 | ```python theme={null} StatsForecast.plot(values) ``` Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | MSTL | MSTL-lo-95 | MSTL-hi-95 | | --- | ---------- | ------------------- | ------------- | ------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 157848.500000 | 157796.406250 | 157900.593750 | | 1 | 1 | 2017-09-20 19:00:00 | 159790.328125 | 159714.218750 | 159866.437500 | | 2 | 1 | 2017-09-20 20:00:00 | 133002.281250 | 132893.937500 | 133110.609375 | | ... | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 98109.875000 | 95957.031250 | 100262.726562 | | 28 | 1 | 2017-09-21 22:00:00 | 86342.015625 | 85410.578125 | 87273.460938 | | 29 | 1 | 2017-09-21 23:00:00 | 76815.976562 | 73476.195312 | 80155.757812 | ```python theme={null} sf.plot(train, Y_hat) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 hours ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | MSTL | | --- | ---------- | ------------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 157848.500000 | | 1 | 1 | 2017-09-20 19:00:00 | 159790.328125 | | 2 | 1 | 2017-09-20 20:00:00 | 133002.281250 | | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 98109.875000 | | 28 | 1 | 2017-09-21 22:00:00 | 86342.015625 | | 29 | 1 | 2017-09-21 23:00:00 | 76815.976562 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[80,95]) forecast_df ``` | | unique\_id | ds | MSTL | MSTL-lo-95 | MSTL-lo-80 | MSTL-hi-80 | MSTL-hi-95 | | --- | ---------- | ------------------- | ------------- | ------------- | ------------- | ------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 157848.500000 | 157796.406250 | 157798.484375 | 157898.531250 | 157900.593750 | | 1 | 1 | 2017-09-20 19:00:00 | 159790.328125 | 159714.218750 | 159716.187500 | 159864.468750 | 159866.437500 | | 2 | 1 | 2017-09-20 20:00:00 | 133002.281250 | 132893.937500 | 132894.515625 | 133110.031250 | 133110.609375 | | ... | ... | ... | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 98109.875000 | 95957.031250 | 96493.921875 | 99725.828125 | 100262.726562 | | 28 | 1 | 2017-09-21 22:00:00 | 86342.015625 | 85410.578125 | 85411.835938 | 87272.195312 | 87273.460938 | | 29 | 1 | 2017-09-21 23:00:00 | 76815.976562 | 73476.195312 | 74494.546875 | 79137.406250 | 80155.757812 | ```python theme={null} sf.plot(train, forecast_df, level=[80, 95]) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second months `(step_size=50)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 500 hours ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=30, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | MSTL | | --- | ---------- | ------------------- | ------------------- | -------- | ------------- | | 0 | 1 | 2017-09-15 18:00:00 | 2017-09-15 17:00:00 | 159725.0 | 158384.250000 | | 1 | 1 | 2017-09-15 19:00:00 | 2017-09-15 17:00:00 | 161085.0 | 162015.171875 | | 2 | 1 | 2017-09-15 20:00:00 | 2017-09-15 17:00:00 | 135520.0 | 138495.093750 | | ... | ... | ... | ... | ... | ... | | 147 | 1 | 2017-09-21 21:00:00 | 2017-09-20 17:00:00 | 103080.0 | 98109.875000 | | 148 | 1 | 2017-09-21 22:00:00 | 2017-09-20 17:00:00 | 95155.0 | 86342.015625 | | 149 | 1 | 2017-09-21 23:00:00 | 2017-09-20 17:00:00 | 80285.0 | 76815.976562 | We’ll now plot the forecast for each cutoff period. To make the plots clearer, we’ll rename the actual values in each period. ```python theme={null} from IPython.display import display cross_validation=crossvalidation_df.copy() cross_validation.rename(columns = {'y' : 'actual'}, inplace = True) # rename actual values cutoff = cross_validation['cutoff'].unique() for k in range(len(cutoff)): cv = cross_validation[cross_validation['cutoff'] == cutoff[k]] display(StatsForecast.plot(df, cv.loc[:, cv.columns != 'cutoff'])) ``` ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=24), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | MSTL | | - | ---------- | ------ | ----------- | | 0 | 1 | mae | 4932.395052 | | 1 | 1 | mape | 0.040514 | | 2 | 1 | mase | 0.609407 | | 3 | 1 | rmse | 6495.207028 | | 4 | 1 | smape | 0.020267 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla MultipleSeasonalTrend API](../../src/core/models.html#mstl) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Optimized Theta Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/optimizedtheta.html > Step-by-step guide on using the `OptimizedTheta Model` with > `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Kostas I. Nikolopoulos, Dimitrios D. Thomakos. Forecasting with the Theta Method-Theory and Applications. 2019 John Wiley & Sons Ltd.](https://onlinelibrary.wiley.com/doi/book/10.1002/9781119320784) 2. [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). ## Table of Contents * [Introduction](#introduction) * [Optimized Theta Model (OTM)](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of OptimizedTheta with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The optimized Theta model is a time series forecasting method that is based on the decomposition of the time series into three components: trend, seasonality and noise. The model then forecasts the long-term trend and seasonality, and uses the noise to adjust the short-term forecasts. The optimized Theta model has been shown to be more accurate than other time series forecasting methods, especially for time series with complex trends and seasonality. The optimized Theta model was developed by Athanasios N. Antoniadis and Nikolaos D. Tsonis in 2013. The model is based on the Theta forecasting method, which was developed by George E. P. Box and Gwilym M. Jenkins in 1976\. Theta method is a time series forecasting method that is based on the decomposition of the time series into three components: trend, seasonality, and noise. The Theta model then forecasts the long-term trend and seasonality, and uses the noise to adjust the short-term forecasts. The Theta Optimized model improves on the Theta method by using an optimization algorithm to find the best parameters for the model. The optimization algorithm is based on the Akaike loss function (AIC), which is a measure of the goodness of fit of a model to the data. The optimization algorithm looks for the parameters that minimize the AIC function. The optimized Theta model has been shown to be more accurate than other time series forecasting methods, especially for time series with complex trends and seasonality. The model has been used to forecast a variety of time series, including sales, production, prices, and weather. Below are some of the benefits of the optimized Theta model: * It is more accurate than other time series forecasting methods. * It’s easy to use. * Can be used to forecast a variety of time series. * It is flexible and can be adapted to different scenarios. If you are looking for an easy-to-use and accurate time series forecasting method, the Optimized Theta model is a good choice. The optimized Theta model can be applied in a variety of areas, including: * **Sales:** The optimized Theta model can be used to forecast sales of products or services. This can help companies make decisions about production, inventory, and marketing. * **Production:** The optimized Theta model can be used to forecast the production of goods or services. This can help companies ensure they have the capacity to meet demand and avoid overproduction. * **Prices:** The optimized Theta model can be used to forecast the prices of goods or services. This can help companies make decisions about pricing and marketing strategy. * **Weather:** The optimized Theta model can be used to forecast the weather. This can help companies make decisions about agricultural production, travel planning and risk management. * **Other:** The optimized Theta model can also be used to forecast other types of time series, including traffic, energy demand, and population. The Optimized Theta model is a powerful tool that can be used to improve the accuracy of time series forecasts. It is easy to use and can be applied to a variety of areas. If you are looking for a tool to improve your time series forecasts, the Optimized Theta model is a good choice. ## Optimized Theta Model (OTM) Assume that either the time series $Y_1, \cdots Y_n$ is non-seasonal or it has been seasonally adjusted using the multiplicative classical decomposition approach. Let $X_t$ be the linear combination of two theta lines, $$ \begin{equation} X_t=\omega \text{Z}_t (\theta_1) +(1-\omega) \text{Z}_t (\theta_2) \tag 1 \end{equation} $$ where $\omega \in [0,1]$ is the weight parameter. Assuming that $\theta_1 <1$ and $\theta_2 \geq 1$, the weight $\omega$ can be derived as $$ \begin{equation} \omega:=\omega(\theta_1, \theta_2)=\frac{\theta_2 -1}{\theta_2 -\theta_1} \tag 2 \end{equation} $$ It is straightforward to see from Eqs. (1), (2) that $X_t=Y_t, \ t=1, \cdots n$ i.e., the weights are calculated properly in such a way that Eq. (1) reproduces the original series. **Theorem 1:** Let $\theta_1 <1$ and $\theta_2 \geq 1$. We will prove that 1. the linear system given by $X_t=Y_t$ for all $t=1, \cdots, n$, where $X_t$ is given by Eq.(4), has the single solution $\omega= (\theta_2 -1)/(\theta_2 - \theta_1)$ 1. the error of choosing a non-optimal weight $\omega_{\delta} =\omega + \delta$ is proportional to the error for a simple linear regression model. In Theorem 1 , we prove that the solution is unique and that the error from not choosing the optimal weights ($\omega$ and $1-\omega$) s proportional to the error of a linear regression model. As a consequence, the STheta method is given simply by setting $\theta_1=0$ and $\theta_2=2$ while from Eq. (2) we get $\omega=0.5$. Thus, Eqs. (1), (2) allow us to construct a generalisation of the Theta model that maintains the re-composition propriety of the original time series for any theta lines $\text{Z}_t (\theta_1)$ and $\text{Z}_t (\theta_2)$. In order to maintain the modelling of the long-term component and retain a fair comparison with the STheta method, in this work we fix $\theta_1=0$ and focus on the optimisation of the short-term component, $\theta_2=0$ with $\theta \geq 1$. Thus, $\theta$ is the only parameter that requires estimation so far. The theta decomposition is now given by $Y_t=(1-\frac{1}{\theta}) (\text{A}_n+\text{B}_n t)+ \frac{1}{\theta} \text{Z}_t (\theta), \ t=1, \cdots , n$ The $h$ -step-ahead forecasts calculated at origin are given by $$ \begin{equation} \hat Y_{n+h|n} = (1-\frac{1}{\theta}) [\text{A}_n+\text{B}_n (n+h)]+ \frac{1}{\theta} \tilde {\text{Z}}_{n+h|n} (\theta) \tag 3 \end{equation} $$ where $\tilde {\text{Z}}_{n+h|n} (\theta)=\tilde {\text{Z}}_{n+1|n} (\theta)=\alpha \sum_{i=0}^{n-1}(1-\alpha)^i \text{Z}_{n-i}(\theta)+(1-\alpha)^n \ell_{0}^{*}$ is the extrapolation of $\text{Z}_t(\theta)$ by an SES model with $\ell_{0}^{*} \in \mathbb{R}$ as the initial level parameter and $\alpha \in (0,1)$ as the smoothing parameter. Note that for $\theta=2$ Eq. (3) corresponds to Step 4 of the STheta algorithm. After some algebra, we can write $$ \begin{equation} \tilde {\text{Z}}_{n+1|n} (\theta)=\theta \ell{n}+(1-\theta) \{ \text{A}_n [1-(1-\alpha)^n] + \text{B}_n [n+(1-\frac{1}{\alpha}) [1-(1-\alpha)^n] ] \} \tag 4 \end{equation} $$ where $\ell_{t}=\alpha Y_t +(1-\alpha) \ell_{t-1}$ for $t=1, \cdots, n$ and $\ell_{0}=\ell_{0}^{*}/\theta$. In the light of Eqs. (3), (4), we suggest four stochastic approaches. These approaches differ due to the parameter $\theta$ which may be either fixed at two or optimised, and the coefficients $\text{A}_n$ and $\text{B}_n$, which can be either fixed or dynamic functions. To formulate the state space models, it is helpful to adopt $\mu_{t}$ as the one-step-ahead forecast at origin $t-1$ and $\varepsilon_{t}$ as the respective additive error, i.e., $\varepsilon_{t}=Y_t - \mu_{t}$ if $\mu_{t}= \hat Y_{t|t-1}$. We assume $\{ \varepsilon_{t} \}$ to be a Gaussian white noise process with mean zero and variance $\sigma^2$. ### More on Optimised Theta models Let $\text{A}_n$ and $\text{B}_n$ be fixed coefficients for all $t=1, \cdots, n$ so that Eqs. (3), (4) configure the state space model given by $$ \begin{equation} Y_t=\mu_{t}+\varepsilon_{t} \tag 5 \end{equation} $$ $$ \begin{equation} \mu_{t}=\ell_{t-1}+(1-\frac{1}{\theta}) \{(1-\alpha)^{t-1} \text{A}_n +[\frac{1-(1-\alpha)^t}{\alpha} \text{B}_n] \tag 6 \end{equation} $$ $$ \begin{equation} \ell_{t}=\alpha Y_t +(1-\alpha)\ell_{t-1} \tag 7 \end{equation} $$ with parameters $\ell_{0} \in \mathbb{R}$, $\alpha \in (0,1)$ and $\theta \in [1,\infty)$ . The parameter $\theta$ is to be estimated along with $\alpha$ and $\ell_{0}$ We call this the optimised Theta model (OTM). The $h$-step-ahead forecast at origin $n$ is given by $\hat Y_{n+h|n}=E[Y_{n+h}|Y_1,\cdots, Y_n]=\ell_{n}+(1-\frac{1}{\theta}) \{(1-\alpha)^n \text{A}_n +[(h-1) + \frac{1-(1-\alpha)^{n+1}}{\alpha}] \text{B}_n \}$ which is equivalent to Eq. (3). The conditional variance $\text{Var}[Y_{n+h}|Y_1, \cdots, Y_n]=[1+(h-1)\alpha^2]\sigma^2$ can be computed easily from the state space model. Thus, the $(1-\alpha)\%$ prediction interval for $Y_{n+h}$ is given by $\hat Y_{n+h|n} \ \pm \ q_{1-\alpha/2} \sqrt{[1+(h-1)\alpha^2 ]\sigma^2 }$ For $\theta=2$ OTM reproduces the forecasts of the STheta method; hereafter, we will refer to this particular case as the standard Theta model (STM). **Theorem 2:** The SES-d $(\ell_{0}^{**}, \alpha, b)$ model, where $\ell_{0}^{**} \in \mathbb{R}, \alpha \in (0,1)$ and $b \in \mathbb{R}$ is equivalent to $\text{OTM} (\ell_{0}, \alpha, \theta )$ where $\ell_{0} \in \mathbb{R}$ and $\theta \geq 1$, if $\ell_{0}^{**} = \ell_{0} + (1- \frac{1}{\theta} )A_n \ \ and \ \ b=(1-\frac{1}{\theta} )B_n$ In Theorem 2, we show that OTM is mathematically equivalent to the SES-d model. As a corollary of Theorem 2, STM is mathematically equivalent to SES-d with $b=\frac{1}{2} \text{B}_n$. Therefore, for $\theta=2$ the corollary also re-confirms the H\&B result on the relationship between STheta and the SES-d model. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} import pandas as pd df = pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/milk_production.csv", usecols=[1,2]) df.head() ``` | | month | production | | - | ---------- | ---------- | | 0 | 1962-01-01 | 589 | | 1 | 1962-02-01 | 561 | | 2 | 1962-03-01 | 640 | | 3 | 1962-04-01 | 656 | | 4 | 1962-05-01 | 727 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ---------- | --- | ---------- | | 0 | 1962-01-01 | 589 | 1 | | 1 | 1962-02-01 | 561 | 1 | | 2 | 1962-03-01 | 640 | 1 | | 3 | 1962-04-01 | 656 | 1 | | 4 | 1962-05-01 | 727 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ### Additive ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "additive", period=12) a.plot(); ``` ### Multiplicative ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "Multiplicative", period=12) a.plot(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Optimized Theta model`. 2. Data to test our model For the test data we will use the last 12 months to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='1974-12-01'] test = df[df.ds>'1974-12-01'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((156, 3), (12, 3)) ``` ## Implementation of OptimizedTheta with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import OptimizedTheta ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 12 # Monthly data horizon = len(test) # number of predictions models = [OptimizedTheta(season_length=season_length, decomposition_type="additive")] # multiplicative additive ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='MS') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[OptimizedTheta]) ``` Let’s see the results of our `Optimized Theta Model (OTM)`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ print(result.keys()) print(result['fit']) ``` ```text theme={null} dict_keys(['mse', 'amse', 'fit', 'residuals', 'm', 'states', 'par', 'n', 'modeltype', 'mean_y', 'decompose', 'decomposition_type', 'seas_forecast', 'fitted']) results(x=array([-83.14191626, 0.73681394, 12.45013763]), fn=10.448217519858636, nit=47, simplex=array([[-58.73988124, 0.7441127 , 11.69842922], [-49.97233449, 0.73580297, 11.41787513], [-83.14191626, 0.73681394, 12.45013763], [-77.04867427, 0.73498431, 11.99254037]])) ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("residuals"), columns=["residual Model"]) residual ``` | | residual Model | | --- | -------------- | | 0 | -271.899414 | | 1 | -114.671692 | | 2 | 4.768066 | | ... | ... | | 153 | -60.233887 | | 154 | -92.472839 | | 155 | -44.143982 | ```python theme={null} import scipy.stats as stats fig, axs = plt.subplots(nrows=2, ncols=2) residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like `ARIMA` and `Theta`) ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | OptimizedTheta | | --- | ---------- | ---------- | -------------- | | 0 | 1 | 1975-01-01 | 839.682800 | | 1 | 1 | 1975-02-01 | 802.071838 | | 2 | 1 | 1975-03-01 | 896.117126 | | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 824.135498 | | 10 | 1 | 1975-11-01 | 795.691223 | | 11 | 1 | 1975-12-01 | 833.316345 | Let’s visualize the fitted values ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | OptimizedTheta | | - | ---------- | ---------- | ----- | -------------- | | 0 | 1 | 1962-01-01 | 589.0 | 860.899414 | | 1 | 1 | 1962-02-01 | 561.0 | 675.671692 | | 2 | 1 | 1962-03-01 | 640.0 | 635.231934 | | 3 | 1 | 1962-04-01 | 656.0 | 614.731323 | | 4 | 1 | 1962-05-01 | 727.0 | 609.770752 | ```python theme={null} StatsForecast.plot(values) ``` Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | OptimizedTheta | OptimizedTheta-lo-95 | OptimizedTheta-hi-95 | | --- | ---------- | ---------- | -------------- | -------------------- | -------------------- | | 0 | 1 | 1975-01-01 | 839.682800 | 742.509583 | 955.414307 | | 1 | 1 | 1975-02-01 | 802.071838 | 643.581360 | 945.119202 | | 2 | 1 | 1975-03-01 | 896.117126 | 710.785095 | 1065.057495 | | ... | ... | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 824.135498 | 555.948669 | 1084.320190 | | 10 | 1 | 1975-11-01 | 795.691223 | 503.147858 | 1036.519531 | | 11 | 1 | 1975-12-01 | 833.316345 | 530.259705 | 1106.636597 | ```python theme={null} sf.plot(train, Y_hat) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | OptimizedTheta | | --- | ---------- | ---------- | -------------- | | 0 | 1 | 1975-01-01 | 839.682800 | | 1 | 1 | 1975-02-01 | 802.071838 | | 2 | 1 | 1975-03-01 | 896.117126 | | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 824.135498 | | 10 | 1 | 1975-11-01 | 795.691223 | | 11 | 1 | 1975-12-01 | 833.316345 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[80,95]) forecast_df ``` | | unique\_id | ds | OptimizedTheta | OptimizedTheta-lo-80 | OptimizedTheta-hi-80 | OptimizedTheta-lo-95 | OptimizedTheta-hi-95 | | --- | ---------- | ---------- | -------------- | -------------------- | -------------------- | -------------------- | -------------------- | | 0 | 1 | 1975-01-01 | 839.682800 | 766.665955 | 928.326172 | 742.509583 | 955.414307 | | 1 | 1 | 1975-02-01 | 802.071838 | 704.290039 | 899.335815 | 643.581360 | 945.119202 | | 2 | 1 | 1975-03-01 | 896.117126 | 761.334778 | 1007.408447 | 710.785095 | 1065.057495 | | ... | ... | ... | ... | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 824.135498 | 623.903992 | 996.567200 | 555.948669 | 1084.320190 | | 10 | 1 | 1975-11-01 | 795.691223 | 576.546570 | 975.490784 | 503.147858 | 1036.519531 | | 11 | 1 | 1975-12-01 | 833.316345 | 606.713623 | 1033.885742 | 530.259705 | 1106.636597 | ```python theme={null} sf.plot(train, test.merge(forecast_df), level=[80, 95]) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=5)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=train, h=horizon, step_size=12, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` index. If you dont like working with index just run crossvalidation\_df.resetindex() * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | OptimizedTheta | | --- | ---------- | ---------- | ---------- | ----- | -------------- | | 0 | 1 | 1972-01-01 | 1971-12-01 | 826.0 | 828.836365 | | 1 | 1 | 1972-02-01 | 1971-12-01 | 799.0 | 792.592346 | | 2 | 1 | 1972-03-01 | 1971-12-01 | 890.0 | 883.269592 | | ... | ... | ... | ... | ... | ... | | 33 | 1 | 1974-10-01 | 1973-12-01 | 812.0 | 812.183838 | | 34 | 1 | 1974-11-01 | 1973-12-01 | 773.0 | 783.898376 | | 35 | 1 | 1974-12-01 | 1973-12-01 | 813.0 | 821.124329 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | OptimizedTheta | | - | ---------- | ------ | -------------- | | 0 | 1 | mae | 6.740204 | | 1 | 1 | mape | 0.007828 | | 2 | 1 | mase | 0.303120 | | 3 | 1 | rmse | 8.701501 | | 4 | 1 | smape | 0.003893 | ## References 1. [Kostas I. Nikolopoulos, Dimitrios D. Thomakos. Forecasting with the Theta Method-Theory and Applications. 2019 John Wiley & Sons Ltd.](https://onlinelibrary.wiley.com/doi/book/10.1002/9781119320784) 2. [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). 3. [Nixtla OptimizedTheta API](../../src/core/models.html#optimizedtheta) 4. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 5. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 6. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Seasonal Exponential Smoothing Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/seasonalexponentialsmoothing.html > Step-by-step guide on using the `SeasonalExponentialSmoothing Model` > with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Seasonal Exponential Smoothing](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of SeasonalExponentialSmoothing with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction Simple Exponential Smoothing (SES) is a forecasting method that uses a weighted average of historical values to predict the next value. The weight is assigned to the most recent values, and the oldest values receive a lower weight. This is because SES assumes that more recent values are more relevant to predicting the future than older values. SES is implemented by a simple formula: $\hat{y}_{T+1|T} = \alpha y_T + \alpha(1-\alpha) y_{T-1} + \alpha(1-\alpha)^2 y_{T-2}+ \cdots,$ The smoothing factor controls the amount of weight that is assigned to the most recent values. A higher α value means more weight will be assigned to newer values, while a lower α value means more weight will be assigned to older values. Seasonality in time series refers to the regular, repeating pattern of variation in a time series over a specified period of time. Seasonality can be a challenge to deal with in time series analysis, as it can obscure the underlying trend in the data. Seasonality is an important factor to consider when analyzing time series data. By understanding the seasonal patterns in the data, it is possible to make more accurate forecasts and better decisions. ## Seasonal Exponential Smoothing Model The simplest of the exponentially smoothing methods is naturally called simple exponential smoothing (SES). This method is suitable for forecasting data with no clear trend or seasonal pattern. Using the naïve method, all forecasts for the future are equal to the last observed value of the series, $\hat{y}_{T+h|T} = y_{T},$ for $h=1,2,\dots$. Hence, the naïve method assumes that the most recent observation is the only important one, and all previous observations provide no information for the future. This can be thought of as a weighted average where all of the weight is given to the last observation. Using the average method, all future forecasts are equal to a simple average of the observed data, $\hat{y}_{T+h|T} = \frac1T \sum_{t=1}^T y_t,$ for $h=1,2,\dots$ Hence, the average method assumes that all observations are of equal importance, and gives them equal weights when generating forecasts. We often want something between these two extremes. For example, it may be sensible to attach larger weights to more recent observations than to observations from the distant past. This is exactly the concept behind simple exponential smoothing. Forecasts are calculated using weighted averages, where the weights decrease exponentially as observations come from further in the past — the smallest weights are associated with the oldest observations: where $0 \le \alpha \le 1$ is the smoothing parameter. The one-step-ahead forecast for time $T+1$ is a weighted average of all of the observations in the series $y_1,\dots,y_T$. The rate at which the weights decrease is controlled by the parameter $\alpha$. For any $\alpha$ between 0 and 1, the weights attached to the observations decrease exponentially as we go back in time, hence the name “exponential smoothing”. If $\alpha$ is small (i.e., close to 0), more weight is given to observations from the more distant past. If $\alpha$ is large (i.e., close to 1), more weight is given to the more recent observations. For the extreme case where $\alpha=1$, $\hat{y}_{T+1|T}=y_T$ and the forecasts are equal to the naïve forecasts. ### How do you know the value of the seasonal parameters? To determine the value of the seasonal parameter s in the Seasonally Adjusted `Simple Exponential Smoothing (SES Seasonally Adjusted) model`, different methods can be used, depending on the nature of the data and the objective of the analysis. Here are some common methods to determine the value of the seasonal parameter $s$: 1. **Visual Analysis:** A visual analysis of the time series data can be performed to identify any seasonal patterns. If a clear seasonal pattern is observed in the data, the length of the seasonal period can be used as the value of $s$. 2. **Statistical methods:** Statistical techniques, such as autocorrelation, can be used to identify seasonal patterns in the data. The value of $s$ can be the number of periods in which a significant peak in the autocorrelation function is observed. 3. **Frequency Analysis:** A frequency analysis of the data can be performed to identify seasonal patterns. The value of $s$ can be the number of periods in which a significant peak in the frequency spectrum is observed. [see](https://robjhyndman.com/hyndsight/seasonal-periods/) 4. **Trial and error:** You can try different values of $s$ and select the value that results in the best fit of the model to the data. It is important to note that the choice of the value of $s$ can significantly affect the `accuracy` of the seasonally adjusted SES model predictions. Therefore, it is recommended to test different values of $s$ and evaluate the performance of the model using appropriate evaluation measures before selecting the final value of $s$. ### How can we validate the simple exponential smoothing model with seasonal adjustment? To validate the Seasonally Adjusted Simple Exponential Smoothing (SES Seasonally Adjusted) model, different theorems and evaluation measures can be used, depending on the objective of the analysis and the nature of the data. Here are some common theorems used to validate the seasonally adjusted SES model: 1. Gauss-Markov Theorem: This theorem states that, if certain conditions are met, the least squares estimator is the best linear unbiased estimator. In the case of the seasonally adjusted SES, the model parameters are estimated using least squares, so the Gauss-Markov theorem can be used to assess the quality of model fit. 2. Unit Root Theorem: This theorem is used to determine if a time series is stationary or not. If a time series is non-stationary, the seasonally adjusted SES model is not appropriate, since it assumes that the time series is stationary. Therefore, the unit root theorem is used to assess the stationarity of the time series and determine whether the seasonally adjusted SES model is appropriate. 3. Ljung-Box Theorem: This theorem is used to assess the goodness of fit of the model and to determine if the model residuals are white noise. If the residuals are white noise, the model fits the data well and the model predictions are accurate. The Ljung-Box theorem is used to test whether the model residuals are independent and uncorrelated. In addition to these theorems, various evaluation measures, such as root mean square error (MSE), mean absolute error (MAE), and coefficient of determination (R²), can be used to evaluate the performance of the seasonally adjusted SES model and compare it with other forecast models. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/ads.csv") df.head() ``` | | Time | Ads | | - | ------------------- | ------ | | 0 | 2017-09-13T00:00:00 | 80115 | | 1 | 2017-09-13T01:00:00 | 79885 | | 2 | 2017-09-13T02:00:00 | 89325 | | 3 | 2017-09-13T03:00:00 | 101930 | | 4 | 2017-09-13T04:00:00 | 121630 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | ------ | ---------- | | 0 | 2017-09-13T00:00:00 | 80115 | 1 | | 1 | 2017-09-13T01:00:00 | 79885 | 1 | | 2 | 2017-09-13T02:00:00 | 89325 | 1 | | 3 | 2017-09-13T03:00:00 | 101930 | 1 | | 4 | 2017-09-13T04:00:00 | 121630 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### The Augmented Dickey-Fuller Test An Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis. Augmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis. Null Hypothesis: Time Series is non-stationary. It gives a time-dependent trend. Alternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time. ADF or t Statistic \< critical values: Reject the null hypothesis, time series is stationary. ADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary. ```python theme={null} from statsmodels.tsa.stattools import adfuller def Augmented_Dickey_Fuller_Test_func(series , column_name): print (f'Dickey-Fuller test results for columns: {column_name}') dftest = adfuller(series, autolag='AIC') dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used']) for key,value in dftest[4].items(): dfoutput['Critical Value (%s)'%key] = value print (dfoutput) if dftest[1] <= 0.05: print("Conclusion:====>") print("Reject the null hypothesis") print("The data is stationary") else: print("Conclusion:====>") print("The null hypothesis cannot be rejected") print("The data is not stationary") ``` ```python theme={null} Augmented_Dickey_Fuller_Test_func(df["y"],'Ads') ``` ```text theme={null} Dickey-Fuller test results for columns: Ads Test Statistic -7.089634e+00 p-value 4.444804e-10 No Lags Used 9.000000e+00 ... Critical Value (1%) -3.462499e+00 Critical Value (5%) -2.875675e+00 Critical Value (10%) -2.574304e+00 Length: 7, dtype: float64 Conclusion:====> Reject the null hypothesis The data is stationary ``` ### Autocorrelation plots The important characteristics of Autocorrelation (ACF) and Partial Autocorrelation (PACF) are as follows: Autocorrelation (ACF): 1. Identify patterns of temporal dependence: The ACF shows the correlation between an observation and its lagged values at different time intervals. Helps identify patterns of temporal dependency in a time series, such as the presence of trends or seasonality. 1. Indicates the “memory” of the series: The ACF allows us to determine how much past observations influence future ones. If the ACF shows significant autocorrelations in several lags, it indicates that the series has a long-term memory and that past observations are relevant to predict future ones. 2. Helps identify MA (moving average) models: The shape of the ACF can reveal the presence of moving average components in the time series. Lags where the ACF shows a significant correlation may indicate the order of an MA model. Partial Autocorrelation (PACF): 1. Identify direct dependence: Unlike the ACF, the PACF eliminates the indirect effects of intermediate lags and measures the direct correlation between an observation and its lagged values. It helps to identify the direct dependence between an observation and its lag values, without the influence of intermediate lags. 1. Helps to identify AR (autoregressive) models: The shape of the PACF can reveal the presence of autoregressive components in the time series. Lags in which the PACF shows a significant correlation may indicate the order of an AR model. 2. Used in conjunction with the ACF: The PACF is used in conjunction with the ACF to determine the order of an AR or MA model. By analyzing both the ACF and the PACF, significant lags can be identified and a model suitable for time series analysis and forecasting can be built. In summary, the ACF and the PACF are complementary tools in time series analysis that provide information on time dependence and help identify the appropriate components to build forecast models. ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ### Additive ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "additive", period=12) a.plot(); ``` ### Multiplicative ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "Multiplicative", period=12) a.plot(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Seasonal Exponential Smoothing Model`. 2. Data to test our model For the test data we will use the last 30 hourly to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2017-09-20 17:00:00'] test = df[df.ds>'2017-09-20 17:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((186, 3), (30, 3)) ``` Now let’s plot the training data and the test data. ```python theme={null} sns.lineplot(train,x="ds", y="y", label="Train", linestyle="--") sns.lineplot(test, x="ds", y="y", label="Test") plt.title("Ads watched (hourly data)"); plt.show() ``` ## Implementation of SeasonalExponentialSmoothing with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import SeasonalExponentialSmoothing ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 24 # Hourly data horizon = len(test) # number of predictions models = [SeasonalExponentialSmoothing(alpha=0.8, season_length=season_length)] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[SeasonalES]) ``` Let’s see the results of our `Seasonal Exponential Smoothing Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'mean': array([161567.6 , 163186.56 , 134410.94 , 106145.6 , 93383.164, 79489.72 , 79769. , 77651.984, 85288.33 , 99665.31 , 123067.336, 115759.51 , 103556.234, 100510.09 , 97411.65 , 107672.88 , 121150.84 , 140041.22 , 140075.19 , 140903.34 , 142615.28 , 142360.75 , 142615.6 , 142658.62 ], dtype=float32), 'fitted': array([ nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, 163840. , 166235. , 139520. , 105895. , 96780. , 82520. , 80125. , 75335. , 85105. , 102080. , 125135. , 118030. , 109225. , 102475. , 102240. , 115840. , 130540. , 144325. , 148970. , 149150. , 148040. , 148810. , 149830. , 150570. , 152320. , 153663. , 131208. , 104231. , 93096. , 82716. , 77077. , 75171. , 83133. , 91452. , 119771. , 115758. , 110597. , 99583. , 103796. , 110100. , 127420. , 141213. , 151770. , 146850. , 148024. , 147950. , 146566. , 149542. , 158244. , 159600.6 , 134657.6 , 111202.2 , 98779.2 , 86635.2 , 85683.4 , 86110.2 , 90506.6 , 101862.4 , 116706.2 , 126311.6 , 135227.4 , 135468.6 , 135359.2 , 128572. , 130476. , 142226.6 , 156254. , 151370. , 152592.8 , 150546. , 149829.2 , 147856.4 , 153668.8 , 149420.12 , 127127.52 , 116580.44 , 97115.84 , 92215.04 , 88384.68 , 88698.04 , 90561.32 , 99004.48 , 113397.24 , 128838.32 , 140169.48 , 149141.72 , 149135.84 , 138650.4 , 144135.2 , 157333.31 , 164318.8 , 163698. , 161030.56 , 155953.2 , 157209.84 , 157587.28 , 165409.77 , 165804.03 , 139593.5 , 113680.086, 97299.17 , 83783.01 , 81284.94 , 80419.61 , 88548.266, 99632.9 , 121703.445, 114827.664, 107585.9 , 107952.34 , 107951.17 , 109782.08 , 124771.04 , 140070.66 , 144959.77 , 146123.6 , 145982.11 , 147478.64 , 147709.97 , 151845.45 , 162297.95 , 155892.81 , 135694.7 , 108388.016, 95495.836, 80368.6 , 78924.984, 75819.92 , 83301.66 , 98286.58 , 119816.69 , 113457.53 , 100621.18 , 96790.47 , 96518.234, 105304.414, 120754.21 , 136806.12 , 146155.95 , 140556.72 , 146976.42 , 145443.73 , 150638. , 155233.1 ], dtype=float32)} ``` Let us now visualize the fitted values of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} fitted=pd.DataFrame(result.get("fitted"), columns=["fitted"]) fitted["ds"]=df["ds"] fitted ``` | | fitted | ds | | --- | ------------- | ------------------- | | 0 | NaN | 2017-09-13 00:00:00 | | 1 | NaN | 2017-09-13 01:00:00 | | 2 | NaN | 2017-09-13 02:00:00 | | ... | ... | ... | | 183 | 145443.734375 | 2017-09-20 15:00:00 | | 184 | 150638.000000 | 2017-09-20 16:00:00 | | 185 | 155233.093750 | 2017-09-20 17:00:00 | ```python theme={null} sns.lineplot(df, x="ds", y="y", label="Actual", linewidth=2) sns.lineplot(fitted,x="ds", y="fitted", label="Fitted", linestyle="--" ) plt.title("Ads watched (hourly data)"); plt.show() ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | SeasonalES | | --- | ---------- | ------------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 161567.593750 | | 1 | 1 | 2017-09-20 19:00:00 | 163186.562500 | | 2 | 1 | 2017-09-20 20:00:00 | 134410.937500 | | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 106145.601562 | | 28 | 1 | 2017-09-21 22:00:00 | 93383.164062 | | 29 | 1 | 2017-09-21 23:00:00 | 79489.718750 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | SeasonalES | | - | ---------- | ------------------- | -------- | ---------- | | 0 | 1 | 2017-09-13 00:00:00 | 80115.0 | NaN | | 1 | 1 | 2017-09-13 01:00:00 | 79885.0 | NaN | | 2 | 1 | 2017-09-13 02:00:00 | 89325.0 | NaN | | 3 | 1 | 2017-09-13 03:00:00 | 101930.0 | NaN | | 4 | 1 | 2017-09-13 04:00:00 | 121630.0 | NaN | ```python theme={null} sf.plot(train, Y_hat) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 hourly ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecast_df = sf.predict(h=horizon) forecast_df ``` | | unique\_id | ds | SeasonalES | | --- | ---------- | ------------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 161567.593750 | | 1 | 1 | 2017-09-20 19:00:00 | 163186.562500 | | 2 | 1 | 2017-09-20 20:00:00 | 134410.937500 | | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 106145.601562 | | 28 | 1 | 2017-09-21 22:00:00 | 93383.164062 | | 29 | 1 | 2017-09-21 23:00:00 | 79489.718750 | ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=5)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 30 hourly ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=12, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier. * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | SeasonalES | | --- | ---------- | ------------------- | ------------------- | -------- | ------------- | | 0 | 1 | 2017-09-19 18:00:00 | 2017-09-19 17:00:00 | 161385.0 | 162297.953125 | | 1 | 1 | 2017-09-19 19:00:00 | 2017-09-19 17:00:00 | 165010.0 | 155892.812500 | | 2 | 1 | 2017-09-19 20:00:00 | 2017-09-19 17:00:00 | 134090.0 | 135694.703125 | | ... | ... | ... | ... | ... | ... | | 87 | 1 | 2017-09-21 21:00:00 | 2017-09-20 17:00:00 | 103080.0 | 106145.601562 | | 88 | 1 | 2017-09-21 22:00:00 | 2017-09-20 17:00:00 | 95155.0 | 93383.164062 | | 89 | 1 | 2017-09-21 23:00:00 | 2017-09-20 17:00:00 | 80285.0 | 79489.718750 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | SeasonalES | | - | ---------- | ------ | ----------- | | 0 | 1 | mae | 5728.207812 | | 1 | 1 | mape | 0.049386 | | 2 | 1 | mase | 0.707731 | | 3 | 1 | rmse | 7290.840738 | | 4 | 1 | smape | 0.024009 | ## Acknowledgements We would like to thank [Naren Castellon](https://www.linkedin.com/in/naren-castellon-1541b8101/?originalSubdomain=pa) for writing this tutorial. ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla SeasonalExponentialSmoothing API](../../src/core/models.html#seasonalexponentialsmoothing) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Seasonal Exponential Smoothing Optimized Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/seasonalexponentialsmoothingoptimized.html > Step-by-step guide on using the > `SeasonalExponentialSmoothingOptimized Model` with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Seasonal Exponential Smoothing Optimized Model](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of SeasonalExponentialSmoothingOptimized with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The Seasonal Exponential Smoothing Optimized (SESO) model is a forecasting technique used to predict future values of a time series that exhibits seasonal patterns. It is a variant of the exponential smoothing method, which uses a combination of past and predicted values to generate a prediction. The SESO algorithm uses an optimization approach to find the optimal values of the seasonal exponential smoothing parameters. These parameters include the smoothing coefficients for the levels, trends, and seasonal components of the time series. The SESO model is particularly useful for forecasting time series with pronounced seasonal patterns, such as seasonal product sales or seasonal temperatures, and many other areas. By using SESO, accurate and useful forecasts can be generated for business planning and decision making. ## Seasonal Exponential Smoothing Model The SESO model is based on the exponential smoothing method, which uses a combination of past and predicted values to generate a prediction. The mathematical formula for the SESO model is as follows: $\hat{y}{t+1,s} = \alpha y_t + (1-\alpha) \hat{y}{t-1,s}$ Where: - $\hat{y}{t+1,s}$ is the forecast for the next period of the season $s$. - $\alpha$ is the smoothing parameter that is optimized by minimizing the squared error. - $y_t$ is the current observation of station $s$ in period $t$. - $\hat{y}{t-1,s}$ is the forecast for the previous period of the season $s$. The equation indicates that the forecast value for the next season period $s$ is calculated as a weighted combination of the current observation and the previous forecast for the same station. The smoothing parameter $\alpha$ controls the relative influence of these two terms on the final prediction. A high value of α gives more weight to the current observation and less weight to the previous forecast, making the model more sensitive to recent changes in the time series. A low value of $\alpha$, on the other hand, gives more weight to the previous forecast and less weight to the current observation, making the model more stable and smooth. The optimal value of the smoothing parameter $\alpha$ is determined by minimizing the squared error between the forecasts generated by the model and the actual values of the time series. ### Model selection Model selection in the context of the SESO model refers to the process of choosing the optimal values of the smoothing parameters and the seasonal component for the model. The optimal values of these parameters are the ones that result in the best forecast performance for the given data set. A great advantage of the ETS statistical framework is that information criteria can be used for model selection. The $AIC, AIC_c$ and $BIC$, that also can be used here to determine which of the ETS models is most appropriate for a given time series. For ETS models, Akaike’s Information Criterion (AIC) is defined as $\text{AIC} = -2\log(L) + 2k,$ where $L$ is the likelihood of the model and $k$ is the total number of parameters and initial states that have been estimated (including the residual variance). The AIC corrected for small sample bias ($AIC_c$) is defined as $\text{AIC}_{\text{c}} = \text{AIC} + \frac{2k(k+1)}{T-k-1},$ and the Bayesian Information Criterion (BIC) is $\text{BIC} = \text{AIC} + k[\log(T)-2].$ These criteria balance the goodness of fit with the complexity of the model and provide a way to choose the model that maximizes the likelihood of the data while minimizing the number of parameters. In addition to these techniques, expert judgment and domain knowledge can also be used to select the optimal SESO model. This involves considering the underlying dynamics of the time series, the patterns of seasonality, and any other relevant factors that may influence the choice of the model. Overall, the process of model selection for the SESO model involves a combination of statistical techniques, information criteria, and expert judgment to identify the optimal values of the smoothing parameters and the seasonal component that result in the best forecast performance for the given data set. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/ads.csv") df.head() ``` | | Time | Ads | | - | ------------------- | ------ | | 0 | 2017-09-13T00:00:00 | 80115 | | 1 | 2017-09-13T01:00:00 | 79885 | | 2 | 2017-09-13T02:00:00 | 89325 | | 3 | 2017-09-13T03:00:00 | 101930 | | 4 | 2017-09-13T04:00:00 | 121630 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | ------ | ---------- | | 0 | 2017-09-13T00:00:00 | 80115 | 1 | | 1 | 2017-09-13T01:00:00 | 79885 | 1 | | 2 | 2017-09-13T02:00:00 | 89325 | 1 | | 3 | 2017-09-13T03:00:00 | 101930 | 1 | | 4 | 2017-09-13T04:00:00 | 121630 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### The Augmented Dickey-Fuller Test An Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis. Augmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis. Null Hypothesis: Time Series is non-stationary. It gives a time-dependent trend. Alternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time. ADF or t Statistic \< critical values: Reject the null hypothesis, time series is stationary. ADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary. ```python theme={null} from statsmodels.tsa.stattools import adfuller def Augmented_Dickey_Fuller_Test_func(series , column_name): print (f'Dickey-Fuller test results for columns: {column_name}') dftest = adfuller(series, autolag='AIC') dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used']) for key,value in dftest[4].items(): dfoutput['Critical Value (%s)'%key] = value print (dfoutput) if dftest[1] <= 0.05: print("Conclusion:====>") print("Reject the null hypothesis") print("The data is stationary") else: print("Conclusion:====>") print("The null hypothesis cannot be rejected") print("The data is not stationary") ``` ```python theme={null} Augmented_Dickey_Fuller_Test_func(df["y"],'Ads') ``` ```text theme={null} Dickey-Fuller test results for columns: Ads Test Statistic -7.089634e+00 p-value 4.444804e-10 No Lags Used 9.000000e+00 ... Critical Value (1%) -3.462499e+00 Critical Value (5%) -2.875675e+00 Critical Value (10%) -2.574304e+00 Length: 7, dtype: float64 Conclusion:====> Reject the null hypothesis The data is stationary ``` ### Autocorrelation plots The important characteristics of Autocorrelation (ACF) and Partial Autocorrelation (PACF) are as follows: Autocorrelation (ACF): 1. Identify patterns of temporal dependence: The ACF shows the correlation between an observation and its lagged values at different time intervals. Helps identify patterns of temporal dependency in a time series, such as the presence of trends or seasonality. 1. Indicates the “memory” of the series: The ACF allows us to determine how much past observations influence future ones. If the ACF shows significant autocorrelations in several lags, it indicates that the series has a long-term memory and that past observations are relevant to predict future ones. 2. Helps identify MA (moving average) models: The shape of the ACF can reveal the presence of moving average components in the time series. Lags where the ACF shows a significant correlation may indicate the order of an MA model. Partial Autocorrelation (PACF): 1. Identify direct dependence: Unlike the ACF, the PACF eliminates the indirect effects of intermediate lags and measures the direct correlation between an observation and its lagged values. It helps to identify the direct dependence between an observation and its lag values, without the influence of intermediate lags. 1. Helps to identify AR (autoregressive) models: The shape of the PACF can reveal the presence of autoregressive components in the time series. Lags in which the PACF shows a significant correlation may indicate the order of an AR model. 2. Used in conjunction with the ACF: The PACF is used in conjunction with the ACF to determine the order of an AR or MA model. By analyzing both the ACF and the PACF, significant lags can be identified and a model suitable for time series analysis and forecasting can be built. In summary, the ACF and the PACF are complementary tools in time series analysis that provide information on time dependence and help identify the appropriate components to build forecast models. ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); # Grafico plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') #plt.savefig("Gráfico de Densidad y qq") plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ### Additive ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "additive", period=12) a.plot(); ``` ### Multiplicative ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "Multiplicative", period=12) a.plot(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Seasonal Exponential Smoothing Optimized Model`. 2. Data to test our model For the test data we will use the last 30 hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2017-09-20 17:00:00'] test = df[df.ds>'2017-09-20 17:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((186, 3), (30, 3)) ``` ## Implementation of SeasonalExponentialSmoothingOptimized with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import SeasonalExponentialSmoothingOptimized ``` ### Building Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 24 # Hourly data horizon = len(test) # number of predictions models = [SeasonalExponentialSmoothingOptimized(season_length=season_length)] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[SeasESOpt]) ``` Let’s see the results of our `Seasonal Exponential Smoothing Optimized Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'mean': array([161532.05 , 161051.69 , 135531.64 , 105600.39 , 96717.39 , 82608.34 , 80224.33 , 78075.98 , 85233.23 , 100179.336, 122245.62 , 118087.57 , 109614.81 , 104729.91 , 104895.02 , 115862.96 , 130370.98 , 144231.89 , 149036.73 , 149072.73 , 148110.77 , 148760.73 , 149767.53 , 150561.8 ], dtype=float32), 'fitted': array([ nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, 163840. , 166235. , 139520. , 105895. , 96780. , 82520. , 80125. , 75335. , 85105. , 102080. , 125135. , 118030. , 109225. , 102475. , 102240. , 115840. , 130540. , 144325. , 148970. , 149150. , 148040. , 148810. , 149830. , 150570. , 162030.27 , 163222.1 , 137347.33 , 103835.8 , 96733.95 , 82522.45 , 80086.9 , 75132.05 , 85074.36 , 100452.66 , 121044.03 , 118001.6 , 109242.15 , 102349.03 , 102321.49 , 115768.25 , 130501. , 144286.1 , 149005. , 149121.25 , 148039.8 , 148799.25 , 149789.2 , 150557.16 , 161740.55 , 162812.36 , 136965.22 , 112853.91 , 96768.61 , 82573.375, 80164.38 , 88707.87 , 85164.8 , 100944.15 , 117929.875, 118111.086, 109563.58 , 103815.7 , 104036.375, 115942.47 , 130508.39 , 144268.03 , 149088.7 , 149155.03 , 148096.75 , 148823.2 , 149797.77 , 150525.92 , 160582.38 , 159756.83 , 134514.39 , 117874.29 , 96767.92 , 82683.74 , 80253.336, 89338.625, 85232.055, 100619.03 , 114659.62 , 118224.67 , 109881.99 , 105514.21 , 106070.33 , 116194.74 , 130678.805, 144436.45 , 149261.16 , 149331.28 , 148247.19 , 148908.03 , 149890.33 , 150620.88 , 161557.95 , 161701.48 , 136228.19 , 113004.195, 96773.695, 82673.66 , 80245.91 , 78459.88 , 85267.016, 100517.48 , 120224.3 , 118155.68 , 109777.57 , 105240.35 , 105717.734, 116058.445, 130571.32 , 144349.64 , 149169.75 , 149255.28 , 148186.9 , 148872.55 , 149844.78 , 150618.77 , 161553.17 , 160112.8 , 135912.81 , 107124.39 , 96756.41 , 82642.07 , 80226.8 , 74707.9 , 85226.28 , 100202.98 , 119687.805, 118105.27 , 109668.59 , 104848.68 , 105212.516, 115939.71 , 130463.1 , 144266.05 , 149142.61 , 149154.38 , 148177.3 , 148833.17 , 149860.03 , 150673.38 ], dtype=float32)} ``` Let us now visualize the fitted values of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} fitted=pd.DataFrame(result.get("fitted"), columns=["fitted"]) fitted["ds"]=df["ds"] fitted ``` | | fitted | ds | | --- | ------------- | ------------------- | | 0 | NaN | 2017-09-13 00:00:00 | | 1 | NaN | 2017-09-13 01:00:00 | | 2 | NaN | 2017-09-13 02:00:00 | | ... | ... | ... | | 183 | 148833.171875 | 2017-09-20 15:00:00 | | 184 | 149860.031250 | 2017-09-20 16:00:00 | | 185 | 150673.375000 | 2017-09-20 17:00:00 | ```python theme={null} sns.lineplot(df, x="ds", y="y", label="Actual", linewidth=2) sns.lineplot(fitted,x="ds", y="fitted", label="Fitted", linestyle="--" ) plt.title("Ads watched (hourly data)"); plt.show() ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} # Prediction Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | SeasESOpt | | --- | ---------- | ------------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 161532.046875 | | 1 | 1 | 2017-09-20 19:00:00 | 161051.687500 | | 2 | 1 | 2017-09-20 20:00:00 | 135531.640625 | | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 105600.390625 | | 28 | 1 | 2017-09-21 22:00:00 | 96717.390625 | | 29 | 1 | 2017-09-21 23:00:00 | 82608.343750 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | SeasESOpt | | - | ---------- | ------------------- | -------- | --------- | | 0 | 1 | 2017-09-13 00:00:00 | 80115.0 | NaN | | 1 | 1 | 2017-09-13 01:00:00 | 79885.0 | NaN | | 2 | 1 | 2017-09-13 02:00:00 | 89325.0 | NaN | | 3 | 1 | 2017-09-13 03:00:00 | 101930.0 | NaN | | 4 | 1 | 2017-09-13 04:00:00 | 121630.0 | NaN | ```python theme={null} sf.plot(train, Y_hat) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecast_df = sf.predict(h=horizon) forecast_df ``` | | unique\_id | ds | SeasESOpt | | --- | ---------- | ------------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 161532.046875 | | 1 | 1 | 2017-09-20 19:00:00 | 161051.687500 | | 2 | 1 | 2017-09-20 20:00:00 | 135531.640625 | | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 105600.390625 | | 28 | 1 | 2017-09-21 22:00:00 | 96717.390625 | | 29 | 1 | 2017-09-21 23:00:00 | 82608.343750 | ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=30, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier. * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | SeasESOpt | | --- | ---------- | ------------------- | ------------------- | -------- | ------------- | | 0 | 1 | 2017-09-18 06:00:00 | 2017-09-18 05:00:00 | 99440.0 | 141401.750000 | | 1 | 1 | 2017-09-18 07:00:00 | 2017-09-18 05:00:00 | 97655.0 | 152474.250000 | | 2 | 1 | 2017-09-18 08:00:00 | 2017-09-18 05:00:00 | 97655.0 | 152482.796875 | | ... | ... | ... | ... | ... | ... | | 87 | 1 | 2017-09-21 21:00:00 | 2017-09-20 17:00:00 | 103080.0 | 105600.390625 | | 88 | 1 | 2017-09-21 22:00:00 | 2017-09-20 17:00:00 | 95155.0 | 96717.390625 | | 89 | 1 | 2017-09-21 23:00:00 | 2017-09-20 17:00:00 | 80285.0 | 82608.343750 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | SeasESOpt | | - | ---------- | ------ | ----------- | | 0 | 1 | mae | 6694.042188 | | 1 | 1 | mape | 0.060392 | | 2 | 1 | mase | 0.827062 | | 3 | 1 | rmse | 8118.297509 | | 4 | 1 | smape | 0.028961 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla SeasonalExponentialSmoothingOptimized API](../../src/core/models.html#seasonalexponentialsmoothingoptimized) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Simple Exponential Smoothing Optimized Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/simpleexponentialoptimized.html > Step-by-step guide on using the > `SimpleExponentialSmoothingOptimized Model` with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Simple Exponential Smoothing Optimized Model](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of SimpleExponentialSmoothingOptimized with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction Simple Exponential Smoothing Optimized (SES Optimized) is a forecasting model used to predict future values in univariate time series. It is a variant of the simple exponential smoothing (SES) method that uses an optimization approach to estimate the model parameters more accurately. The SES Optimized method uses a single smoothing parameter to estimate the trend and seasonality in the time series data. The model attempts to minimize the mean squared error (MSE) between the predictions and the actual values in the training sample using an optimization algorithm. The SES Optimized approach is especially useful for time series with strong trend and seasonality patterns, or for time series with noisy data. However, it is important to note that this model assumes that the time series is stationary and that the variation in the data is random and there are no non-random patterns in the data. If these assumptions are not met, the SES Optimized model may not perform well and another forecasting method may be required. ## Simple Exponential Smoothing Model The simplest of the exponentially smoothing methods is naturally called simple exponential smoothing (SES). This method is suitable for forecasting data with no clear trend or seasonal pattern. Using the naïve method, all forecasts for the future are equal to the last observed value of the series, $\hat{y}_{T+h|T} = y_{T},$ for $h=1,2,\dots$. Hence, the naïve method assumes that the most recent observation is the only important one, and all previous observations provide no information for the future. This can be thought of as a weighted average where all of the weight is given to the last observation. Using the average method, all future forecasts are equal to a simple average of the observed data, $\hat{y}_{T+h|T} = \frac1T \sum_{t=1}^T y_t,$ for $h=1,2,\dots$ Hence, the average method assumes that all observations are of equal importance, and gives them equal weights when generating forecasts. We often want something between these two extremes. For example, it may be sensible to attach larger weights to more recent observations than to observations from the distant past. This is exactly the concept behind simple exponential smoothing. Forecasts are calculated using weighted averages, where the weights decrease exponentially as observations come from further in the past — the smallest weights are associated with the oldest observations: where $0 \le \alpha \le 1$ is the smoothing parameter. The one-step-ahead forecast for time $T+1$ is a weighted average of all of the observations in the series $y_1,\dots,y_T$. The rate at which the weights decrease is controlled by the parameter $\alpha$. For any $\alpha$ between 0 and 1, the weights attached to the observations decrease exponentially as we go back in time, hence the name “exponential smoothing”. If $\alpha$ is small (i.e., close to 0), more weight is given to observations from the more distant past. If $\alpha$ is large (i.e., close to 1), more weight is given to the more recent observations. For the extreme case where $\alpha=1$, $\hat{y}_{T+1|T}=y_T$ and the forecasts are equal to the naïve forecasts. ## Optimisation The application of every exponential smoothing method requires the smoothing parameters and the initial values to be chosen. In particular, for simple exponential smoothing, we need to select the values of $\alpha$ and $\ell_0$ . All forecasts can be computed from the data once we know those values. For the methods that follow there is usually more than one smoothing parameter and more than one initial component to be chosen. In some cases, the smoothing parameters may be chosen in a subjective manner — the forecaster specifies the value of the smoothing parameters based on previous experience. However, a more reliable and objective way to obtain values for the unknown parameters is to estimate them from the observed data. From regression models we estimated the coefficients of a regression model by minimising the sum of the squared residuals (usually known as SSE or “sum of squared errors”). Similarly, the unknown parameters and the initial values for any exponential smoothing method can be estimated by minimising the SSE. The residuals are specified as $e_t=y_t - \hat{y}_{t|t-1}$ for $t=1,\dots,T$. Hence, we find the values of the unknown parameters and the initial values that minimise Unlike the regression case (where we have formulas which return the values of the regression coefficients that minimise the SSE), this involves a non-linear minimisation problem, and we need to use an optimisation tool to solve it. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/ads.csv") df.head() ``` | | Time | Ads | | - | ------------------- | ------ | | 0 | 2017-09-13T00:00:00 | 80115 | | 1 | 2017-09-13T01:00:00 | 79885 | | 2 | 2017-09-13T02:00:00 | 89325 | | 3 | 2017-09-13T03:00:00 | 101930 | | 4 | 2017-09-13T04:00:00 | 121630 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | ------ | ---------- | | 0 | 2017-09-13T00:00:00 | 80115 | 1 | | 1 | 2017-09-13T01:00:00 | 79885 | 1 | | 2 | 2017-09-13T02:00:00 | 89325 | 1 | | 3 | 2017-09-13T03:00:00 | 101930 | 1 | | 4 | 2017-09-13T04:00:00 | 121630 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Simple Exponential Smoothing Optimized Model` 2. Data to test our model For the test data we will use the last 30 Hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2017-09-20 17:00:00'] test = df[df.ds>'2017-09-20 17:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((186, 3), (30, 3)) ``` ## Implementation of SimpleExponentialSmoothingOptimized with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import SimpleExponentialSmoothingOptimized ``` ### Instantiating Model ```python theme={null} horizon = len(test) # number of predictions models = [SimpleExponentialSmoothingOptimized()] # multiplicative additive ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[SESOpt]) ``` Let’s see the results of our `Simple Exponential Smoothing Optimized model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'mean': array([139526.04792941]), 'fitted': array([ nan, 80115. , 79887.3 , 89230.625, 101803.01 , 121431.73 , 116524.57 , 106595.3 , 102833. , 108002.78 , 116043.78 , 130880.14 , 148838.6 , 157502.48 , 150782.88 , 149309.88 , 150092.1 , 144833.12 , 150631.44 , 163707.92 , 166209.73 , 139786.89 , 106233.92 , 96874.54 , 82663.55 , 80150.38 , 75383.16 , 85007.78 , 101909.28 , 124902.74 , 118098.73 , 109313.734, 102543.39 , 102243.03 , 115704.03 , 130391.64 , 144185.67 , 148922.16 , 149147.72 , 148051.08 , 148802.4 , 149819.72 , 150562.5 , 149451.22 , 150509.31 , 129343.8 , 104070.29 , 92293.95 , 82860.29 , 76380.45 , 75142.51 , 82565.02 , 88732.7 , 118133.02 , 115219.43 , 110982.8 , 98981.23 , 104132.96 , 108619.68 , 126459.8 , 140295.25 , 152348.25 , 146335.73 , 148003.16 , 147737.69 , 145769.88 , 149249.84 , 159620.25 , 161070.36 , 135775.5 , 113173.305, 100329.734, 87742.15 , 87834.07 , 88834.89 , 92314.85 , 104343.5 , 115824.03 , 128818.74 , 141259.34 , 144408.19 , 143261.58 , 133290.72 , 131260.5 , 142367.81 , 157224.92 , 152547.25 , 153723.12 , 151220.28 , 150650.75 , 147467.16 , 152474.42 , 146931. , 125461.86 , 118000.37 , 96913. , 93643.03 , 89105.83 , 89342.61 , 90562.68 , 98212.73 , 112426.43 , 129299.56 , 141283.95 , 152447.23 , 152578.67 , 141284.1 , 147487.34 , 160973.77 , 166281.39 , 166775.02 , 163176.34 , 157363.72 , 159038.1 , 160010.19 , 168261.66 , 169883.61 , 142981.73 , 113255.266, 97504.1 , 81833.29 , 79533.234, 78361.836, 87948.17 , 99671.58 , 123538.914, 111447.14 , 99560.07 , 97674.05 , 97655.19 , 102515.9 , 119755.86 , 135595.02 , 140074.75 , 141713.45 , 142214.94 , 145328.55 , 145334.94 , 150359.25 , 161408.39 , 153494.94 , 134907.75 , 107343.43 , 95167.984, 79671.53 , 78348.37 , 74706.78 , 81917.164, 97789.67 , 119129.445, 113175.14 , 99022.95 , 94050.23 , 93663.9 , 104079.79 , 119593.3 , 135826.03 , 146348.7 , 139236.84 , 147145.12 , 144957.1 , 151305.88 , 156032.27 , 161331.47 , 164973.22 , 134398.83 , 105873.14 , 92985.18 , 79407.15 , 79974.27 , 78128.64 , 85708.44 , 99866.984, 123639.87 , 116408.05 , 104411.18 , 101469.71 , 97673.34 , 108159.086, 121119.09 , 140652.69 , 138575.98 , 140965.86 , 141519.4 , 141589.3 , 140619.8 ], dtype=float32)} ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} fitted=pd.DataFrame(result.get("fitted"), columns=["fitted"]) fitted["ds"]=df["ds"] fitted ``` | | fitted | ds | | --- | ------------- | ------------------- | | 0 | NaN | 2017-09-13 00:00:00 | | 1 | 80115.000000 | 2017-09-13 01:00:00 | | 2 | 79887.296875 | 2017-09-13 02:00:00 | | ... | ... | ... | | 183 | 141519.406250 | 2017-09-20 15:00:00 | | 184 | 141589.296875 | 2017-09-20 16:00:00 | | 185 | 140619.796875 | 2017-09-20 17:00:00 | ```python theme={null} sns.lineplot(df, x="ds", y="y", label="Actual", linewidth=2) sns.lineplot(fitted,x="ds", y="fitted", label="Fitted", linestyle="--" ) plt.title("Ads watched (hourly data)"); plt.show() ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 hors ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} # Prediction Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | SESOpt | | --- | ---------- | ------------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 139526.046875 | | 1 | 1 | 2017-09-20 19:00:00 | 139526.046875 | | 2 | 1 | 2017-09-20 20:00:00 | 139526.046875 | | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 139526.046875 | | 28 | 1 | 2017-09-21 22:00:00 | 139526.046875 | | 29 | 1 | 2017-09-21 23:00:00 | 139526.046875 | Let’s visualize the fitted values ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | SESOpt | | - | ---------- | ------------------- | -------- | ------------- | | 0 | 1 | 2017-09-13 00:00:00 | 80115.0 | NaN | | 1 | 1 | 2017-09-13 01:00:00 | 79885.0 | 80115.000000 | | 2 | 1 | 2017-09-13 02:00:00 | 89325.0 | 79887.296875 | | 3 | 1 | 2017-09-13 03:00:00 | 101930.0 | 89230.625000 | | 4 | 1 | 2017-09-13 04:00:00 | 121630.0 | 101803.007812 | ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecast_df = sf.predict(h=horizon) forecast_df ``` | | unique\_id | ds | SESOpt | | --- | ---------- | ------------------- | ------------- | | 0 | 1 | 2017-09-20 18:00:00 | 139526.046875 | | 1 | 1 | 2017-09-20 19:00:00 | 139526.046875 | | 2 | 1 | 2017-09-20 20:00:00 | 139526.046875 | | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 139526.046875 | | 28 | 1 | 2017-09-21 22:00:00 | 139526.046875 | | 29 | 1 | 2017-09-21 23:00:00 | 139526.046875 | ```python theme={null} sf.plot(train, forecast_df) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 30 hours ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=30, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` index. If you dont like working with index just run `crossvalidation_df.resetindex()`. * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | SESOpt | | --- | ---------- | ------------------- | ------------------- | -------- | ------------- | | 0 | 1 | 2017-09-18 06:00:00 | 2017-09-18 05:00:00 | 99440.0 | 111447.140625 | | 1 | 1 | 2017-09-18 07:00:00 | 2017-09-18 05:00:00 | 97655.0 | 111447.140625 | | 2 | 1 | 2017-09-18 08:00:00 | 2017-09-18 05:00:00 | 97655.0 | 111447.140625 | | ... | ... | ... | ... | ... | ... | | 87 | 1 | 2017-09-21 21:00:00 | 2017-09-20 17:00:00 | 103080.0 | 139526.046875 | | 88 | 1 | 2017-09-21 22:00:00 | 2017-09-20 17:00:00 | 95155.0 | 139526.046875 | | 89 | 1 | 2017-09-21 23:00:00 | 2017-09-20 17:00:00 | 80285.0 | 139526.046875 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=24), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | SESOpt | | - | ---------- | ------ | ------------ | | 0 | 1 | mae | 29230.182292 | | 1 | 1 | mape | 0.314203 | | 2 | 1 | mase | 3.611444 | | 3 | 1 | rmse | 35866.963426 | | 4 | 1 | smape | 0.124271 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla SeasonalExponentialOptimized API](../../src/core/models.html#simpleexponentialsmoothingoptimized) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Simple Exponential Smoothing Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/simpleexponentialsmoothing.html > Step-by-step guide on using the `SimpleExponentialSmoothing Model` > with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Simple Exponential Smoothing](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of SimpleExponentialSmoothing with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction Exponential smoothing was proposed in the late 1950s (Brown, 1959; Holt, 1957; Winters, 1960), and has motivated some of the most successful forecasting methods. Forecasts produced using exponential smoothing methods are weighted averages of past observations, with the weights decaying exponentially as the observations get older. In other words, the more recent the observation the higher the associated weight. This framework generates reliable forecasts quickly and for a wide range of time series, which is a great advantage and of major importance to applications in industry. The simple exponential smoothing model is a method used in time series analysis to predict future values based on historical observations. This model is based on the idea that future values of a time series will be influenced by past values, and that the influence of past values will decrease exponentially as you go back in time. The simple exponential smoothing model uses a smoothing factor, which is a number between 0 and 1 that indicates the relative importance given to past observations in predicting future values. A value of 1 indicates that all past observations are given equal importance, while a value of 0 indicates that only the latest observation is considered. The simple exponential smoothing model can be expressed mathematically as: $\hat{y}_{T+1|T} = \alpha y_T + \alpha(1-\alpha) y_{T-1} + \alpha(1-\alpha)^2 y_{T-2}+ \cdots,$ where $y_T$ is the observed value in period $t$, $\hat{y}_{T+1|T}$ is the predicted value for the next period, y $(t-1)$ is the observed value in the previous period, and $\alpha$ is the smoothing factor. The simple exponential smoothing model is a widely used forecasting model due to its simplicity and ease of use. However, it also has its limitations, as it cannot capture complex patterns in the data and is not suitable for time series with trends or seasonal patterns. ## Building of Simple exponential smoothing model The simplest of the exponentially smoothing methods is naturally called simple exponential smoothing (SES). This method is suitable for forecasting data with no clear trend or seasonal pattern. Using the naïve method, all forecasts for the future are equal to the last observed value of the series, $\hat{y}_{T+h|T} = y_{T},$ for $h=1,2,\dots$. Hence, the naïve method assumes that the most recent observation is the only important one, and all previous observations provide no information for the future. This can be thought of as a weighted average where all of the weight is given to the last observation. Using the average method, all future forecasts are equal to a simple average of the observed data, $\hat{y}_{T+h|T} = \frac1T \sum_{t=1}^T y_t,$ for $h=1,2,\dots$ Hence, the average method assumes that all observations are of equal importance, and gives them equal weights when generating forecasts. We often want something between these two extremes. For example, it may be sensible to attach larger weights to more recent observations than to observations from the distant past. This is exactly the concept behind simple exponential smoothing. Forecasts are calculated using weighted averages, where the weights decrease exponentially as observations come from further in the past — the smallest weights are associated with the oldest observations: where $0 \le \alpha \le 1$ is the smoothing parameter. The one-step-ahead forecast for time $T+1$ is a weighted average of all of the observations in the series $y_1,\dots,y_T$. The rate at which the weights decrease is controlled by the parameter $\alpha$. For any $\alpha$ between 0 and 1, the weights attached to the observations decrease exponentially as we go back in time, hence the name “exponential smoothing”. If $\alpha$ is small (i.e., close to 0), more weight is given to observations from the more distant past. If $\alpha$ is large (i.e., close to 1), more weight is given to the more recent observations. For the extreme case where $\alpha=1$, $\hat{y}_{T+1|T}=y_T$ and the forecasts are equal to the naïve forecasts. We present two equivalent forms of simple exponential smoothing, each of which leads to the forecast Equation (1). ### Weighted average form The forecast at time $T+1$ is equal to a weighted average between the most recent observation $y_T$ and the previous forecast $\hat{y}_{T|T-1}$: $\hat{y}_{T+1|T} = \alpha y_T + (1-\alpha) \hat{y}_{T|T-1},$ where $0 \le \alpha \le 1$ is the smoothing parameter. Similarly, we can write the fitted values as $\hat{y}_{t+1|t} = \alpha y_t + (1-\alpha) \hat{y}_{t|t-1},$ for $t=1,\dots,T$. (Recall that fitted values are simply one-step forecasts of the training data.) The process has to start somewhere, so we let the first fitted value at time 1 be denoted by $\ell_{0}$ (which we will have to estimate). Then Substituting each equation into the following equation, we obtain The last term becomes tiny for large $T$. So, the weighted average form leads to the same forecast Equation (1). ### Component form An alternative representation is the component form. For simple exponential smoothing, the only component included is the level, $\ell_{t}$. Component form representations of exponential smoothing methods comprise a forecast equation and a smoothing equation for each of the components included in the method. The component form of simple exponential smoothing is given by: where $\ell_{t}$ is the level (or the smoothed value) of the series at time $t$. Setting $h=1$ gives the fitted values, while setting $t=T$ gives the true forecasts beyond the training data. The forecast equation shows that the forecast value at time $t+1$ is the estimated level at time $t$. The smoothing equation for the level (usually referred to as the level equation) gives the estimated level of the series at each period $t$. If we replace $\ell_{t}$ with $\hat{y}_{t+1|t}$ and $\ell_{t-1}$ with $\hat{y}_{t|t-1}$ in the smoothing equation, we will recover the weighted average form of simple exponential smoothing. The component form of simple exponential smoothing is not particularly useful on its own, but it will be the easiest form to use when we start adding other components. ### Flat forecasts Simple exponential smoothing has a “flat” forecast function: $\hat{y}_{T+h|T} = \hat{y}_{T+1|T}=\ell_T, \qquad h=2,3,\dots.$ That is, all forecasts take the same value, equal to the last level component. Remember that these forecasts will only be suitable if the time series has no trend or seasonal component. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/ads.csv") df.head() ``` | | Time | Ads | | - | ------------------- | ------ | | 0 | 2017-09-13T00:00:00 | 80115 | | 1 | 2017-09-13T01:00:00 | 79885 | | 2 | 2017-09-13T02:00:00 | 89325 | | 3 | 2017-09-13T03:00:00 | 101930 | | 4 | 2017-09-13T04:00:00 | 121630 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df ``` | | ds | y | unique\_id | | --- | ------------------- | ------ | ---------- | | 0 | 2017-09-13T00:00:00 | 80115 | 1 | | 1 | 2017-09-13T01:00:00 | 79885 | 1 | | 2 | 2017-09-13T02:00:00 | 89325 | 1 | | ... | ... | ... | ... | | 213 | 2017-09-21T21:00:00 | 103080 | 1 | | 214 | 2017-09-21T22:00:00 | 95155 | 1 | | 215 | 2017-09-21T23:00:00 | 80285 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); # Grafico plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') #plt.savefig("Density and QQ Plot") plt.show(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Simple Exponential Smoothing (SES)`. 2. Data to test our model For the test data we will use the last 30 hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2017-09-20 17:00:00'] test = df[df.ds>'2017-09-20 17:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((186, 3), (30, 3)) ``` ## Implementation of SimpleExponentialSmoothing with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import SimpleExponentialSmoothing ``` ### Instantiating Model We are going to build different models, for different values of alpha. ```python theme={null} horizon = len(test) # We call the model that we are going to use models = [SimpleExponentialSmoothing(alpha=0.1, alias="SES01"), SimpleExponentialSmoothing(alpha=0.5,alias="SES05"), SimpleExponentialSmoothing(alpha=0.8,alias="SES08") ] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[SES01,SES05,SES08]) ``` Let’s see the results of our Simple `Simple Exponential Smoothing model (SES)`. We can observe it with the following instruction: ```python theme={null} result01=sf.fitted_[0,0].model_ result05=sf.fitted_[0,1].model_ result08=sf.fitted_[0,2].model_ result01 ``` ```text theme={null} {'mean': array([126112.90072589]), 'fitted': array([ nan, 80115. , 80092. , 81015.3 , 83106.77 , 86959.09 , 89910.69 , 91569.12 , 92691.7 , 94228.03 , 96417.73 , 99878.96 , 104793.06 , 110072.76 , 114136.98 , 117652.78 , 120897.5 , 123285.75 , 126026.18 , 129807.56 , 133450.3 , 134057.28 , 131241.05 , 127794.945, 123267.445, 118953.2 , 114591.38 , 111642.74 , 110686.47 , 112131.32 , 112721.19 , 112371.57 , 111381.914, 110467.73 , 111004.95 , 112958.45 , 116095.11 , 119382.6 , 122359.336, 124927.41 , 127315.664, 129567.1 , 131667.39 , 133444.66 , 135152.19 , 134549.97 , 131476.47 , 127546.32 , 123068.19 , 118392.875, 114066.586, 110923.92 , 108711.03 , 109682.93 , 110233.64 , 110304.27 , 109159.84 , 108662.36 , 108662.625, 110460.36 , 113457.83 , 117359.05 , 120250.64 , 123027.58 , 125498.32 , 127523.484, 129699.64 , 132702.17 , 135540.45 , 135538.4 , 133279.06 , 129971.164, 125735.55 , 121945.49 , 118635.445, 116006.9 , 114852.71 , 114961.44 , 116360.3 , 118862.766, 121420.484, 123603.44 , 124562.09 , 125229.88 , 126954.9 , 129996.91 , 132247.22 , 134396. , 136075.89 , 137532.81 , 138523.03 , 139923.22 , 140618.4 , 139081.06 , 136965.45 , 132938.9 , 129006.016, 125011.414, 121444.77 , 118357.8 , 116351.016, 115972.914, 117322.625, 119730.86 , 123013.77 , 125970.4 , 127490.36 , 129496.32 , 132657.69 , 136025.42 , 139100.88 , 141504.8 , 143084.81 , 144681.83 , 146215.64 , 148428.58 , 150575.72 , 149789.16 , 146105.73 , 141229.66 , 135274.2 , 129697.77 , 124563. , 120911.2 , 118799.08 , 119297.17 , 118499.95 , 116593.96 , 114700.06 , 112995.555, 111952.5 , 112750.25 , 115050.73 , 117557.66 , 119974.89 , 122199.4 , 124515.46 , 126597.414, 128978.67 , 132232.81 , 134351.03 , 134387.92 , 131655.62 , 127994.57 , 123146.61 , 118665.445, 114265.91 , 111038.31 , 109729.484, 110691.03 , 110933.43 , 109728.086, 108155.28 , 106705.75 , 106453.68 , 107783.305, 110603.98 , 114189.08 , 116686.67 , 119740.51 , 122259.95 , 125170.96 , 128261.86 , 131574.17 , 134917.77 , 134834.98 , 131909.98 , 128004.484, 123131.04 , 118815.94 , 114745.34 , 111849.305, 110665.375, 111986.836, 112421.66 , 111608.49 , 110591.64 , 109295.98 , 109192.875, 110398.59 , 113443.734, 115954.86 , 118458.375, 120765.04 , 122847.53 , 124623.78 ], dtype=float32)} ``` As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} fitted=pd.DataFrame(result01.get("fitted"), columns=["fitted01"]) fitted["fitted05"]=result05.get("fitted") fitted["fitted08"]=result08.get("fitted") fitted["ds"]=df["ds"] fitted ``` | | fitted01 | fitted05 | fitted08 | ds | | --- | ------------- | --------- | ------------- | ------------------- | | 0 | NaN | NaN | NaN | 2017-09-13 00:00:00 | | 1 | 80115.000000 | 80115.00 | 80115.000000 | 2017-09-13 01:00:00 | | 2 | 80092.000000 | 80000.00 | 79931.000000 | 2017-09-13 02:00:00 | | ... | ... | ... | ... | ... | | 183 | 120765.039062 | 139195.00 | 141302.828125 | 2017-09-20 15:00:00 | | 184 | 122847.531250 | 140392.50 | 141532.562500 | 2017-09-20 16:00:00 | | 185 | 124623.781250 | 140501.25 | 140794.515625 | 2017-09-20 17:00:00 | ```python theme={null} sns.lineplot(df, x="ds", y="y", label="Actual", linewidth=2) sns.lineplot(fitted,x="ds", y="fitted01", label="Fitted01", linestyle="--", ) sns.lineplot(fitted, x="ds", y="fitted05", label="Fitted05", color="lime") sns.lineplot(fitted, x="ds", y="fitted08", label="Fitted08") plt.title("Ads watched (hourly data)"); plt.show() ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 30 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} # Prediction Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat.head() ``` | | unique\_id | ds | SES01 | SES05 | SES08 | | - | ---------- | ------------------- | ------------- | ---------- | ------------ | | 0 | 1 | 2017-09-20 18:00:00 | 126112.898438 | 140008.125 | 139770.90625 | | 1 | 1 | 2017-09-20 19:00:00 | 126112.898438 | 140008.125 | 139770.90625 | | 2 | 1 | 2017-09-20 20:00:00 | 126112.898438 | 140008.125 | 139770.90625 | | 3 | 1 | 2017-09-20 21:00:00 | 126112.898438 | 140008.125 | 139770.90625 | | 4 | 1 | 2017-09-20 22:00:00 | 126112.898438 | 140008.125 | 139770.90625 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | SES01 | SES05 | SES08 | | - | ---------- | ------------------- | -------- | ------------ | -------- | ------------ | | 0 | 1 | 2017-09-13 00:00:00 | 80115.0 | NaN | NaN | NaN | | 1 | 1 | 2017-09-13 01:00:00 | 79885.0 | 80115.000000 | 80115.00 | 80115.000000 | | 2 | 1 | 2017-09-13 02:00:00 | 89325.0 | 80092.000000 | 80000.00 | 79931.000000 | | 3 | 1 | 2017-09-13 03:00:00 | 101930.0 | 81015.296875 | 84662.50 | 87446.203125 | | 4 | 1 | 2017-09-13 04:00:00 | 121630.0 | 83106.773438 | 93296.25 | 99033.242188 | ### Predict method To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon). \* `h (int):` represents the forecast $h$ steps into the future. In this case, 30 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the `y hat` values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecast_df = sf.predict(h=horizon) forecast_df ``` | | unique\_id | ds | SES01 | SES05 | SES08 | | --- | ---------- | ------------------- | ------------- | ---------- | ------------ | | 0 | 1 | 2017-09-20 18:00:00 | 126112.898438 | 140008.125 | 139770.90625 | | 1 | 1 | 2017-09-20 19:00:00 | 126112.898438 | 140008.125 | 139770.90625 | | 2 | 1 | 2017-09-20 20:00:00 | 126112.898438 | 140008.125 | 139770.90625 | | ... | ... | ... | ... | ... | ... | | 27 | 1 | 2017-09-21 21:00:00 | 126112.898438 | 140008.125 | 139770.90625 | | 28 | 1 | 2017-09-21 22:00:00 | 126112.898438 | 140008.125 | 139770.90625 | | 29 | 1 | 2017-09-21 23:00:00 | 126112.898438 | 140008.125 | 139770.90625 | ```python theme={null} sf.plot(train, forecast_df) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 30 hourly `(n_windows=)`, forecasting every second months `(step_size=30)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 30 hours ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=30, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | SES01 | SES05 | SES08 | | --- | ---------- | ------------------- | ------------------- | -------- | ------------- | ---------- | ------------- | | 0 | 1 | 2017-09-18 06:00:00 | 2017-09-18 05:00:00 | 99440.0 | 118499.953125 | 109816.250 | 112747.695312 | | 1 | 1 | 2017-09-18 07:00:00 | 2017-09-18 05:00:00 | 97655.0 | 118499.953125 | 109816.250 | 112747.695312 | | 2 | 1 | 2017-09-18 08:00:00 | 2017-09-18 05:00:00 | 97655.0 | 118499.953125 | 109816.250 | 112747.695312 | | ... | ... | ... | ... | ... | ... | ... | ... | | 87 | 1 | 2017-09-21 21:00:00 | 2017-09-20 17:00:00 | 103080.0 | 126112.898438 | 140008.125 | 139770.906250 | | 88 | 1 | 2017-09-21 22:00:00 | 2017-09-20 17:00:00 | 95155.0 | 126112.898438 | 140008.125 | 139770.906250 | | 89 | 1 | 2017-09-21 23:00:00 | 2017-09-20 17:00:00 | 80285.0 | 126112.898438 | 140008.125 | 139770.906250 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=24), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | SES01 | SES05 | SES08 | | - | ---------- | ------ | ------------ | ------------ | ------------ | | 0 | 1 | mae | 25173.939583 | 29390.875000 | 29311.802083 | | 1 | 1 | mape | 0.255088 | 0.316440 | 0.315339 | | 2 | 1 | mase | 3.110288 | 3.631298 | 3.621528 | | 3 | 1 | rmse | 28923.395381 | 36184.340869 | 36027.710540 | | 4 | 1 | smape | 0.109972 | 0.124803 | 0.124542 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla SeasonalExponentialSmoothing API](../../src/core/models.html#simpleexponentialsmoothing) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Standard Theta Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/standardtheta.html > Step-by-step guide on using the `Standard Theta Model` with > `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). 2\. [V. Assimakopoulos, K. Nikolopoulos, “The theta model: a decomposition approach to forecasting”](https://www.sciencedirect.com/science/article/abs/pii/S0169207000000662) 3\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [Standard Theta](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of StandardTheta with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The Theta method [(Assimakopoulos & Nikolopoulos, 2000, hereafter A\&N)](https://www.sciencedirect.com/science/article/abs/pii/S0169207000000662) is applied to non-seasonal or deseasonalised time series, where the deseasonalisation is usually performed via the multiplicative classical decomposition. The method decomposes the original time series into two new lines through the so-called theta coefficients, denoted by ${\theta}_1$ and ${\theta}_2$ for ${\theta}_1, {\theta}_2 \in \mathbb{R}$, which are applied to the second difference of the data. The second differences are reduced when ${\theta}<1$, resulting in a better approximation of the long-term behaviour of the series (Assimakopoulos, 1995). If ${\theta}$ is equal to zero, the new line is a straight line. When ${\theta}>1$ the local curvatures are increased, magnifying the short-term movements of the time series (A\&N). The new lines produced are called theta lines, denoted here by $\text{Z}(\theta_1)$ and $\text{Z}(\theta_2)$. These lines have the same mean value and slope as the original data, but the local curvatures are either filtered out or enhanced, depending on the value of the $\theta$ coefficient. In other words, the decomposition process has the advantage of exploiting information in the data that usually cannot be captured and modelled completely through the extrapolation of the original time series. The theta lines can be regarded as new time series and are extrapolated separately using an appropriate forecasting method. Once the extrapolation of each theta line has been completed, recomposition takes place through a combination scheme in order to calculate the point forecasts of the original time series. Combining has long been considered as a useful practice in the forecasting literature (for example, [Clemen, 1989, Makridakis and Winkler, 1983, Petropoulos et al., 2014]()), and therefore its application to the Theta method is expected to result in more accurate and robust forecasts. The Theta method is quite versatile in terms of choosing the number of theta lines, the theta coefficients and the extrapolation methods, and combining these to obtain robust forecasts. However, A\&N proposed a simplified version involving the use of only two theta lines with prefixed $\theta$ coefficients that are extrapolated over time using a linear regression (LR) model for the theta line with ${\theta}_1 =0$ and simple exponential smoothing (SES) for the theta line with ${\theta}_2 =2$. The final forecasts are produced by combining the forecasts of the two theta lines with equal weights. The performance of the Theta method has also been confirmed by other empirical studies (for example Nikolopoulos et al., 2012, Petropoulos and Nikolopoulos, 2013). Moreover, Hyndman and Billah (2003), hereafter H\&B, showed that the simple exponential smoothing with drift model (SES-d) is a statistical model for the simplified version of the Theta method. More recently, Thomakos and Nikolopoulos (2014) provided additional theoretical insights, while Thomakos and Nikolopoulos (2015) derived new theoretical formulations for the application of the method to multivariate time series, and investigated the conditions under which the bivariate Theta method is expected to forecast better than the univariate one. Despite these advances, we believe that the Theta method deserves more attention from the forecasting community, given its simplicity and superior forecasting performance. One key aspect of the Theta method is that, by definition, it is dynamic. One can choose different theta lines and combine the produced forecasts using either equal or unequal weights. However, AN limit this important property by fixing the theta coefficients to have predefined values. ## Standard Theta Model Assimakopoulos and Nikolopoulo for standard theta model proposed the Theta line as the solution of the equation $$ \begin{equation} D^2 \zeta_t(\theta) = \theta D^2 Y_t, t = 1,\cdots,T \tag 1 \end{equation} $$ where $Y_1, \cdots , Y_T$ represent the original time series data and $DX_t = (X_t − X_{t−1})$. The initial values $\zeta_1$ and $\zeta_2$ are obtained by minimizing $\sum_{i=1}^{T} [Y_t - \zeta_t (\theta) ]^2$. However, the analytical solution of (1) is given by $$ \begin{equation} \zeta_t(\theta)=\theta Y_t +(1−\theta)(A_T +B_T t),\ t=1, \cdots, T, \tag 2 \end{equation} $$ where $A_T$ and $B_T$ are the minimum square coefficients of a simple linear regression over $Y_1, \cdots,Y_T$ against $1, \cdots , T$ which are only dependent on the original data and given as follow $$ \begin{equation} A_T=\frac{1}{T} \sum_{i=1}^{T} Y_t - \frac{T+1}{2} B_T \tag 3 \end{equation} $$ $$ \begin{equation} B_T=\frac{6}{T^2 - 1} (\frac{2}{T} \sum_{t=1}^{T} tY_t - \frac{T+1}{T} \sum_{t=1}^{T} Y_t \tag 4 \end{equation} $$ Theta lines can be understood as functions of the linear regression model directly applied to the data from this perspective. Indeed, the Theta method’s projections for h steps ahead are an ad hoc combination (50 percent - 50 percent) of the linear extrapolations of $\zeta(0)$ and $\zeta(2)$. * When $\theta < 1$ is applied to the second differences of the data, the decomposition process is defined by a theta coefficient, which reduces the second differences and improves the approximation of series behavior. * If $\theta = 0$, the deconstructed line is turned into a constant straight line. (see Fig) * If $\theta > 1$ then the short term movements of the analyzed series show more local curvatures (see fig)
Figure
Figure
We will refer to the above setup as the standard Theta method. The steps for building the theta method are as follows: 1. **Deseasonalisation:** Firstly, the time series data is tested for statistically significant seasonal behaviour. A time series is seasonal if $|\rho_m| > q_{1- \frac{\alpha}{2} } \sqrt{\frac{1+2 \sum_{i=1}^{m-1} \rho_{i}^{2} }{T} }$ where ρk denotes the lag $k$ autocorrelation function, $m$ is the number of the periods within a seasonal cycle (for example, 12 for monthly data), $T$ is the sample size, $q$ is the quantile function of the standard normal distribution, and $(1 − a)\%$ is the confidence level. Assimakopoulos and Nikolopoulo \[Standar Theta model] opted for a 90% confidence level. If the time series is identified as seasonal, then it is deseasonalised via the classical decomposition method, assuming the seasonal component to have a multiplicative relationship. 1. **Decomposition:** The second step consits for the decomposition of the seasonally adjusted time series into two Theta lines, the `linear regression` line $\zeta(0)$ and the theta line $\zeta(2)$. 2. **Extrapolation:** $\zeta(2)$ is extrapolated using `simple exponential smoothing (SES)`, while $\zeta(0)$ is extrapolated as a normal `linear regression` line. 3. **Combination:** the final forecast is a combination of the forecasts of the two $\theta$ lines using equal weights. 4. Reseasonalisation: In the presence of seasonality in first step, then the final forecasts are multiplied by the respective seasonal indices. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ### Read Data ```python theme={null} import pandas as pd df = pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/milk_production.csv", usecols=[1,2]) df.head() ``` | | month | production | | - | ---------- | ---------- | | 0 | 1962-01-01 | 589 | | 1 | 1962-02-01 | 561 | | 2 | 1962-03-01 | 640 | | 3 | 1962-04-01 | 656 | | 4 | 1962-05-01 | 727 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ---------- | --- | ---------- | | 0 | 1962-01-01 | 589 | 1 | | 1 | 1962-02-01 | 561 | 1 | | 2 | 1962-03-01 | 640 | 1 | | 3 | 1962-04-01 | 656 | 1 | | 4 | 1962-05-01 | 727 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ### Additive ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "additive", period=12) a.plot(); ``` ### Multiplicative ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose a = seasonal_decompose(df["y"], model = "Multiplicative", period=12) a.plot(); ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `Theta` model 2. Data to test our model For the test data we will use the last 12 months to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='1974-12-01'] test = df[df.ds>'1974-12-01'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((156, 3), (12, 3)) ``` ## Implementation of StandardTheta with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import Theta ``` ### Instantiating Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 12 # Monthly data horizon = len(test) # number of predictions models = [Theta(season_length=season_length, decomposition_type="additive")] # multiplicative additive ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='MS') ``` ### Fit Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[Theta]) ``` Let’s see the results of our Theta model. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ print(result.keys()) print(result['fit']) ``` ```text theme={null} dict_keys(['mse', 'amse', 'fit', 'residuals', 'm', 'states', 'par', 'n', 'modeltype', 'mean_y', 'decompose', 'decomposition_type', 'seas_forecast', 'fitted']) results(x=array([225.82002697, 0.76015625]), fn=10.638733596938778, nit=19, simplex=array([[241.83142594, 0.76274414], [225.82002697, 0.76015625], [212.41789302, 0.76391602]])) ``` Let us now visualize the residuals of our models. As we can see, the result obtained above has an output in a dictionary, to extract each element from the dictionary we are going to use the `.get()` function to extract the element and then we are going to save it in a `pd.DataFrame()`. ```python theme={null} residual=pd.DataFrame(result.get("residuals"), columns=["residual Model"]) residual ``` | | residual Model | | --- | -------------- | | 0 | -17.596375 | | 1 | -46.997192 | | 2 | 23.093933 | | ... | ... | | 153 | -59.003235 | | 154 | -91.150085 | | 155 | -42.749451 | ```python theme={null} import scipy.stats as stats fig, axs = plt.subplots(nrows=2, ncols=2) residual.plot(ax=axs[0,0]) axs[0,0].set_title("Residuals"); sns.distplot(residual, ax=axs[0,1]); axs[0,1].set_title("Density plot - Residual"); stats.probplot(residual["residual Model"], dist="norm", plot=axs[1,0]) axs[1,0].set_title('Plot Q-Q') plot_acf(residual, lags=35, ax=axs[1,1],color="fuchsia") axs[1,1].set_title("Autocorrelation"); plt.show(); ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} # Prediction Y_hat = sf.forecast(df=train, h=horizon, fitted=True) Y_hat ``` | | unique\_id | ds | Theta | | --- | ---------- | ---------- | ---------- | | 0 | 1 | 1975-01-01 | 838.559814 | | 1 | 1 | 1975-02-01 | 800.188232 | | 2 | 1 | 1975-03-01 | 893.472900 | | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 816.166931 | | 10 | 1 | 1975-11-01 | 786.962036 | | 11 | 1 | 1975-12-01 | 823.826538 | ```python theme={null} values=sf.forecast_fitted_values() values.head() ``` | | unique\_id | ds | y | Theta | | - | ---------- | ---------- | ----- | ---------- | | 0 | 1 | 1962-01-01 | 589.0 | 606.596375 | | 1 | 1 | 1962-02-01 | 561.0 | 607.997192 | | 2 | 1 | 1962-03-01 | 640.0 | 616.906067 | | 3 | 1 | 1962-04-01 | 656.0 | 608.873047 | | 4 | 1 | 1962-05-01 | 727.0 | 607.395142 | Adding 95% confidence interval with the forecast method ```python theme={null} sf.forecast(df=train, h=horizon, level=[95]) ``` | | unique\_id | ds | Theta | Theta-lo-95 | Theta-hi-95 | | --- | ---------- | ---------- | ---------- | ----------- | ----------- | | 0 | 1 | 1975-01-01 | 838.559814 | 741.324280 | 954.365540 | | 1 | 1 | 1975-02-01 | 800.188232 | 640.785583 | 944.996887 | | 2 | 1 | 1975-03-01 | 893.472900 | 705.123901 | 1064.757324 | | ... | ... | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 816.166931 | 539.706665 | 1083.791626 | | 10 | 1 | 1975-11-01 | 786.962036 | 487.945831 | 1032.029053 | | 11 | 1 | 1975-12-01 | 823.826538 | 512.674500 | 1101.965576 | ```python theme={null} sf.plot(train, Y_hat) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 12 months ahead. * `level (list of floats):` this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, `level=[95]` means that the model expects the real value to be inside that interval 95% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} sf.predict(h=horizon) ``` | | unique\_id | ds | Theta | | --- | ---------- | ---------- | ---------- | | 0 | 1 | 1975-01-01 | 838.559814 | | 1 | 1 | 1975-02-01 | 800.188232 | | 2 | 1 | 1975-03-01 | 893.472900 | | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 816.166931 | | 10 | 1 | 1975-11-01 | 786.962036 | | 11 | 1 | 1975-12-01 | 823.826538 | ```python theme={null} forecast_df = sf.predict(h=horizon, level=[80,95]) forecast_df ``` | | unique\_id | ds | Theta | Theta-lo-80 | Theta-hi-80 | Theta-lo-95 | Theta-hi-95 | | --- | ---------- | ---------- | ---------- | ----------- | ----------- | ----------- | ----------- | | 0 | 1 | 1975-01-01 | 838.559814 | 765.496094 | 927.260071 | 741.324280 | 954.365540 | | 1 | 1 | 1975-02-01 | 800.188232 | 701.729736 | 898.807434 | 640.785583 | 944.996887 | | 2 | 1 | 1975-03-01 | 893.472900 | 758.480957 | 1006.847595 | 705.123901 | 1064.757324 | | ... | ... | ... | ... | ... | ... | ... | ... | | 9 | 1 | 1975-10-01 | 816.166931 | 611.404236 | 991.667175 | 539.706665 | 1083.791626 | | 10 | 1 | 1975-11-01 | 786.962036 | 561.990540 | 969.637634 | 487.945831 | 1032.029053 | | 11 | 1 | 1975-12-01 | 823.826538 | 591.283508 | 1029.491211 | 512.674500 | 1101.965576 | ```python theme={null} sf.plot(train, test.merge(forecast_df), level=[80, 95]) ``` ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=5)`, forecasting every second months `(step_size=12)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 12 months ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=train, h=horizon, step_size=12, n_windows=3) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the n\_windows. * `y:` true value * `"model":` columns with the model’s name and fitted value. ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | Theta | | - | ---------- | ------ | -------- | | 0 | 1 | mae | 8.111287 | | 1 | 1 | mape | 0.009649 | | 2 | 1 | mase | 0.364780 | | 3 | 1 | rmse | 9.730347 | | 4 | 1 | smape | 0.004829 | ## Acknowledgements We would like to thank [Naren Castellon](https://www.linkedin.com/in/naren-castellon-1541b8101/?originalSubdomain=pa) for writing this tutorial. ## References 1. [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). “Models for optimising the theta method and their relationship to state space models”. International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243). 2. [V. Assimakopoulos, K. Nikolopoulos, “The theta model: a decomposition approach to forecasting”](https://www.sciencedirect.com/science/article/abs/pii/S0169207000000662) 3. [Nixtla StandardTheta API](../../src/core/models.html#theta) 4. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 5. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 6. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # TSB Model Source: https://nixtlaverse.nixtla.io/statsforecast/docs/models/tsb.html > Step-by-step guide on using the `TSB Model` with `Statsforecast`. During this walkthrough, we will become familiar with the main `StatsForecast` class and some relevant methods such as `StatsForecast.plot`, `StatsForecast.forecast` and `StatsForecast.cross_validation` in other. The text in this article is largely taken from: 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4\. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). ## Table of Contents * [Introduction](#introduction) * [TSB](#model) * [Loading libraries and data](#loading) * [Explore data with the plot method](#plotting) * [Split the data into training and testing](#splitting) * [Implementation of TSB with StatsForecast](#implementation) * [Cross-validation](#cross_validate) * [Model evaluation](#evaluate) * [References](#references) ## Introduction The Teunter-Syntetos-Babai (TSB) model is a model used in the field of inventory management and demand forecasting in time series. It was proposed by Teunter, Syntetos, and Babai in 2001 as an extension of Croston’s demand forecasting model. The TSB model is specifically used to forecast demand for products with intermittent demand characteristics, that is, products that experience periods of demand followed by periods of non-demand. It is designed to handle time series data with many zeros and variability in the intervals between non-null observations. The TSB model is based on two main components: the level model and the interval model. The level model estimates the level of demand when it occurs, while the interval model estimates the interval between demand occurrences. These two components combine to generate accurate forecasts of future demand. The TSB model has proven to be effective in intermittent demand forecasting and has been widely used in various industrial sectors. However, it is important to note that there are other models and approaches available for demand forecasting, and the choice of the appropriate model will depend on the specific characteristics of the data and the context in which it is applied. ## TSB Model TSB (Teunter, Syntetos and Babai) is a new method proposed in 2011, the method replace the demand interval by demand probability which is updated every period. The reason for this is the Croston’s method only update demand when it occur, however in real life there are plenty of cases with many zero demands, therefore, the result of forecast will be unsuitable for estimating the risk of obsolescence because of the outdated information. In TSB method, the $D_t$ represent the demand occurrence indicator for period $t$, so : If $D_t=0$, then $Z'_t=Z'_{t-1}$ $D_t=D'_{t-1}+\beta (0- D'_{t-1})$ Otherwise $Z'_t=Z'_{t-1}+\alpha(Z_t - Z'_{t-2})$ $D'_t=D'_{t-1}+\beta(1-D'_{t-1})$ Hence, the forecast is given by $Y'_t=D'_t \cdot Z'_t$ Where * $Y'_t:$ Average demand per period * $Z_t:$ Actual demand at period $t$ * $Z'_t:$ Time between two positive demand * $D'_t:$ Estimate probability of a demand occurrence at the end of period $t$ * $\alpha, \beta:$ Smoothing Constant, $0 \leq \alpha, \beta \leq 1$ ### TSB General Properties The Teunter-Syntetos-Babai (TSB) model for time series has the following properties: 1. Intermittent Demand Modelling: The TSB model is specifically designed to forecast intermittent demand, which is characterized by periods of non-demand followed by periods of demand. The model efficiently addresses this characteristic of demand. 2. Level and interval components: The TSB model is based on two main components: the level model and the interval model. The level model estimates the level of demand when it occurs, while the interval model estimates the interval between demand occurrences. 3. Handling data with many zeros: The TSB model can efficiently handle time series data with many zeros, which are common in intermittent demand. The model properly considers these zeros in the forecasting process. 4. Exponential Smoothing: The TSB model uses exponential smoothing methods to estimate demand levels and intervals between occurrences. Exponential smoothing is a widely used technique in time series forecasting. 5. Confidence interval estimation: The TSB model provides confidence interval estimates for the generated forecasts. This allows having a measure of the uncertainty associated with forecasts and facilitates decision making. 6. Simplicity and ease of implementation: The TSB model is relatively simple and easy to implement compared to other more complex approaches. It does not require sophisticated assumptions about the distribution of demand and can be applied in a practical way. Those are some of the fundamental properties of the Teunter-Syntetos-Babai model in the context of time series and intermittent demand forecasting. ## Loading libraries and data > **Tip** > > Statsforecast will be needed. To install, see > [instructions](../getting-started/installation.html). Next, we import plotting libraries and configure the plotting style. ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plt.style.use('grayscale') # fivethirtyeight grayscale classic plt.rcParams['lines.linewidth'] = 1.5 dark_style = { 'figure.facecolor': '#008080', # #212946 'axes.facecolor': '#008080', 'savefig.facecolor': '#008080', 'axes.grid': True, 'axes.grid.which': 'both', 'axes.spines.left': False, 'axes.spines.right': False, 'axes.spines.top': False, 'axes.spines.bottom': False, 'grid.color': '#000000', #2A3459 'grid.linewidth': '1', 'text.color': '0.9', 'axes.labelcolor': '0.9', 'xtick.color': '0.9', 'ytick.color': '0.9', 'font.size': 12 } plt.rcParams.update(dark_style) from pylab import rcParams rcParams['figure.figsize'] = (18,7) ``` ```python theme={null} import pandas as pd df=pd.read_csv("https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/intermittend_demand2") df.head() ``` | | date | sales | | - | ------------------- | ----- | | 0 | 2022-01-01 00:00:00 | 0 | | 1 | 2022-01-01 01:00:00 | 10 | | 2 | 2022-01-01 02:00:00 | 0 | | 3 | 2022-01-01 03:00:00 | 0 | | 4 | 2022-01-01 04:00:00 | 100 | The input to StatsForecast is always a data frame in long format with three columns: unique\_id, ds and y: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} df["unique_id"]="1" df.columns=["ds", "y", "unique_id"] df.head() ``` | | ds | y | unique\_id | | - | ------------------- | --- | ---------- | | 0 | 2022-01-01 00:00:00 | 0 | 1 | | 1 | 2022-01-01 01:00:00 | 10 | 1 | | 2 | 2022-01-01 02:00:00 | 0 | 1 | | 3 | 2022-01-01 03:00:00 | 0 | 1 | | 4 | 2022-01-01 04:00:00 | 100 | 1 | ```python theme={null} print(df.dtypes) ``` ```text theme={null} ds object y int64 unique_id object dtype: object ``` We can see that our time variable `(ds)` is in an object format, we need to convert to a date format ```python theme={null} df["ds"] = pd.to_datetime(df["ds"]) ``` ## Explore Data with the plot method Plot some series using the plot method from the StatsForecast class. This method prints a random series from the dataset and is useful for basic EDA. ```python theme={null} from statsforecast import StatsForecast StatsForecast.plot(df) ``` ### Autocorrelation plots Autocorrelation (ACF) and partial autocorrelation (PACF) plots are statistical tools used to analyze time series. ACF charts show the correlation between the values of a time series and their lagged values, while PACF charts show the correlation between the values of a time series and their lagged values, after the effect of previous lagged values has been removed. ACF and PACF charts can be used to identify the structure of a time series, which can be helpful in choosing a suitable model for the time series. For example, if the ACF chart shows a repeating peak and valley pattern, this indicates that the time series is stationary, meaning that it has the same statistical properties over time. If the PACF chart shows a pattern of rapidly decreasing spikes, this indicates that the time series is invertible, meaning it can be reversed to get a stationary time series. The importance of the ACF and PACF charts is that they can help analysts better understand the structure of a time series. This understanding can be helpful in choosing a suitable model for the time series, which can improve the ability to predict future values of the time series. To analyze ACF and PACF charts: * Look for patterns in charts. Common patterns include repeating peaks and valleys, sawtooth patterns, and plateau patterns. * Compare ACF and PACF charts. The PACF chart generally has fewer spikes than the ACF chart. * Consider the length of the time series. ACF and PACF charts for longer time series will have more spikes. * Use a confidence interval. The ACF and PACF plots also show confidence intervals for the autocorrelation values. If an autocorrelation value is outside the confidence interval, it is likely to be significant. ```python theme={null} fig, axs = plt.subplots(nrows=1, ncols=2) plot_acf(df["y"], lags=30, ax=axs[0],color="fuchsia") axs[0].set_title("Autocorrelation"); plot_pacf(df["y"], lags=30, ax=axs[1],color="lime") axs[1].set_title('Partial Autocorrelation') plt.show(); ``` ### Decomposition of the time series How to decompose a time series and why? In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are: * **Level:** This is the primary value that averages over time. * **Trend:** The trend is the value that causes increasing or decreasing patterns in a time series. * **Seasonality:** This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series. * **Residual/Noise:** These are the random variations in the time series. Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values. If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series. The combination of the components in time series can be of two types: \* Additive \* Multiplicative ### Additive time series If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: $y(t) = level + Trend + seasonality + noise$ ### Multiplicative time series If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as. $y(t) = Level * Trend * seasonality * Noise$ ```python theme={null} from plotly.subplots import make_subplots ``` ```python theme={null} from statsmodels.tsa.seasonal import seasonal_decompose from plotly.subplots import make_subplots import plotly.graph_objects as go def plotSeasonalDecompose( x, model='additive', filt=None, period=None, two_sided=True, extrapolate_trend=0, title="Seasonal Decomposition"): result = seasonal_decompose( x, model=model, filt=filt, period=period, two_sided=two_sided, extrapolate_trend=extrapolate_trend) fig = make_subplots( rows=4, cols=1, subplot_titles=["Observed", "Trend", "Seasonal", "Residuals"]) for idx, col in enumerate(['observed', 'trend', 'seasonal', 'resid']): fig.add_trace( go.Scatter(x=result.observed.index, y=getattr(result, col), mode='lines'), row=idx+1, col=1, ) return fig ``` ```python theme={null} plotSeasonalDecompose( df["y"], model="additive", period=24, title="Seasonal Decomposition") ``` ## Split the data into training and testing Let’s divide our data into sets 1. Data to train our `TSB Model`. 2. Data to test our model For the test data we will use the last 500 Hours to test and evaluate the performance of our model. ```python theme={null} train = df[df.ds<='2023-01-31 19:00:00'] test = df[df.ds>'2023-01-31 19:00:00'] ``` ```python theme={null} train.shape, test.shape ``` ```text theme={null} ((9500, 3), (500, 3)) ``` ## Implementation of `TSB Model` with StatsForecast ### Load libraries ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import TSB ``` ### Building Model Import and instantiate the models. Setting the argument is sometimes tricky. This article on [Seasonal periods](https://robjhyndman.com/hyndsight/seasonal-periods/) by the master, Rob Hyndmann, can be useful for `season_length`. ```python theme={null} season_length = 24 # Hourly data horizon = len(test) # number of predictions models = [TSB(alpha_d=0.8, alpha_p=0.9)] ``` We fit the models by instantiating a new StatsForecast object with the following parameters: models: a list of models. Select the models you want from models and import them. * `freq:` a string indicating the frequency of the data. (See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) * `n_jobs:` n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model:` a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast(models=models, freq='h') ``` ### Fit the Model ```python theme={null} sf.fit(df=train) ``` ```text theme={null} StatsForecast(models=[TSB]) ``` Let’s see the results of our `TSB Model`. We can observe it with the following instruction: ```python theme={null} result=sf.fitted_[0,0].model_ result ``` ```text theme={null} {'mean': array([65.58645721]), 'fitted': array([ nan, 0. , 9. , ..., 14.937817 , 1.4937816 , 0.14937817], dtype=float32), 'sigma': np.float32(63.87893)} ``` ### Forecast Method If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The forecast method takes two arguments: forecasts next `h` (horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 500 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. ```python theme={null} Y_hat = sf.forecast(df=train, h=horizon) Y_hat ``` | | unique\_id | ds | TSB | | --- | ---------- | ------------------- | --------- | | 0 | 1 | 2023-01-31 20:00:00 | 65.586456 | | 1 | 1 | 2023-01-31 21:00:00 | 65.586456 | | 2 | 1 | 2023-01-31 22:00:00 | 65.586456 | | ... | ... | ... | ... | | 497 | 1 | 2023-02-21 13:00:00 | 65.586456 | | 498 | 1 | 2023-02-21 14:00:00 | 65.586456 | | 499 | 1 | 2023-02-21 15:00:00 | 65.586456 | ```python theme={null} sf.plot(train, Y_hat) ``` ### Predict method with confidence interval To generate forecasts use the predict method. The predict method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h (int):` represents the forecast h steps into the future. In this case, 500 hours ahead. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecast_df = sf.predict(h=horizon) forecast_df ``` | | unique\_id | ds | TSB | | --- | ---------- | ------------------- | --------- | | 0 | 1 | 2023-01-31 20:00:00 | 65.586456 | | 1 | 1 | 2023-01-31 21:00:00 | 65.586456 | | 2 | 1 | 2023-01-31 22:00:00 | 65.586456 | | ... | ... | ... | ... | | 497 | 1 | 2023-02-21 13:00:00 | 65.586456 | | 498 | 1 | 2023-02-21 14:00:00 | 65.586456 | | 499 | 1 | 2023-02-21 15:00:00 | 65.586456 | ## Cross-validation In previous steps, we’ve taken our historical data to predict the future. However, to asses its accuracy we would also like to know how the model would have performed in the past. To assess the accuracy and robustness of your models on your data perform Cross-Validation. With time series data, Cross Validation is done by defining a sliding window across the historical data and predicting the period following it. This form of cross-validation allows us to arrive at a better estimation of our model’s predictive abilities across a wider range of temporal instances while also keeping the data in the training set contiguous as is required by our models. The following graph depicts such a Cross Validation Strategy: ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) ### Perform time series cross-validation Cross-validation of time series models is considered a best practice but most implementations are very slow. The statsforecast library implements cross-validation as a distributed operation, making the process less time-consuming to perform. If you have big datasets you can also perform Cross Validation in a distributed cluster using Ray, Dask or Spark. In this case, we want to evaluate the performance of each model for the last 5 months `(n_windows=)`, forecasting every second months `(step_size=50)`. Depending on your computer, this step should take around 1 min. The cross\_validation method from the StatsForecast class takes the following arguments. * `df:` training data frame * `h (int):` represents h steps into the future that are being forecasted. In this case, 500 hours ahead. * `step_size (int):` step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows(int):` number of windows used for cross validation. In other words: what number of forecasting processes in the past do you want to evaluate. ```python theme={null} crossvalidation_df = sf.cross_validation(df=df, h=horizon, step_size=50, n_windows=5) ``` The crossvaldation\_df object is a new data frame that includes the following columns: * `unique_id:` series identifier * `ds:` datestamp or temporal index * `cutoff:` the last datestamp or temporal index for the `n_windows`. * `y:` true value * `model:` columns with the model’s name and fitted value. ```python theme={null} crossvalidation_df ``` | | unique\_id | ds | cutoff | y | TSB | | ---- | ---------- | ------------------- | ------------------- | ---- | --------- | | 0 | 1 | 2023-01-23 12:00:00 | 2023-01-23 11:00:00 | 0.0 | 0.000005 | | 1 | 1 | 2023-01-23 13:00:00 | 2023-01-23 11:00:00 | 0.0 | 0.000005 | | 2 | 1 | 2023-01-23 14:00:00 | 2023-01-23 11:00:00 | 0.0 | 0.000005 | | ... | ... | ... | ... | ... | ... | | 2497 | 1 | 2023-02-21 13:00:00 | 2023-01-31 19:00:00 | 60.0 | 65.586456 | | 2498 | 1 | 2023-02-21 14:00:00 | 2023-01-31 19:00:00 | 20.0 | 65.586456 | | 2499 | 1 | 2023-02-21 15:00:00 | 2023-01-31 19:00:00 | 20.0 | 65.586456 | ## Model Evaluation Now we are going to evaluate our model with the results of the predictions, we will use different types of metrics MAE, MAPE, MASE, RMSE, SMAPE to evaluate the accuracy. ```python theme={null} from functools import partial import utilsforecast.losses as ufl from utilsforecast.evaluation import evaluate ``` ```python theme={null} evaluate( test.merge(Y_hat), metrics=[ufl.mae, ufl.mape, partial(ufl.mase, seasonality=season_length), ufl.rmse, ufl.smape], train_df=train, ) ``` | | unique\_id | metric | TSB | | - | ---------- | ------ | --------- | | 0 | 1 | mae | 55.584594 | | 1 | 1 | mape | 1.177129 | | 2 | 1 | mase | 1.326048 | | 3 | 1 | rmse | 60.884468 | | 4 | 1 | smape | 0.740778 | ## References 1. [Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.](https://link.springer.com/book/10.1007/978-3-031-13584-2) 2. Ivan Svetunkov. [Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)](https://openforecast.org/adam/) 3. [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) 4. [Nixtla TSB API](../../src/core/models.html#tsb) 5. [Pandas available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). 6. [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting Principles and Practice (3rd ed)”](https://otexts.com/fpp3/tscv.html). 7. [Seasonal periods- Rob J Hyndman](https://robjhyndman.com/hyndsight/seasonal-periods/). # Anomaly Detection Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/anomalydetection.html > In this notebook, we’ll implement anomaly detection in time series > data > **Prerequisites** > > This tutorial assumes basic familiarity with StatsForecast. For a > minimal example visit the [Quick > Start](../getting-started/getting_started_short.html) ## Introduction Anomaly detection is a crucial task in time series forecasting. It involves identifying unusual observations that don’t follow the expected dataset patterns. Anomalies, also known as outliers, can be caused by a variety of factors, such as errors in the data collection process, sudden changes in the underlying patterns of the data, or unexpected events. They can pose problems for many forecasting models since they can distort trends, seasonal patterns, or autocorrelation estimates. As a result, anomalies can have a significant impact on the accuracy of the forecasts, and for this reason, it is essential to be able to identify them. Furthermore, anomaly detection has many applications across different industries, such as detecting fraud in financial data, monitoring the performance of online services, or identifying usual patterns in energy usage. By the end of this tutorial, you’ll have a good understanding of how to detect anomalies in time series data using [StatsForecast](../../index.html)’s probabilistic models. **Outline:** 1. Install libraries 2. Load and explore data 3. Train model 4. Recover insample forecasts and identify anomalies > **Important** > > Once an anomaly has been identified, we must decide what to do with > it. For example, we could remove it or replace it with another value. > The correct course of action is context-dependent and beyond this > notebook’s scope. Removing an anomaly will likely improve the accuracy > of the forecast, but it can also underestimate the amount of > randomness in the data. > **Tip** > > You can use Colab to run this Notebook interactively > >
> Open In Colab > ## Install libraries We assume that you have StatsForecast already installed. If not, check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html) Install the necessary packages using `pip install statsforecast` ```python theme={null} pip install statsforecast -U ``` ## Load and explore the data For this example, we’ll use the hourly dataset of the [M4 Competition](https://www.sciencedirect.com/science/article/pii/S0169207019301128). ```python theme={null} import pandas as pd ``` ```python theme={null} df_total = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') df_total.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | The input to StatsForecast is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`. * `unique_id`: (string, int or category) A unique identifier for the series. * `ds`: (timestamp or int) A timestamp in format YYYY-MM-DD or YYYY-MM-DD HH:MM:SS or an integer indexing time. * `y`: (numeric) The measurement we wish to forecast. From this dataset, we’ll select the first 8 time series to reduce the total execution time. You can select any number you want by changing the value of `n_series`. ```python theme={null} n_series = 8 uids = df_total['unique_id'].unique()[:n_series] df = df_total.query('unique_id in @uids') ``` We can plot these series using the `plot_series` function from the `utilsforecast` package. This function has multiple parameters, and the required ones to generate the plots in this notebook are explained below. * `df`: A pandas dataframe with columns \[unique\_id, ds, y]. * `forecasts_df`: A pandas dataframe with columns \[unique\_id, ds] and models. * `ids`: A list with the ids of the time series we want to plot. * `level`: Prediction interval levels to plot. * `plot_anomalies`: Whether or not to include the anomalies for each prediction interval. ```python theme={null} from statsforecast import StatsForecast from utilsforecast.plotting import plot_series ``` ```python theme={null} plot_series(df) ``` ## Train model To generate the forecast, we’ll use the [MSTL](../../src/core/models.html#multipleseasonaltrend) model, which is well-suited for low-frequency data like the one used here. We first need to import it from `statsforecast.models` and then we need to instantiate it. Since we’re using hourly data, we have two seasonal periods: one every 24 hours (hourly) and one every 24\*7 hours (daily). Hence, we need to set `season_length = [24, 24*7]`. ```python theme={null} from statsforecast.models import MSTL ``` ```python theme={null} # Create a list of models and instantiation parameters models = [MSTL(season_length = [24, 24*7])] ``` To instantiate a new StatsForecast object, we need the following parameters: * `models`: The list of models defined in the previous step. * `freq`: A string or integer indicating the frequency of the data. See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). * `n_jobs`: An integer that indicates the number of jobs used in parallel processing. Use -1 to select all cores. ```python theme={null} sf = StatsForecast( models=models, freq=1, n_jobs=-1, ) ``` We’ll now predict the next 48 hours. To do this, we’ll use the `forecast` method, which requires the following arguments: * `df`: The dataframe with the training data. * `h`: The forecasting horizon. * `level`: The confidence levels of the prediction intervals. * `fitted`: Return insample predictions. It is important that we select a `level` and set `fitted=True` since we’ll need the insample forecasts and their prediction intervals to detect the anomalies. ```python theme={null} horizon = 48 levels = [99] fcst = sf.forecast(df=df, h=48, level=levels, fitted=True) fcst.head() ``` | | unique\_id | ds | MSTL | MSTL-lo-99 | MSTL-hi-99 | | - | ---------- | --- | ---------- | ---------- | ---------- | | 0 | H1 | 749 | 607.607223 | 587.173250 | 628.041196 | | 1 | H1 | 750 | 552.364253 | 521.069710 | 583.658796 | | 2 | H1 | 751 | 506.785334 | 465.894977 | 547.675691 | | 3 | H1 | 752 | 472.906141 | 423.114088 | 522.698195 | | 4 | H1 | 753 | 452.240231 | 394.064394 | 510.416067 | We can plot the forecasts using the `plot_series` function from before. ```python theme={null} plot_series(df, fcst) ``` ## Recover insample forecasts and identify anomalies In this example, an **anomaly** will be any observation outside the prediction interval of the insample forecasts for a given confidence level (here we selected 99%). Hence, we first need to recover the insample forecasts using the `forecast_fitted_values` method. ```python theme={null} insample_forecasts = sf.forecast_fitted_values() insample_forecasts.head() ``` | | unique\_id | ds | y | MSTL | MSTL-lo-99 | MSTL-hi-99 | | - | ---------- | -- | ----- | ---------- | ---------- | ---------- | | 0 | H1 | 1 | 605.0 | 605.098607 | 584.678408 | 625.518805 | | 1 | H1 | 2 | 586.0 | 588.496673 | 568.076474 | 608.916872 | | 2 | H1 | 3 | 586.0 | 585.586856 | 565.166657 | 606.007054 | | 3 | H1 | 4 | 559.0 | 554.012377 | 533.592178 | 574.432576 | | 4 | H1 | 5 | 511.0 | 510.153508 | 489.733309 | 530.573707 | We can now find all the observations above or below the 99% prediction interval for the insample forecasts. ```python theme={null} anomalies = insample_forecasts[~insample_forecasts['y'].between(insample_forecasts['MSTL-lo-99'], insample_forecasts['MSTL-hi-99'])] anomalies.head() ``` | | unique\_id | ds | y | MSTL | MSTL-lo-99 | MSTL-hi-99 | | --- | ---------- | --- | ----- | ---------- | ---------- | ---------- | | 42 | H1 | 43 | 613.0 | 649.404871 | 628.984672 | 669.825069 | | 47 | H1 | 48 | 683.0 | 662.245526 | 641.825328 | 682.665725 | | 48 | H1 | 49 | 687.0 | 655.382320 | 634.962122 | 675.802519 | | 100 | H1 | 101 | 507.0 | 484.934230 | 464.514031 | 505.354428 | | 110 | H1 | 111 | 451.0 | 474.899006 | 454.478808 | 495.319205 | We can plot the anomalies by setting the `level` and the `plot_anomalies` arguments of the `plot_series` function. ```python theme={null} plot_series(forecasts_df=insample_forecasts, level=levels, plot_anomalies=True) ``` If we want to take a closer look, we can use the `ids` argument to select one particular time series, for example, `H10`. ```python theme={null} plot_series(forecasts_df=insample_forecasts, level=[99], plot_anomalies=True, ids=['H10']) ``` Here we identified the anomalies in the data using the MSTL model, but any [probabilistic model](../../src/core/models.html) from StatsForecast can be used. We also selected the 99% prediction interval of the insample forecasts, but other confidence levels can be used as well. # Conformal Prediction Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/conformalprediction.html > Learn how to generate calibrated prediction intervals for any > forecasting model using conformal prediction, a distribution-free > method for uncertainty quantification in Python. ## What You’ll Learn In this tutorial, you’ll discover how to: * Generate **calibrated prediction intervals** without distributional assumptions * Apply conformal prediction to any forecasting model in Python * Implement uncertainty quantification with StatsForecast’s `ConformalIntervals` * Compare conformal prediction with traditional uncertainty methods * Evaluate prediction interval coverage and calibration ## Prerequisites This tutorial assumes basic familiarity with StatsForecast. For a minimal example visit the [Quick Start](../getting-started/getting_started_short.html) ## What is Conformal Prediction? **Conformal prediction** is a distribution-free framework for generating prediction intervals with guaranteed coverage properties. Unlike traditional methods that assume normally distributed errors, conformal prediction works with any forecasting model and provides **well-calibrated uncertainty estimates** without making distributional assumptions. ### Why Use Conformal Prediction for Time Series? When generating forecasts, a point forecast alone doesn’t convey uncertainty. **Prediction intervals** quantify this uncertainty by providing a range of values where future observations are likely to fall. A properly calibrated 95% prediction interval should contain the actual value 95% of the time. The challenge: many forecasting models either don’t provide prediction intervals, or generate intervals that are poorly calibrated. Traditional statistical methods also assume normality, which often doesn’t hold in practice. **Conformal prediction solves this by:** * Working with any forecasting model (model-agnostic) * Requiring no distributional assumptions * Using cross-validation to generate calibrated intervals * Providing theoretical coverage guarantees * Treating the forecasting model as a black box ### Conformal Prediction vs. Traditional Methods | Method | Distributional Assumption | Model-Agnostic | Calibration Guarantee | | ------------------------------- | ------------------------- | -------------- | --------------------- | | **Conformal Prediction** | None | ✓ | ✓ | | Bootstrap | Parametric assumptions | ✓ | \~ | | Quantile Regression | None | ✓ | \~ | | Statistical Models (ARIMA, ETS) | Normal errors | ✗ | \~ | For a video introduction, see the [PyData Seattle presentation](https://www.youtube.com/watch?v=Bj1U-Rrxk48). More resources available in [Valery Manokhin’s curated list](https://github.com/valeman/awesome-conformal-prediction). ## Models with Native Prediction Intervals For models that already provide forecast distributions (like AutoARIMA, AutoETS), check [Prediction Intervals](./uncertaintyintervals.html). Conformal prediction is particularly useful for models that only produce point forecasts, or when you want distribution-free intervals. > **Tip** > > StatsForecast also includes > [`ConformalSeasonalPool`](../models/conformalseasonalpool.html), a > training-free seasonal model whose prediction intervals are *natively* > conformal. ## How Conformal Prediction Works Conformal prediction uses **cross-validation** to generate prediction intervals: 1. **Split the training data** into multiple windows 2. **Train the model** on each window and forecast the next period 3. **Calculate residuals** (prediction errors) on the held-out data 4. **Construct intervals** using the distribution of these residuals The key insight: by studying how the model performs on historical data through cross-validation, we can quantify uncertainty for future predictions without assuming any particular error distribution. ### Real-World Applications Conformal prediction is particularly valuable for: * **Demand forecasting**: Inventory planning with quantified uncertainty * **Energy prediction**: Load forecasting with reliable confidence bounds * **Financial forecasting**: Risk management with calibrated intervals * **Production models**: Any black-box forecasting model requiring uncertainty quantification [StatsForecast](../../index.html) implements conformal prediction for all available models, making it easy to add calibrated prediction intervals to any forecasting pipeline. ## Install libraries We assume that you have StatsForecast already installed. If not, check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html) Install the necessary packages using `pip install statsforecast` ```python theme={null} %%capture pip install statsforecast -U ``` ## Load and explore the data For this example, we’ll use the hourly dataset from the [M4 Competition](https://www.sciencedirect.com/science/article/pii/S0169207019301128). We first need to download the data from a URL and then load it as a `pandas` dataframe. Notice that we’ll load the train and the test data separately. We’ll also rename the `y` column of the test data as `y_test`. ```python theme={null} import pandas as pd ``` ```python theme={null} train = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly.csv') test = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly-test.csv').rename(columns={'y': 'y_test'}) train.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | Since the goal of this notebook is to generate prediction intervals, we’ll only use the first 8 series of the dataset to reduce the total computational time. ```python theme={null} n_series = 8 uids = train['unique_id'].unique()[:n_series] # select first n_series of the dataset train = train.query('unique_id in @uids') test = test.query('unique_id in @uids') ``` We can plot these series using the `plot_series` function from the utilsforecast library. This function method has multiple parameters, and the required ones to generate the plots in this notebook are explained below. * `df`: A `pandas` dataframe with columns \[`unique_id`, `ds`, `y`]. * `forecasts_df`: A `pandas` dataframe with columns \[`unique_id`, `ds`] and models. * `plot_random`: bool = `True`. Plots the time series randomly. * `models`: List\[str]. A list with the models we want to plot. * `level`: List\[float]. A list with the prediction intervals we want to plot. * `engine`: str = `matplotlib`. It can also be `plotly`. `plotly` generates interactive plots, while `matplotlib` generates static plots. ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} plot_series(train, test, plot_random=False) ``` ## Implementing Conformal Prediction in Python StatsForecast makes it simple to add conformal prediction to any forecasting model. We’ll demonstrate with models that don’t natively provide prediction intervals: * **[SeasonalExponentialSmoothing](../../src/core/models.html#SimpleExponentialSmoothing)**: A simple smoothing model * **[ADIDA](../../src/core/models.html#adida)**: Aggregation method for intermittent demand * **[ARIMA](../../src/core/models.html#ARIMA)**: Traditional statistical model (to show distribution-free intervals) ### Setting Up Conformal Intervals The key is the `ConformalIntervals` class, which requires two parameters: * `h`: Forecast horizon (how many steps ahead to predict) * `n_windows`: Number of cross-validation windows for calibration ### Parameter Requirements * `n_windows * h` must be less than your time series length * `n_windows` should be at least 2 for reliable calibration * Larger `n_windows` improves calibration but increases computation time ```python theme={null} from statsforecast.models import SeasonalExponentialSmoothing, ADIDA, ARIMA from statsforecast.utils import ConformalIntervals # Create a list of models and instantiation parameters intervals = ConformalIntervals(h=24, n_windows=2) # P.S. n_windows*h should be less than the count of data elements in your time series sequence. # P.S. Also value of n_windows should be atleast 2 or more. models = [ SeasonalExponentialSmoothing(season_length=24, alpha=0.1, prediction_intervals=intervals), ADIDA(prediction_intervals=intervals), ARIMA(order=(24,0,12), prediction_intervals=intervals), ] ``` To instantiate a new StatsForecast object, we need the following parameters: * `df`: The dataframe with the training data. * `models`: The list of models defined in the previous step. * `freq`: A string indicating the frequency of the data. See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). * `n_jobs`: An integer that indicates the number of jobs used in parallel processing. Use -1 to select all cores. ```python theme={null} sf = StatsForecast(models=models, freq=1, n_jobs=-1) ``` ## Generating Forecasts with Prediction Intervals The `forecast` method generates both point forecasts and conformal prediction intervals: * `h`: Forecast horizon (number of steps ahead) * `level`: List of confidence levels (e.g., `[80, 90]` for 80% and 90% intervals) The output includes columns for each model’s forecast and corresponding prediction interval bounds (`model-lo-{level}`, `model-hi-{level}`). ```python theme={null} levels = [80, 90] # confidence levels of the prediction intervals forecasts = sf.forecast(df=train, h=24, level=levels) forecasts.head() ``` | | unique\_id | ds | SeasonalES | SeasonalES-lo-90 | SeasonalES-lo-80 | SeasonalES-hi-80 | SeasonalES-hi-90 | ADIDA | ADIDA-lo-90 | ADIDA-lo-80 | ADIDA-hi-80 | ADIDA-hi-90 | ARIMA | ARIMA-lo-90 | ARIMA-lo-80 | ARIMA-hi-80 | ARIMA-hi-90 | | - | ---------- | --- | ---------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------- | ----------- | ----------- | ----------- | ----------- | ---------- | ----------- | ----------- | ----------- | ----------- | | 0 | H1 | 701 | 624.132703 | 553.097423 | 556.359139 | 691.906266 | 695.167983 | 747.292568 | 599.519220 | 600.030467 | 894.554670 | 895.065916 | 618.078274 | 609.440076 | 610.583304 | 625.573243 | 626.716472 | | 1 | H1 | 702 | 555.698193 | 496.653559 | 506.833156 | 604.563231 | 614.742827 | 747.292568 | 491.669220 | 498.330467 | 996.254670 | 1002.915916 | 549.789291 | 510.464070 | 515.232352 | 584.346231 | 589.114513 | | 2 | H1 | 703 | 514.403029 | 462.673117 | 464.939840 | 563.866218 | 566.132941 | 747.292568 | 475.105038 | 475.793791 | 1018.791346 | 1019.480099 | 508.099925 | 496.574844 | 496.990264 | 519.209587 | 519.625007 | | 3 | H1 | 704 | 482.057899 | 433.030711 | 436.161413 | 527.954385 | 531.085087 | 747.292568 | 440.069220 | 440.130467 | 1054.454670 | 1054.515916 | 486.376622 | 471.141813 | 471.516997 | 501.236246 | 501.611431 | | 4 | H1 | 705 | 460.222522 | 414.270186 | 416.959492 | 503.485552 | 506.174858 | 747.292568 | 415.805038 | 416.193791 | 1078.391346 | 1078.780099 | 470.159478 | 445.162316 | 446.808608 | 493.510348 | 495.156640 | ## Visualizing Calibrated Prediction Intervals Let’s examine the prediction intervals for each model to understand their characteristics and calibration quality. ### SeasonalExponentialSmoothing: Well-Calibrated Intervals The conformal prediction intervals show proper nesting: the 80% interval is contained within the 90% interval, indicating well-calibrated uncertainty quantification. Even though this model only produces point forecasts, conformal prediction successfully generates meaningful prediction intervals. ```python theme={null} plot_series(train, forecasts, level=levels, ids=['H105'], models=['SeasonalES']) ``` ### ADIDA: Wider Intervals for Weaker Models Models with higher prediction errors produce wider conformal intervals. This is a feature, not a bug: the interval width honestly reflects the model’s uncertainty. A better-fitting model will produce narrower, more informative intervals. ```python theme={null} plot_series(train, forecasts, level=levels, ids=['H105'], models=['ADIDA']) ``` ### ARIMA: Distribution-Free Alternative ARIMA models typically provide prediction intervals assuming normally distributed errors. By using conformal prediction, we get distribution-free intervals that don’t rely on this assumption, which is valuable when the normality assumption is questionable. ```python theme={null} plot_series(train, forecasts, level=levels, ids=['H105'], models=['ARIMA']) ``` ## Alternative: Setting Conformal Intervals on StatsForecast Object You can apply conformal prediction to all models at once by specifying `prediction_intervals` in the `StatsForecast` object. This is convenient when you want the same conformal setup for multiple models. ```python theme={null} from statsforecast.models import SimpleExponentialSmoothing, ADIDA from statsforecast.utils import ConformalIntervals from statsforecast import StatsForecast models = [ SimpleExponentialSmoothing(alpha=0.1), ADIDA() ] res = StatsForecast( models=models, freq=1, ).forecast(df=train, h=24, prediction_intervals=ConformalIntervals(h=24, n_windows=2), level=[80]) res.head() ``` | | unique\_id | ds | SES | SES-lo-80 | SES-hi-80 | ADIDA | ADIDA-lo-80 | ADIDA-hi-80 | | - | ---------- | --- | ---------- | ---------- | ----------- | ---------- | ----------- | ----------- | | 0 | H1 | 701 | 742.669064 | 649.221405 | 836.116722 | 747.292568 | 600.030467 | 894.554670 | | 1 | H1 | 702 | 742.669064 | 550.551324 | 934.786804 | 747.292568 | 498.330467 | 996.254670 | | 2 | H1 | 703 | 742.669064 | 523.621405 | 961.716722 | 747.292568 | 475.793791 | 1018.791346 | | 3 | H1 | 704 | 742.669064 | 488.121405 | 997.216722 | 747.292568 | 440.130467 | 1054.454670 | | 4 | H1 | 705 | 742.669064 | 464.021405 | 1021.316722 | 747.292568 | 416.193791 | 1078.391346 | ## Future work Conformal prediction has become a powerful framework for uncertainty quantification, providing well-calibrated prediction intervals without making any distributional assumptions. Its use has surged in both academia and industry over the past few years. We’ll continue working on it, and future tutorials may include: * Exploring larger datasets * Incorporating industry-specific examples * Investigating specialized methods like the jackknife+ that are closely related to conformal prediction (for details on the jackknife+ see [here](https://valeman.medium.com/jackknife-a-swiss-knife-of-conformal-prediction-for-regression-ce3b56432f4f)) If you’re interested in any of these, or in any other related topic, please let us know by opening an issue on [GitHub](https://github.com/Nixtla/statsforecast/issues) ## Key Takeaways ### Summary: Conformal Prediction for Time Series * **Model-agnostic**: Works with any forecasting model in Python * **Distribution-free**: No normality assumptions required * **Well-calibrated**: Theoretical coverage guarantees * **Easy to implement**: Just add `ConformalIntervals` to your StatsForecast models * **Flexible**: Apply to individual models or all models at once **Next steps:** * Try conformal prediction on your own forecasting problems * Experiment with different `n_windows` values for optimal calibration * Compare with native prediction intervals from statistical models * Explore [advanced uncertainty quantification methods](./uncertaintyintervals.html) ## Acknowledgements We would like to thank [Kevin Kho](https://github.com/kvnkho) for writing this tutorial, and Valeriy [Manokhin](https://github.com/valeman) for his expertise on conformal prediction, as well as for promoting this work. ## References [Manokhin, Valery. (2022). Machine Learning for Probabilistic Prediction. 10.5281/zenodo.6727505.](https://zenodo.org/record/6727505) # Cross validation | StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/crossvalidation.html > In this example, we’ll implement time series cross-validation to > evaluate model’s performance. > **Prerequisites** > > This tutorial assumes basic familiarity with StatsForecast. For a > minimal example visit the [Quick > Start](../getting-started/getting_started_short.html) ## Introduction Time series cross-validation is a method for evaluating how a model would have performed in the past. It works by defining a sliding window across the historical data and predicting the period following it. ![](https://raw.githubusercontent.com/Nixtla/statsforecast/main/nbs/imgs/ChainedWindows.gif) [Statsforecast](../../src/core/core.html#statsforecast) has an implementation of time series cross-validation that is fast and easy to use. This implementation makes cross-validation a distributed operation, which makes it less time-consuming. In this notebook, we’ll use it on a subset of the [M4 Competition](https://www.sciencedirect.com/science/article/pii/S0169207019301128) hourly dataset. **Outline:** 1. Install libraries 2. Load and explore data 3. Train model 4. Perform time series cross-validation 5. Evaluate results > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Install libraries We assume that you have StatsForecast already installed. If not, check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html) Install the necessary packages with `pip install statsforecast` ```python theme={null} pip install statsforecast ``` ```python theme={null} import os os.environ['NIXTLA_ID_AS_COL'] = '1' from statsforecast import StatsForecast # required to instantiate StatsForecast object and use cross-validation method ``` ```text theme={null} /Users/nasaul/nixtla/statsforecast/.venv/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm ``` ## Load and explore the data As stated in the introduction, we’ll use the M4 Competition hourly dataset. We’ll first import the data from an URL using `pandas`. ```python theme={null} import pandas as pd ``` ```python theme={null} Y_df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') # load the data Y_df.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | The input to `StatsForecast` is a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and y: * The `unique_id` (string, int, or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestamp in format YYYY-MM-DD or YYYY-MM-DD HH:MM:SS. * The `y` (numeric) represents the measurement we wish to forecast. The data in this example already has this format, so no changes are needed. To keep the time required to execute this notebook to a minimum, we’ll only use one time series from the data, namely the one with `unique_id == 'H1'`. However, you can use as many as you want, with no additional changes to the code needed. ```python theme={null} df = Y_df[Y_df['unique_id'] == 'H1'] # select time series ``` We can plot the time series we’ll work with using `StatsForecast.plot` method. ```python theme={null} StatsForecast.plot(df) ``` ## Train model For this example, we’ll use StatsForecast [AutoETS](../../src/core/models.html#autoets). We first need to import it from `statsforecast.models` and then we need to instantiate a new `StatsForecast` object. The `StatsForecast` object has the following parameters: * models: a list of models. Select the models you want from [models](../../src/core/models.html) and import them. * freq: a string indicating the frequency of the data. See [panda’s available frequencies.](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases) * n\_jobs: int, number of jobs used in the parallel processing, use -1 for all cores. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame `df`. ```python theme={null} from statsforecast.models import AutoETS ``` ```python theme={null} models = [AutoETS(season_length = 24)] sf = StatsForecast( models = models, freq = 1, n_jobs = 1 ) ``` ## Perform time series cross-validation Once the `StatsForecast`object has been instantiated, we can use the `cross_validation` method, which takes the following arguments: * `df`: training data frame with `StatsForecast` format * `h` (int): represents the h steps into the future that will be forecasted * `step_size` (int): step size between each window, meaning how often do you want to run the forecasting process. * `n_windows` (int): number of windows used for cross-validation, meaning the number of forecasting processes in the past you want to evaluate. For this particular example, we’ll use 3 windows of 24 hours. ```python theme={null} cv_df = sf.cross_validation( df = df, h = 24, step_size = 24, n_windows = 3 ) ``` The `cv_df` object is a new data frame that includes the following columns: * `unique_id`: series identifier * `ds`: datestamp or temporal index * `cutoff`: the last datestamp or temporal index for the n\_windows. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_df.head() ``` | | unique\_id | ds | cutoff | y | AutoETS | | - | ---------- | --- | ------ | ----- | ---------- | | 0 | H1 | 677 | 676 | 691.0 | 677.761053 | | 1 | H1 | 678 | 676 | 618.0 | 607.817879 | | 2 | H1 | 679 | 676 | 563.0 | 569.437729 | | 3 | H1 | 680 | 676 | 529.0 | 537.340007 | | 4 | H1 | 681 | 676 | 504.0 | 515.571123 | We’ll now plot the forecast for each cutoff period. To make the plots clearer, we’ll rename the actual values in each period. ```python theme={null} from IPython.display import display ``` ```python theme={null} cv_df.rename(columns = {'y' : 'actual'}, inplace = True) # rename actual values cutoff = cv_df['cutoff'].unique() for k in range(len(cutoff)): cv = cv_df[cv_df['cutoff'] == cutoff[k]] display(StatsForecast.plot(df, cv.loc[:, cv.columns != 'cutoff'])) ``` Notice that in each cutoff period, we generated a forecast for the next 24 hours using only the data `y` before said period. ## Evaluate results We can now compute the accuracy of the forecast using an appropriate accuracy metric. Here we’ll use the [Root Mean Squared Error (RMSE).](https://en.wikipedia.org/wiki/Root-mean-square_deviation). ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import rmse ``` The function to compute the RMSE takes two arguments: 1. The actual values. 2. The forecasts, in this case, `AutoETS`. ```python theme={null} cv_rmse = evaluate(cv_df, metrics=[rmse], models=['AutoETS'], target_col='actual', agg_fn='mean')['AutoETS'] print(f"RMSE using cross-validation: {cv_rmse:.2f}") ``` ```text theme={null} RMSE using cross-validation: 32.21 ``` This measure should better reflect the predictive abilities of our model, since it used different time periods to test its accuracy. > **Tip** > > Cross validation is especially useful when comparing multiple models. > Here’s an [example](../getting-started/getting_started_complete.html) > with multiple models and time series. ## References [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, Time series cross-validation”](https://otexts.com/fpp3/tscv.html). # Electricity Load Forecast | StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/electricityloadforecasting.html > In this example we will show how to perform electricity load > forecasting considering a model capable of handling multiple > seasonalities (MSTL). Open In Colab ## Introduction Some time series are generated from very low frequency data. These data generally exhibit multiple seasonalities. For example, hourly data may exhibit repeated patterns every hour (every 24 observations) or every day (every 24 \* 7, hours per day, observations). This is the case for electricity load. Electricity load may vary hourly, e.g., during the evenings electricity consumption may be expected to increase. But also, the electricity load varies by week. Perhaps on weekends there is an increase in electrical activity. In this example we will show how to model the two seasonalities of the time series to generate accurate forecasts in a short time. We will use hourly PJM electricity load data. The original data can be found [here](https://github.com/jnagura/Energy-consumption-prediction-analysis). ## Libraries In this example we will use the following libraries: * `StatsForecast`. Lightning ⚡️ fast forecasting with statistical and econometric models. Includes the MSTL model for multiple seasonalities. * [`Prophet`](https://github.com/facebook/prophet). Benchmark model developed by Facebook. * [`NeuralProphet`](https://github.com/ourownstory/neural_prophet). Deep Learning version of `Prophet`. Used as benchark. ```python theme={null} # !pip install statsforecast "neuralprophet[live]" prophet ``` ## Forecast using Multiple Seasonalities ### Electricity Load Data According to the [dataset’s page](https://www.kaggle.com/datasets/robikscube/hourly-energy-consumption), > PJM Interconnection LLC (PJM) is a regional transmission organization > (RTO) in the United States. It is part of the Eastern Interconnection > grid operating an electric transmission system serving all or parts of > Delaware, Illinois, Indiana, Kentucky, Maryland, Michigan, New Jersey, > North Carolina, Ohio, Pennsylvania, Tennessee, Virginia, West > Virginia, and the District of Columbia. The hourly power consumption > data comes from PJM’s website and are in megawatts (MW). Let’s take a look to the data. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import pandas as pd from utilsforecast.plotting import plot_series ``` ```python theme={null} pd.plotting.register_matplotlib_converters() plt.rc("figure", figsize=(10, 8)) plt.rc("font", size=10) ``` ```python theme={null} df = pd.read_csv('https://raw.githubusercontent.com/panambY/Hourly_Energy_Consumption/master/data/PJM_Load_hourly.csv') df.columns = ['ds', 'y'] df.insert(0, 'unique_id', 'PJM_Load_hourly') df['ds'] = pd.to_datetime(df['ds']) df = df.sort_values(['unique_id', 'ds']).reset_index(drop=True) df.tail() ``` | | unique\_id | ds | y | | ----- | ----------------- | ------------------- | ------- | | 32891 | PJM\_Load\_hourly | 2001-12-31 20:00:00 | 36392.0 | | 32892 | PJM\_Load\_hourly | 2001-12-31 21:00:00 | 35082.0 | | 32893 | PJM\_Load\_hourly | 2001-12-31 22:00:00 | 33890.0 | | 32894 | PJM\_Load\_hourly | 2001-12-31 23:00:00 | 32590.0 | | 32895 | PJM\_Load\_hourly | 2002-01-01 00:00:00 | 31569.0 | ```python theme={null} plot_series(df) ``` We clearly observe that the time series exhibits seasonal patterns. Moreover, the time series contains `32,896` observations, so it is necessary to use very computationally efficient methods to display them in production. ### MSTL model The [MSTL](../../src/core/models.html#multipleseasonaltrend) (Multiple Seasonal-Trend decomposition using LOESS) model, originally developed by [Kasun Bandara, Rob J Hyndman and Christoph Bergmeir](https://arxiv.org/abs/2107.13462), decomposes the time series in multiple seasonalities using a Local Polynomial Regression (LOESS). Then it forecasts the trend using a custom non-seasonal model and each seasonality using a [SeasonalNaive](../../src/core/models.html#seasonalnaive) model. `StatsForecast` contains a fast implementation of the [MSTL](../../src/core/models.html#multipleseasonaltrend) model. Also, the decomposition of the time series can be calculated. ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import MSTL, AutoARIMA, SeasonalNaive from statsforecast.utils import AirPassengers as ap ``` First we must define the model parameters. As mentioned before, the electricity load presents seasonalities every 24 hours (Hourly) and every 24 \* 7 (Daily) hours. Therefore, we will use `[24, 24 * 7]` as the seasonalities that the [MSTL](../../src/core/models.html#multipleseasonaltrend) model receives. We must also specify the manner in which the trend will be forecasted. In this case we will use the [AutoARIMA](../../src/core/models.html#autoarima) model. ```python theme={null} mstl = MSTL( season_length=[24, 24 * 7], # seasonalities of the time series trend_forecaster=AutoARIMA() # model used to forecast trend ) ``` Once the model is instantiated, we have to instantiate the `StatsForecast` class to create forecasts. ```python theme={null} sf = StatsForecast( models=[mstl], # model used to fit each time series freq='h', # frequency of the data ) ``` #### Fit the model Afer that, we just have to use the `fit` method to fit each model to each time series. ```python theme={null} sf = sf.fit(df=df) ``` #### Decompose the time series in multiple seasonalities Once the model is fitted, we can access the decomposition using the `fitted_` attribute of `StatsForecast`. This attribute stores all relevant information of the fitted models for each of the time series. In this case we are fitting a single model for a single time series, so by accessing the fitted\_ location \[0, 0] we will find the relevant information of our model. The [MSTL](../../src/core/models.html#mstl) class generates a `model_` attribute that contains the way the series was decomposed. ```python theme={null} sf.fitted_[0, 0].model_ ``` | | data | trend | seasonal24 | seasonal168 | remainder | | ----- | ------- | ------------ | ------------ | ----------- | ------------ | | 0 | 22259.0 | 25899.808157 | -4720.213546 | 581.308595 | 498.096794 | | 1 | 21244.0 | 25900.349395 | -5433.168901 | 571.780657 | 205.038849 | | 2 | 20651.0 | 25900.875973 | -5829.135728 | 557.142643 | 22.117112 | | 3 | 20421.0 | 25901.387631 | -5704.092794 | 597.696957 | -373.991794 | | 4 | 20713.0 | 25901.884103 | -5023.324375 | 922.564854 | -1088.124582 | | ... | ... | ... | ... | ... | ... | | 32891 | 36392.0 | 33329.031577 | 4254.112720 | 917.258336 | -2108.402633 | | 32892 | 35082.0 | 33355.083576 | 3625.077164 | 721.689136 | -2619.849876 | | 32893 | 33890.0 | 33381.108409 | 2571.794472 | 549.661529 | -2612.564409 | | 32894 | 32590.0 | 33407.105839 | 796.356548 | 361.956280 | -1975.418667 | | 32895 | 31569.0 | 33433.075723 | -1260.860917 | 279.777069 | -882.991876 | Let’s look graphically at the different components of the time series. ```python theme={null} sf.fitted_[0, 0].model_.tail(24 * 28).plot(subplots=True, grid=True) plt.tight_layout() plt.show() ``` We observe that there is a clear trend towards the high (orange line). This component would be predicted with the [AutoARIMA](../../src/core/models.html#autoarima) model. We can also observe that every 24 hours and every `24 * 7` hours there is a very well defined pattern. These two components will be forecast separately using a [SeasonalNaive](../../src/core/models.html#seasonalnaive) model. #### Produce forecasts To generate forecasts we only have to use the `predict` method specifying the forecast horizon (`h`). In addition, to calculate prediction intervals associated to the forecasts, we can include the parameter `level` that receives a list of levels of the prediction intervals we want to build. In this case we will only calculate the 90% forecast interval (`level=[90]`). ```python theme={null} forecasts = sf.predict(h=24, level=[90]) forecasts.head() ``` | | unique\_id | ds | MSTL | MSTL-lo-90 | MSTL-hi-90 | | - | ----------------- | ------------------- | ------------ | ------------ | ------------ | | 0 | PJM\_Load\_hourly | 2002-01-01 01:00:00 | 30215.608163 | 29842.185622 | 30589.030705 | | 1 | PJM\_Load\_hourly | 2002-01-01 02:00:00 | 29447.209028 | 28787.123369 | 30107.294687 | | 2 | PJM\_Load\_hourly | 2002-01-01 03:00:00 | 29132.787603 | 28221.354454 | 30044.220751 | | 3 | PJM\_Load\_hourly | 2002-01-01 04:00:00 | 29126.254591 | 27992.821420 | 30259.687762 | | 4 | PJM\_Load\_hourly | 2002-01-01 05:00:00 | 29604.608674 | 28273.428663 | 30935.788686 | Let’s look at our forecasts graphically. ```python theme={null} plot_series(df, forecasts, level=[90], max_insample_length=24*7) ``` In the next section we will plot different models so it is convenient to reuse the previous code with the following function. ```python theme={null} def plot_forecasts(y_hist, y_true, y_pred, models): _, ax = plt.subplots(1, 1, figsize = (20, 7)) y_true = y_true.merge(y_pred, how='left', on=['unique_id', 'ds']) df_plot = pd.concat([y_hist, y_true]).set_index('ds').tail(24 * 7) df_plot[['y'] + models].plot(ax=ax, linewidth=2) colors = ['orange', 'green', 'red'] for model, color in zip(models, colors): ax.fill_between(df_plot.index, df_plot[f'{model}-lo-90'], df_plot[f'{model}-hi-90'], alpha=.35, color=color, label=f'{model}-level-90') ax.set_title('PJM Load Hourly', fontsize=22) ax.set_ylabel('Electricity Load', fontsize=20) ax.set_xlabel('Timestamp [t]', fontsize=20) ax.legend(prop={'size': 15}) ax.grid() ``` ### Performance of the MSTL model #### Split Train/Test sets To validate the accuracy of the `MSTL` model, we will show its performance on unseen data. We will use a classical time series technique that consists of dividing the data into a training set and a test set. We will leave the last 24 observations (the last day) as the test set. So the model will train on `32,872` observations. ```python theme={null} df_test = df.tail(24) df_train = df.drop(df_test.index) ``` #### MSTL model In addition to the `MSTL` model, we will include the [SeasonalNaive](../../src/core/models.html#seasonalnaive) model as a benchmark to validate the added value of the `MSTL` model. Including `StatsForecast` models is as simple as adding them to the list of models to be fitted. ```python theme={null} sf = StatsForecast( models=[mstl, SeasonalNaive(season_length=24)], # add SeasonalNaive model to the list freq='h' ) ``` To measure the fitting time we will use the `time` module. ```python theme={null} from time import time ``` To retrieve the forecasts of the test set we only have to do fit and predict as before. ```python theme={null} init = time() sf = sf.fit(df=df_train) forecasts_test = sf.predict(h=len(df_test), level=[90]) end = time() forecasts_test.head() ``` | | unique\_id | ds | MSTL | MSTL-lo-90 | MSTL-hi-90 | SeasonalNaive | SeasonalNaive-lo-90 | SeasonalNaive-hi-90 | | - | ----------------- | ------------------- | ------------ | ------------ | ------------ | ------------- | ------------------- | ------------------- | | 0 | PJM\_Load\_hourly | 2001-12-31 01:00:00 | 29158.872180 | 28785.567875 | 29532.176486 | 28326.0 | 23468.555872 | 33183.444128 | | 1 | PJM\_Load\_hourly | 2001-12-31 02:00:00 | 28233.452263 | 27573.789089 | 28893.115438 | 27362.0 | 22504.555872 | 32219.444128 | | 2 | PJM\_Load\_hourly | 2001-12-31 03:00:00 | 27915.251368 | 27004.459000 | 28826.043736 | 27108.0 | 22250.555872 | 31965.444128 | | 3 | PJM\_Load\_hourly | 2001-12-31 04:00:00 | 27969.396560 | 26836.674164 | 29102.118956 | 26865.0 | 22007.555872 | 31722.444128 | | 4 | PJM\_Load\_hourly | 2001-12-31 05:00:00 | 28469.805588 | 27139.306401 | 29800.304775 | 26808.0 | 21950.555872 | 31665.444128 | ```python theme={null} time_mstl = (end - init) / 60 print(f'MSTL Time: {time_mstl:.2f} minutes') ``` ```text theme={null} MSTL Time: 0.46 minutes ``` Then we were able to generate forecasts for the next 24 hours. Now let’s look at the graphical comparison of the forecasts with the actual values. ```python theme={null} plot_series(df_train, df_test.merge(forecasts_test), level=[90], max_insample_length=24*7) ``` Let’s look at those produced only by `MSTL`. ```python theme={null} plot_series(df_train, df_test.merge(forecasts_test), level=[90], max_insample_length=24*7, models=['MSTL']) ``` We note that `MSTL` produces very accurate forecasts that follow the behavior of the time series. Now let us calculate numerically the accuracy of the model. We will use the following metrics: `MAE`, `MAPE`, `MASE`, `RMSE`, `SMAPE`. ```python theme={null} from functools import partial from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae, mape, mase, rmse, smape ``` ```python theme={null} eval_df = evaluate( df=df_test.merge(forecasts_test), train_df=df_train, metrics=[partial(mase, seasonality=24), mae, mape, rmse, smape], agg_fn='mean', ).set_index('metric').T eval_df ``` | metric | mase | mae | mape | rmse | smape | | ------------- | -------- | ----------- | -------- | ----------- | -------- | | MSTL | 0.587265 | 1219.321795 | 0.036052 | 1460.223279 | 0.017577 | | SeasonalNaive | 0.894653 | 1857.541667 | 0.056482 | 2201.384101 | 0.029343 | ```python theme={null} 1 - eval_df.loc['MSTL', 'mase'] / eval_df.loc['SeasonalNaive', 'mase'] ``` ```text theme={null} 0.3435830717111049 ``` We observe that `MSTL` has an improvement of about 35% over the `SeasonalNaive` method in the test set measured in `MASE`. #### Comparison with Prophet One of the most widely used models for time series forecasting is `Prophet`. This model is known for its ability to model different seasonalities (weekly, daily yearly). We will use this model as a benchmark to see if the `MSTL` adds value for this time series. ```python theme={null} from prophet import Prophet ``` ```python theme={null} # create prophet model prophet = Prophet(interval_width=0.9) init = time() prophet.fit(df_train) # produce forecasts future = prophet.make_future_dataframe(periods=len(df_test), freq='H', include_history=False) forecast_prophet = prophet.predict(future) end = time() # data wrangling forecast_prophet = forecast_prophet[['ds', 'yhat', 'yhat_lower', 'yhat_upper']] forecast_prophet.columns = ['ds', 'Prophet', 'Prophet-lo-90', 'Prophet-hi-90'] forecast_prophet.insert(0, 'unique_id', 'PJM_Load_hourly') forecast_prophet.head() ``` ```text theme={null} 16:56:47 - cmdstanpy - INFO - Chain [1] start processing 16:57:09 - cmdstanpy - INFO - Chain [1] done processing ``` | | unique\_id | ds | Prophet | Prophet-lo-90 | Prophet-hi-90 | | - | ----------------- | ------------------- | ------------ | ------------- | ------------- | | 0 | PJM\_Load\_hourly | 2001-12-31 01:00:00 | 25294.246960 | 20299.105766 | 30100.467618 | | 1 | PJM\_Load\_hourly | 2001-12-31 02:00:00 | 24000.725423 | 19285.395144 | 28777.495372 | | 2 | PJM\_Load\_hourly | 2001-12-31 03:00:00 | 23324.771966 | 18536.736306 | 28057.063589 | | 3 | PJM\_Load\_hourly | 2001-12-31 04:00:00 | 23332.519871 | 18591.879190 | 28720.461289 | | 4 | PJM\_Load\_hourly | 2001-12-31 05:00:00 | 24107.126827 | 18934.471254 | 29116.352931 | ```python theme={null} time_prophet = (end - init) / 60 print(f'Prophet Time: {time_prophet:.2f} minutes') ``` ```text theme={null} Prophet Time: 0.41 minutes ``` ```python theme={null} times = pd.DataFrame({'model': ['MSTL', 'Prophet'], 'time (mins)': [time_mstl, time_prophet]}) times ``` | | model | time (mins) | | - | ------- | ----------- | | 0 | MSTL | 0.455999 | | 1 | Prophet | 0.408726 | We observe that the time required for `Prophet` to perform the fit and predict pipeline is greater than `MSTL`. Let’s look at the forecasts produced by `Prophet`. ```python theme={null} forecasts_test = forecasts_test.merge(forecast_prophet, how='left', on=['unique_id', 'ds']) ``` ```python theme={null} plot_series(df_train, forecasts_test, max_insample_length=24*7, level=[90]) ``` We note that `Prophet` is able to capture the overall behavior of the time series. However, in some cases it produces forecasts well below the actual value. It also does not correctly adjust the valleys. ```python theme={null} eval_df = evaluate( df=df_test.merge(forecasts_test), train_df=df_train, metrics=[partial(mase, seasonality=24), mae, mape, rmse, smape], agg_fn='mean', ).set_index('metric').T eval_df ``` | metric | mase | mae | mape | rmse | smape | | ------------- | -------- | ----------- | -------- | ----------- | -------- | | MSTL | 0.587265 | 1219.321795 | 0.036052 | 1460.223279 | 0.017577 | | SeasonalNaive | 0.894653 | 1857.541667 | 0.056482 | 2201.384101 | 0.029343 | | Prophet | 1.099551 | 2282.966977 | 0.073750 | 2721.817203 | 0.038633 | ```python theme={null} 1 - eval_df.loc['MSTL', 'mase'] / eval_df.loc['Prophet', 'mase'] ``` ```text theme={null} 0.4659047602697266 ``` In terms of accuracy, `Prophet` is not able to produce better forecasts than the `SeasonalNaive` model, however, the `MSTL` model improves `Prophet`’s forecasts by 45% (`MASE`). #### Comparison with NeuralProphet `NeuralProphet` is the version of `Prophet` using deep learning. This model is also capable of handling different seasonalities so we will also use it as a benchmark. ```python theme={null} from neuralprophet import NeuralProphet ``` ```python theme={null} neuralprophet = NeuralProphet(quantiles=[0.05, 0.95]) init = time() neuralprophet.fit(df_train.drop(columns='unique_id')) future = neuralprophet.make_future_dataframe(df=df_train.drop(columns='unique_id'), periods=len(df_test)) forecast_np = neuralprophet.predict(future) end = time() forecast_np = forecast_np[['ds', 'yhat1', 'yhat1 5.0%', 'yhat1 95.0%']] forecast_np.columns = ['ds', 'NeuralProphet', 'NeuralProphet-lo-90', 'NeuralProphet-hi-90'] forecast_np.insert(0, 'unique_id', 'PJM_Load_hourly') forecast_np.head() ``` ```text theme={null} WARNING - (NP.forecaster.fit) - When Global modeling with local normalization, metrics are displayed in normalized scale. INFO - (NP.df_utils._infer_frequency) - Major frequency h corresponds to 99.973% of the data. INFO - (NP.df_utils._infer_frequency) - Dataframe freq automatically defined as h INFO - (NP.config.init_data_params) - Setting normalization to global as only one dataframe provided for training. INFO - (NP.config.set_auto_batch_epoch) - Auto-set batch_size to 128 INFO - (NP.config.set_auto_batch_epoch) - Auto-set epochs to 40 WARNING - (NP.config.set_lr_finder_args) - Learning rate finder: The number of batches (257) is too small than the required number for the learning rate finder (262). The results might not be optimal. INFO - (NP.df_utils._infer_frequency) - Major frequency h corresponds to 99.973% of the data. INFO - (NP.df_utils._infer_frequency) - Defined frequency is equal to major frequency - h INFO - (NP.df_utils.return_df_in_original_format) - Returning df with no ID column INFO - (NP.df_utils._infer_frequency) - Major frequency h corresponds to 95.833% of the data. INFO - (NP.df_utils._infer_frequency) - Defined frequency is equal to major frequency - h INFO - (NP.df_utils._infer_frequency) - Major frequency h corresponds to 95.833% of the data. INFO - (NP.df_utils._infer_frequency) - Defined frequency is equal to major frequency - h INFO - (NP.df_utils.return_df_in_original_format) - Returning df with no ID column ``` ```text theme={null} Training: | … ``` ```text theme={null} Finding best initial lr: 0%| | 0/262 [00:00 The forecasts graph shows that `NeuralProphet` generates very similar results to `Prophet`, as expected. ```python theme={null} eval_df = evaluate( df=df_test.merge(forecasts_test), train_df=df_train, metrics=[partial(mase, seasonality=24), mae, mape, rmse, smape], agg_fn='mean', ).set_index('metric').T eval_df ``` | metric | mase | mae | mape | rmse | smape | | ------------- | -------- | ----------- | -------- | ----------- | -------- | | MSTL | 0.587265 | 1219.321795 | 0.036052 | 1460.223279 | 0.017577 | | SeasonalNaive | 0.894653 | 1857.541667 | 0.056482 | 2201.384101 | 0.029343 | | Prophet | 1.099551 | 2282.966977 | 0.073750 | 2721.817203 | 0.038633 | | NeuralProphet | 1.061160 | 2203.255941 | 0.071060 | 2593.708496 | 0.037108 | ```python theme={null} 1 - eval_df.loc['MSTL', 'mase'] / eval_df.loc['NeuralProphet', 'mase'] ``` ```text theme={null} 0.4465818643911057 ``` With respect to numerical evaluation, `NeuralProphet` improves the results of `Prophet`, as expected, however, `MSTL` improves over `NeuralProphet`’s foreacasts by 44% (`MASE`). > **Important** > > The performance of `NeuralProphet` can be improved using > hyperparameter optimization, which can increase the fitting time > significantly. In this example we show its performance with the > default version. ## Conclusion In this post we introduced `MSTL`, a model originally developed by [Kasun Bandara, Rob Hyndman and Christoph Bergmeir](https://arxiv.org/abs/2107.13462) capable of handling time series with multiple seasonalities. We also showed that for the PJM electricity load time series offers better performance in time and accuracy than the `Prophet` and `NeuralProphet` models. ## References * [Bandara, Kasun & Hyndman, Rob & Bergmeir, Christoph. (2021). “MSTL: A Seasonal-Trend Decomposition Algorithm for Time Series with Multiple Seasonal Patterns”](https://arxiv.org/abs/2107.13462). # Detect Demand Peaks | StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/electricitypeakforecasting.html > In this example we will show how to perform electricity load > forecasting on the ERCOT (Texas) market for detecting daily peaks. ## Introduction Predicting peaks in different markets is useful. In the electricity market, consuming electricity at peak demand is penalized with higher tarifs. When an individual or company consumes electricity when its most demanded, regulators calls that a coincident peak (CP). In the Texas electricity market (ERCOT), the peak is the monthly 15-minute interval when the ERCOT Grid is at a point of highest capacity. The peak is caused by all consumers’ combined demand on the electrical grid. The coincident peak demand is an important factor used by ERCOT to determine final electricity consumption bills. ERCOT registers the CP demand of each client for 4 months, between June and September, and uses this to adjust electricity prices. Clients can therefore save on electricity bills by reducing the coincident peak demand. In this example we will train an `MSTL` (Multiple Seasonal-Trend decomposition using LOESS) model on historic load data to forecast day-ahead peaks on September 2022. Multiple seasonality is traditionally present in low sampled electricity data. Demand exhibits daily and weekly seasonality, with clear patterns for specific hours of the day such as 6:00pm vs 3:00am or for specific days such as Sunday vs Friday. First, we will load ERCOT historic demand, then we will use the `StatsForecast.cross_validation` method to fit the MSTL model and forecast daily load during September. Finally, we show how to use the forecasts to detect the coincident peak. **Outline** 1. Install libraries 2. Load and explore the data 3. Fit MSTL model and forecast 4. Peak detection > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Libraries We assume you have StatsForecast already installed. Check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html). Install the necessary packages using `pip install statsforecast` ## Load Data The input to StatsForecast is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestamp ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. ```python theme={null} import numpy as np import pandas as pd ``` ```python theme={null} Y_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/ERCOT-clean.csv', parse_dates=['ds']) ``` Plot the series using the `plot` method from the `StatsForecast` class. This method prints up to 8 random series from the dataset and is useful for basic EDA. > **Note** > > The `StatsForecast.plot` method uses Plotly as a default engine. You > can change to MatPlotLib by setting `engine="matplotlib"`. ```python theme={null} from statsforecast import StatsForecast ``` ```python theme={null} StatsForecast.plot(Y_df) ``` We observe that the time series exhibits seasonal patterns. Moreover, the time series contains `6,552` observations, so it is necessary to use computationally efficient methods to deploy them in production. ## Fit and Forecast MSTL model The MSTL (Multiple Seasonal-Trend decomposition using LOESS) model decomposes the time series in multiple seasonalities using a Local Polynomial Regression (LOESS). Then it forecasts the trend using a custom non-seasonal model and each seasonality using a SeasonalNaive model. > **Tip** > > Check our detailed explanation and tutorial on MSTL > [here](./multipleseasonalities.html) Import the `StatsForecast` class and the models you need. ```python theme={null} from sklearn.linear_model import LinearRegression from utilsforecast.feature_engineering import trend from statsforecast import StatsForecast from statsforecast.models import MSTL, SklearnModel ``` First, instantiate the model and define the parameters. The electricity load presents seasonalities every 24 hours (Hourly) and every 24 \* 7 (Daily) hours. Therefore, we will use `[24, 24 * 7]` as the seasonalities. See [this link](https://robjhyndman.com/hyndsight/seasonal-periods/) for a detailed explanation on how to set seasonal lengths. In this example we use the `SklearnModel` with a `LinearRegression` model for the trend component, however, any StatsForecast model can be used. The complete list of models is available [here](../../src/core/models.html). ```python theme={null} train, _ = trend(Y_df, freq='H') train.head() ``` | | unique\_id | ds | y | trend | | - | ---------- | ------------------- | ------------ | ----- | | 0 | ERCOT | 2021-01-01 00:00:00 | 43719.849616 | 1.0 | | 1 | ERCOT | 2021-01-01 01:00:00 | 43321.050347 | 2.0 | | 2 | ERCOT | 2021-01-01 02:00:00 | 43063.067063 | 3.0 | | 3 | ERCOT | 2021-01-01 03:00:00 | 43090.059203 | 4.0 | | 4 | ERCOT | 2021-01-01 04:00:00 | 43486.590073 | 5.0 | ```python theme={null} models = [ MSTL( season_length=[24, 24 * 7], # seasonalities of the time series trend_forecaster=SklearnModel(LinearRegression()) # model used to forecast trend ) ] ``` We fit the model by instantiating a `StatsForecast` object with the following required parameters: * `models`: a list of models. Select the models you want from [models](../../src/core/models.html) and import them. * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) ```python theme={null} # Instantiate StatsForecast class as sf sf = StatsForecast( models=models, freq='H', ) ``` > **Tip** > > StatsForecast also supports this optional parameter. > > * `n_jobs`: n\_jobs: int, number of jobs used in the parallel > processing, use -1 for all cores. (Default: 1) > > * `fallback_model`: a model to be used if a model fails. (Default: > none) The `cross_validation` method allows the user to simulate multiple historic forecasts, greatly simplifying pipelines by replacing for loops with `fit` and `predict` methods. This method re-trains the model and forecast each window. See [this tutorial](../getting-started/getting_started_complete.html) for an animation of how the windows are defined. Use the `cross_validation` method to produce all the daily forecasts for September. To produce daily forecasts set the forecasting horizon `h` as 24\. In this example we are simulating deploying the pipeline during September, so set the number of windows as 30 (one for each day). Finally, set the step size between windows as 24, to only produce one forecast per day. ```python theme={null} cv_df = sf.cross_validation( df=train, h=24, step_size=24, n_windows=30 ) ``` ```python theme={null} cv_df.head() ``` | | unique\_id | ds | cutoff | y | MSTL | | - | ---------- | ------------------- | ------------------- | ------------ | ------------ | | 0 | ERCOT | 2022-09-01 00:00:00 | 2022-08-31 23:00:00 | 45482.471757 | 47413.944185 | | 1 | ERCOT | 2022-09-01 01:00:00 | 2022-08-31 23:00:00 | 43602.658043 | 45237.153285 | | 2 | ERCOT | 2022-09-01 02:00:00 | 2022-08-31 23:00:00 | 42284.817342 | 43816.390019 | | 3 | ERCOT | 2022-09-01 03:00:00 | 2022-08-31 23:00:00 | 41663.156771 | 42972.956286 | | 4 | ERCOT | 2022-09-01 04:00:00 | 2022-08-31 23:00:00 | 41710.621904 | 42909.899438 | > **Important** > > When using `cross_validation` make sure the forecasts are produced at > the desired timestamps. Check the `cutoff` column which specifices the > last timestamp before the forecasting window. ## Peak Detection Finally, we use the forecasts in `cv_df` to detect the daily hourly demand peaks. For each day, we set the detected peaks as the highest forecasts. In this case, we want to predict one peak (`npeaks`); depending on your setting and goals, this parameter might change. For example, the number of peaks can correspond to how many hours a battery can be discharged to reduce demand. ```python theme={null} npeaks = 1 # Number of peaks ``` For the ERCOT 4CP detection task we are interested in correctly predicting the highest monthly load. Next, we filter the day in September with the highest hourly demand and predict the peak. ```python theme={null} cv_df = cv_df[['ds','y','MSTL']] max_day = cv_df.iloc[cv_df['y'].argmax()].ds.day # Day with maximum load cv_df_day = cv_df.query('ds.dt.day == @max_day') max_hour = cv_df_day['y'].argmax() peaks = cv_df_day['MSTL'].argsort().iloc[-npeaks:].values # Predicted peaks ``` In the following plot we see how the MSTL model is able to correctly detect the coincident peak for September 2022. ```python theme={null} import matplotlib.pyplot as plt ``` ```python theme={null} plt.figure(figsize=(10, 5)) plt.axvline(cv_df_day.iloc[max_hour]['ds'], color='black', label='True Peak') plt.scatter(cv_df_day.iloc[peaks]['ds'], cv_df_day.iloc[peaks]['MSTL'], color='green', label=f'Predicted Top-{npeaks}') plt.plot(cv_df_day['ds'], cv_df_day['y'], label='y', color='blue') plt.plot(cv_df_day['ds'], cv_df_day['MSTL'], label='Forecast', color='red') plt.xlabel('Time') plt.ylabel('Load (MW)') plt.grid() plt.legend() ``` > **Important** > > In this example we only include September. However, MSTL can correctly > predict the peaks for the 4 months of 2022. You can try this by > increasing the `nwindows` parameter of `cross_validation` or filtering > the `Y_df` dataset. The complete run for all months take only 10 > minutes. ## Next steps StatsForecast and MSTL in particular are good benchmarking models for peak detection. However, it might be useful to explore further and newer forecasting algorithms. We have seen particularly good results with the N-HiTS, a deep-learning model from Nixtla’s NeuralForecast library. Learn how to predict ERCOT demand peaks with our deep-learning N-HiTS model and the NeuralForecast library in [this tutorial](../../../neuralforecast/use-cases/electricitypeakforecasting.html). ## References * [Bandara, Kasun & Hyndman, Rob & Bergmeir, Christoph. (2021). “MSTL: A Seasonal-Trend Decomposition Algorithm for Time Series with Multiple Seasonal Patterns”](https://arxiv.org/abs/2107.13462). * [Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, Artur Dubrawski (2021). “N-HiTS: Neural Hierarchical Interpolation for Time Series Forecasting”. Accepted at AAAI 2023.](https://arxiv.org/abs/2201.12886) # Volatility forecasting (GARCH & ARCH) Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/garch_tutorial.html > In this example, we’ll forecast the volatility of the S\&P 500 and > several publicly traded companies using GARCH and ARCH models > **Prerequisites** > > This tutorial assumes basic familiarity with StatsForecast. For a > minimal example visit the [Quick > Start](../getting-started/getting_started_short.html) ## Introduction The Generalized Autoregressive Conditional Heteroskedasticity (GARCH) model is used for time series that exhibit non-constant volatility over time. Here volatility refers to the conditional standard deviation. The GARCH(p,q) model is given by where $v_t$ is independent and identically distributed with zero mean and unit variance, and $\sigma_t$ evolves according to The coefficients in the equation above must satisfy the following conditions: 1. $w>0$, $\alpha_i \geq 0$ for all $i$, and $\beta_j \geq 0$ for all $j$ 2. $\sum_{k=1}^{max(p,q)} \alpha_k + \beta_k < 1$. Here it is assumed that $\alpha_i=0$ for $i>p$ and $\beta_j=0$ for $j>q$. A particular case of the GARCH model is the ARCH model, in which $q=0$. Both models are commonly used in finance to model the volatility of stock prices, exchange rates, interest rates, and other financial instruments. They’re also used in risk management to estimate the probability of large variations in the price of financial assets. By the end of this tutorial, you’ll have a good understanding of how to implement a GARCH or an ARCH model in [StatsForecast](../../index.html) and how they can be used to analyze and predict financial time series data. **Outline:** 1. Install libraries 2. Load and explore the data 3. Train models 4. Perform time series cross-validation 5. Evaluate results 6. Forecast volatility > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Install libraries We assume that you have StatsForecast already installed. If not, check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html) Install the necessary packages using `pip install statsforecast` ```python theme={null} %%capture pip install statsforecast -U ``` ## Load and explore the data In this tutorial, we’ll use the last 5 years of prices from the S\&P 500 and several publicly traded companies. The data can be downloaded from Yahoo! Finance using [yfinance](https://github.com/ranaroussi/yfinance). To install it, use `pip install yfinance`. ```python theme={null} %%capture # pip install yfinance ``` We’ll also need `pandas` to deal with the dataframes. ```python theme={null} import os os.environ['NIXTLA_ID_AS_COL'] = '1' import yfinance as yf import pandas as pd ``` ```python theme={null} tickers = ['SPY', 'MSFT', 'AAPL', 'GOOG', 'AMZN', 'TSLA', 'NVDA', 'META', 'NKE', 'NFLX'] df = yf.download(tickers, start = '2018-01-01', end = '2022-12-31', interval='1mo', progress=False) # use monthly prices df.head() ``` | Price | Adj Close | | | | | | | | | | ... | Volume | | | | | | | | | | | ---------- | --------- | --------- | --------- | ---------- | --------- | ---------- | --------- | -------- | ---------- | --------- | --- | ---------- | ---------- | --------- | --------- | --------- | --------- | --------- | ----------- | ---------- | ---------- | | Ticker | AAPL | AMZN | GOOG | META | MSFT | NFLX | NKE | NVDA | SPY | TSLA | ... | AAPL | AMZN | GOOG | META | MSFT | NFLX | NKE | NVDA | SPY | TSLA | | Date | | | | | | | | | | | | | | | | | | | | | | | 2018-01-01 | 39.388084 | 72.544502 | 58.353695 | 186.328979 | 88.027702 | 270.299988 | 63.341862 | 6.078998 | 252.565216 | 23.620667 | ... | 2638717600 | 1927424000 | 574768000 | 495655700 | 574258400 | 238377600 | 157812200 | 11456216000 | 1985506700 | 1864072500 | | 2018-02-01 | 41.902908 | 75.622498 | 55.101181 | 177.784729 | 86.878807 | 291.380005 | 62.236938 | 5.985018 | 243.381882 | 22.870667 | ... | 3711577200 | 2755680000 | 847640000 | 516251600 | 725663300 | 184585800 | 160317000 | 14915528000 | 2923722000 | 1637850000 | | 2018-03-01 | 39.631344 | 72.366997 | 51.463116 | 159.310333 | 84.959763 | 295.350006 | 61.689133 | 5.731123 | 235.766373 | 17.742001 | ... | 2854910800 | 2608002000 | 907066000 | 996201700 | 750754800 | 263449400 | 174066700 | 14118440000 | 2323561800 | 2359027500 | | 2018-04-01 | 39.036106 | 78.306503 | 50.741886 | 171.483688 | 87.054207 | 312.459991 | 63.691761 | 5.565567 | 237.934006 | 19.593332 | ... | 2664617200 | 2598392000 | 834318000 | 750072700 | 668130700 | 262006000 | 158981900 | 11144008000 | 1998466500 | 2854662000 | | 2018-05-01 | 44.140598 | 81.481003 | 54.116600 | 191.204315 | 92.006393 | 351.600006 | 66.867508 | 6.240908 | 243.717957 | 18.982000 | ... | 2483905200 | 1432310000 | 636988000 | 401144100 | 509417900 | 142050800 | 129566300 | 11978240000 | 1606397200 | 2333671500 | The data downloaded includes different prices. We’ll use the [adjusted closing price](https://help.yahoo.com/kb/SLN28256.html#:~:text=Adjusted%20close%20is%20the%20closing,Security%20Prices%20\(CRSP\)%20standards.), which is the closing price after accounting for any corporate actions like stock splits or dividend distributions. It is also the price that is used to examine historical returns. Notice that the dataframe that `yfinance` returns has a [MultiIndex](https://pandas.pydata.org/docs/user_guide/advanced.html), so we need to select both the adjusted price and the tickers. ```python theme={null} df = df.loc[:, (['Adj Close'], tickers)] df.columns = df.columns.droplevel() # drop MultiIndex df = df.reset_index() df.head() ``` | Ticker | Date | SPY | MSFT | AAPL | GOOG | AMZN | TSLA | NVDA | META | NKE | NFLX | | ------ | ---------- | ---------- | --------- | --------- | --------- | --------- | --------- | -------- | ---------- | --------- | ---------- | | 0 | 2018-01-01 | 252.565216 | 88.027702 | 39.388084 | 58.353695 | 72.544502 | 23.620667 | 6.078998 | 186.328979 | 63.341862 | 270.299988 | | 1 | 2018-02-01 | 243.381882 | 86.878807 | 41.902908 | 55.101181 | 75.622498 | 22.870667 | 5.985018 | 177.784729 | 62.236938 | 291.380005 | | 2 | 2018-03-01 | 235.766373 | 84.959763 | 39.631344 | 51.463116 | 72.366997 | 17.742001 | 5.731123 | 159.310333 | 61.689133 | 295.350006 | | 3 | 2018-04-01 | 237.934006 | 87.054207 | 39.036106 | 50.741886 | 78.306503 | 19.593332 | 5.565567 | 171.483688 | 63.691761 | 312.459991 | | 4 | 2018-05-01 | 243.717957 | 92.006393 | 44.140598 | 54.116600 | 81.481003 | 18.982000 | 6.240908 | 191.204315 | 66.867508 | 351.600006 | The input to StatsForecast is a dataframe in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * `unique_id`: (string, int or category) A unique identifier for the series. * `ds`: (datestamp or int) A datestamp in format YYYY-MM-DD or YYYY-MM-DD HH:MM:SS or an integer indexing time. * `y`: (numeric) The measurement we wish to forecast. Hence, we need to reshape the data. We’ll do this by creating a new dataframe called `price`. ```python theme={null} prices = df.melt(id_vars = 'Date') prices = prices.rename(columns={'Date': 'ds', 'Ticker': 'unique_id', 'value': 'y'}) prices = prices[['unique_id', 'ds', 'y']] prices ``` | | unique\_id | ds | y | | --- | ---------- | ---------- | ---------- | | 0 | SPY | 2018-01-01 | 252.565216 | | 1 | SPY | 2018-02-01 | 243.381882 | | 2 | SPY | 2018-03-01 | 235.766373 | | 3 | SPY | 2018-04-01 | 237.934006 | | 4 | SPY | 2018-05-01 | 243.717957 | | ... | ... | ... | ... | | 595 | NFLX | 2022-08-01 | 223.559998 | | 596 | NFLX | 2022-09-01 | 235.440002 | | 597 | NFLX | 2022-10-01 | 291.880005 | | 598 | NFLX | 2022-11-01 | 305.529999 | | 599 | NFLX | 2022-12-01 | 294.880005 | We can plot this series using the `plot` method of the StatsForecast class. ```python theme={null} from statsforecast import StatsForecast ``` ```python theme={null} StatsForecast.plot(prices) ``` With the prices, we can compute the logarithmic returns of the S\&P 500 and the publicly traded companies. This is the variable we’re interested in since it’s likely to work well with the GARCH framework. The logarithmic return is given by $return_t = log \big( \frac{price_t}{price_{t-1}} \big)$ We’ll compute the returns on the price dataframe and then we’ll create a return dataframe with StatsForecast’s format. To do this, we’ll need `numpy`. ```python theme={null} import numpy as np prices['rt'] = prices['y'].div(prices.groupby('unique_id')['y'].shift(1)) prices['rt'] = np.log(prices['rt']) returns = prices[['unique_id', 'ds', 'rt']] returns = returns.rename(columns={'rt':'y'}) returns ``` | | unique\_id | ds | y | | --- | ---------- | ---------- | --------- | | 0 | SPY | 2018-01-01 | NaN | | 1 | SPY | 2018-02-01 | -0.037038 | | 2 | SPY | 2018-03-01 | -0.031790 | | 3 | SPY | 2018-04-01 | 0.009152 | | 4 | SPY | 2018-05-01 | 0.024018 | | ... | ... | ... | ... | | 595 | NFLX | 2022-08-01 | -0.005976 | | 596 | NFLX | 2022-09-01 | 0.051776 | | 597 | NFLX | 2022-10-01 | 0.214887 | | 598 | NFLX | 2022-11-01 | 0.045705 | | 599 | NFLX | 2022-12-01 | -0.035479 | > **Warning** > > If the order of the data is very small (say $<1e-5$), > `scipy.optimize.minimize` might not terminate successfully. In this > case, rescale the data and then generate the GARCH or ARCH model. ```python theme={null} StatsForecast.plot(returns) ``` From this plot, we can see that the returns seem suited for the GARCH framework, since large shocks *tend* to be followed by other large shocks. This doesn’t mean that after every large shock we should expect another one; merely that the probability of a large variance is greater than the probability of a small one. ## Train models We first need to import the [GARCH](../../src/core/models.html#GARCH.html) and the [ARCH](../../src/core/models.html#ARCH.html) models from `statsforecast.models`, and then we need to fit them by instantiating a new StatsForecast object. Notice that we’ll be using different values of $p$ and $q$. In the next section, we’ll determine which ones produce the most accurate model using cross-validation. We’ll also import the [Naive](../../src/core/models.html#naive) model since we’ll use it as a baseline. ```python theme={null} from statsforecast.models import ( GARCH, ARCH, Naive ) models = [ARCH(1), ARCH(2), GARCH(1,1), GARCH(1,2), GARCH(2,2), GARCH(2,1), Naive() ] ``` To instantiate a new StatsForecast object, we need the following parameters: * `df`: The dataframe with the training data. * `models`: The list of models defined in the previous step. * `freq`: A string indicating the frequency of the data. Here we’ll use **MS**, which correspond to the start of the month. You can see the list of panda’s available frequencies [here](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). * `n_jobs`: An integer that indicates the number of jobs used in parallel processing. Use -1 to select all cores. ```python theme={null} sf = StatsForecast( models = models, freq = 'MS', n_jobs = -1 ) ``` ## Perform time series cross-validation Time series cross-validation is a method for evaluating how a model would have performed in the past. It works by defining a sliding window across the historical data and predicting the period following it. Here we’ll use StatsForecast’s `cross-validation` method to determine the most accurate model for the S\&P 500 and the companies selected. This method takes the following arguments: * `df`: The dataframe with the training data. * `h` (int): represents the h steps into the future that will be forecasted. * `step_size` (int): step size between each window, meaning how often do you want to run the forecasting process. * `n_windows` (int): number of windows used for cross-validation, meaning the number of forecasting processes in the past you want to evaluate. For this particular example, we’ll use 4 windows of 3 months, or all the quarters in a year. ```python theme={null} cv_df = sf.cross_validation( df = returns, h = 3, step_size = 3, n_windows = 4 ) ``` The `cv_df` object is a dataframe with the following columns: * `unique_id`: series identifier. * `ds`: datestamp or temporal index * `cutoff`: the last datestamp or temporal index for the `n_windows`. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_df.rename(columns = {'y' : 'actual'}, inplace = True) cv_df.head() ``` | | unique\_id | ds | cutoff | actual | ARCH(1) | ARCH(2) | GARCH(1,1) | GARCH(1,2) | GARCH(2,2) | GARCH(2,1) | Naive | | - | ---------- | ---------- | ---------- | --------- | --------- | --------- | ---------- | ---------- | ---------- | ---------- | -------- | | 0 | AAPL | 2022-01-01 | 2021-12-01 | -0.015837 | 0.142421 | 0.144016 | 0.142954 | 0.141682 | 0.141682 | 0.144015 | 0.073061 | | 1 | AAPL | 2022-02-01 | 2021-12-01 | -0.056856 | -0.056893 | -0.057158 | -0.056388 | -0.058786 | -0.058785 | -0.057158 | 0.073061 | | 2 | AAPL | 2022-03-01 | 2021-12-01 | 0.057156 | -0.045901 | -0.046479 | -0.047513 | -0.045711 | -0.045711 | -0.046478 | 0.073061 | | 3 | AAPL | 2022-04-01 | 2022-03-01 | -0.102178 | 0.138650 | 0.140222 | 0.228138 | 0.136118 | 0.136132 | 0.140211 | 0.057156 | | 4 | AAPL | 2022-05-01 | 2022-03-01 | -0.057505 | -0.056007 | -0.056268 | -0.087833 | -0.057078 | -0.057085 | -0.056265 | 0.057156 | ```python theme={null} StatsForecast.plot(returns, cv_df.drop(['cutoff', 'actual'], axis=1)) ``` A tutorial on cross-validation can be found [here](./crossvalidation.html) ## Evaluate results To compute the accuracy of the forecasts, we’ll use the mean average error (mae), which is the sum of the absolute errors divided by the number of forecasts. ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae ``` The MAE needs to be computed for every window and then it needs to be averaged across all of them. To do this, we’ll create the following function. ```python theme={null} models = cv_df.columns.drop(['unique_id', 'ds', 'cutoff', 'actual']) ``` ```python theme={null} mae_cv = evaluate(cv_df, metrics=[mae], models=models, target_col='actual') mae_cv = mae_cv.drop(columns=['metric']).set_index('unique_id') mae_cv ``` | | ARCH(1) | ARCH(2) | GARCH(1,1) | GARCH(1,2) | GARCH(2,2) | GARCH(2,1) | Naive | | ---------- | -------- | -------- | ---------- | ---------- | ---------- | ---------- | -------- | | unique\_id | | | | | | | | | AAPL | 0.071773 | 0.068927 | 0.080182 | 0.075321 | 0.069187 | 0.068817 | 0.110426 | | AMZN | 0.127390 | 0.113613 | 0.118859 | 0.119930 | 0.109910 | 0.109910 | 0.115189 | | GOOG | 0.093849 | 0.093753 | 0.109662 | 0.101583 | 0.094648 | 0.103389 | 0.083233 | | META | 0.198334 | 0.198893 | 0.199615 | 0.199711 | 0.199712 | 0.198892 | 0.185346 | | MSFT | 0.082373 | 0.075055 | 0.072241 | 0.072765 | 0.073006 | 0.082066 | 0.086951 | | NFLX | 0.159386 | 0.159528 | 0.199623 | 0.232477 | 0.230075 | 0.230770 | 0.167421 | | NKE | 0.108337 | 0.098918 | 0.103366 | 0.110278 | 0.107179 | 0.102708 | 0.160404 | | NVDA | 0.189461 | 0.207871 | 0.198999 | 0.196170 | 0.211932 | 0.211940 | 0.215289 | | SPY | 0.058511 | 0.058583 | 0.058701 | 0.062492 | 0.057053 | 0.068192 | 0.089012 | | TSLA | 0.192003 | 0.192618 | 0.190225 | 0.192354 | 0.191620 | 0.191423 | 0.218857 | ```python theme={null} mae_cv.idxmin(axis=1) ``` ```text theme={null} unique_id AAPL GARCH(2,1) AMZN GARCH(2,2) GOOG Naive META Naive MSFT GARCH(1,1) NFLX ARCH(1) NKE ARCH(2) NVDA ARCH(1) SPY GARCH(2,2) TSLA GARCH(1,1) dtype: object ``` Hence, the most accurate model to describe the logarithmic returns of Apple’s stock is a GARCH(2, 1), for Amazon’s stock is a GARCH(2,2), and so on. ## Forecast volatility We can now generate a forecast for the next quarter. To do this, we’ll use the `forecast` method, which requires the following arguments: * `h`: (int) The forecasting horizon. * `level`: (list\[float]) The confidence levels of the prediction intervals * `fitted` : (bool = False) Returns insample predictions. ```python theme={null} levels = [80, 95] # confidence levels for the prediction intervals forecasts = sf.forecast(df=returns, h=3, level=levels) forecasts.head() ``` | | unique\_id | ds | ARCH(1) | ARCH(1)-lo-95 | ARCH(1)-lo-80 | ARCH(1)-hi-80 | ARCH(1)-hi-95 | ARCH(2) | ARCH(2)-lo-95 | ARCH(2)-lo-80 | ... | GARCH(2,1) | GARCH(2,1)-lo-95 | GARCH(2,1)-lo-80 | GARCH(2,1)-hi-80 | GARCH(2,1)-hi-95 | Naive | Naive-lo-80 | Naive-lo-95 | Naive-hi-80 | Naive-hi-95 | | - | ---------- | ---------- | --------- | ------------- | ------------- | ------------- | ------------- | --------- | ------------- | ------------- | --- | ---------- | ---------------- | ---------------- | ---------------- | ---------------- | --------- | ----------- | ----------- | ----------- | ----------- | | 0 | AAPL | 2023-01-01 | 0.150457 | 0.133641 | 0.139462 | 0.161453 | 0.167273 | 0.150158 | 0.133409 | 0.139206 | ... | 0.147602 | 0.131418 | 0.137020 | 0.158184 | 0.163786 | -0.128762 | -0.284462 | -0.366885 | 0.026939 | 0.109362 | | 1 | AAPL | 2023-02-01 | -0.056943 | -0.073924 | -0.068046 | -0.045839 | -0.039961 | -0.057207 | -0.074346 | -0.068414 | ... | -0.059512 | -0.078060 | -0.071640 | -0.047384 | -0.040964 | -0.128762 | -0.348956 | -0.465520 | 0.091433 | 0.207997 | | 2 | AAPL | 2023-03-01 | -0.048391 | -0.064843 | -0.059148 | -0.037633 | -0.031939 | -0.049282 | -0.066345 | -0.060439 | ... | -0.054539 | -0.075438 | -0.068204 | -0.040875 | -0.033641 | -0.128762 | -0.398443 | -0.541204 | 0.140920 | 0.283681 | | 3 | AMZN | 2023-01-01 | 0.152147 | 0.134952 | 0.140904 | 0.163391 | 0.169343 | 0.148658 | 0.132242 | 0.137924 | ... | 0.148599 | 0.132196 | 0.137873 | 0.159324 | 0.165001 | -0.139141 | -0.315716 | -0.409190 | 0.037435 | 0.130909 | | 4 | AMZN | 2023-02-01 | -0.057301 | -0.074497 | -0.068545 | -0.046058 | -0.040106 | -0.061187 | -0.080794 | -0.074007 | ... | -0.069303 | -0.094457 | -0.085750 | -0.052856 | -0.044150 | -0.139141 | -0.388856 | -0.521048 | 0.110575 | 0.242767 | With the results of the previous section, we can choose the best model for the S\&P 500 and the companies selected. Some of the plots are shown below. Notice that we’re using some additional arguments in the `plot` method: * `level`: (list\[int]) The confidence levels for the prediction intervals (this was already defined). * `unique_ids`: (list\[str, int or category]) The ids to plot. * `models`: (list(str)). The model to plot. In this case, is the model selected by cross-validation. ```python theme={null} StatsForecast.plot(returns, forecasts, max_insample_length=20) ``` ## References * [Engle, R. F. (1982). Autoregressive conditional heteroscedasticity with estimates of the variance of United Kingdom inflation. Econometrica: Journal of the econometric society, 987-1007.](http://www.econ.uiuc.edu/~econ508/Papers/engle82.pdf) * [Bollerslev, T. (1986). Generalized autoregressive conditional heteroskedasticity. Journal of econometrics, 31(3), 307-327.](https://citeseerx.ist.psu.edu/document?repid=rep1\&type=pdf\&doi=7da8bfa5295375c1141d797e80065a599153c19d) * [Hamilton, J. D. (1994). Time series analysis. Princeton university press.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis) * [Tsay, R. S. (2005). Analysis of financial time series. John wiley & sons.](https://www.wiley.com/en-us/Analysis+of+Financial+Time+Series%2C+3rd+Edition-p-9780470414354) # Intermittent or Sparse Data Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/intermittentdata.html > In this notebook, we’ll implement models for intermittent or sparse > data Intermittent or sparse data has very few non-zero observations. This type of data is hard to forecast because the zero values increase the uncertainty about the underlying patterns in the data. Furthermore, once a non-zero observation occurs, there can be considerable variation in its size. Intermittent time series are common in many industries, including finance, retail, transportation, and energy. Given the ubiquity of this type of series, special methods have been developed to forecast them. The first was from [Croston (1972)](#ref), followed by several variants and by different aggregation frameworks. [StatsForecast](../../index.html) has implemented several models to forecast intermittent time series. By the end of this tutorial, you’ll have a good understanding of these models and how to use them. **Outline:** 1. Install libraries 2. Load and explore the data 3. Train models for intermittent data 4. Plot forecasts and compute accuracy > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > > **Tip** > > For forecasting at scale, we recommend you check [this > notebook](https://www.databricks.com/blog/2022/12/06/intermittent-demand-forecasting-nixtla-databricks.html) > done on Databricks. ## Install libraries We assume that you have StatsForecast already installed. If not, check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html) Install the necessary packages using `pip install statsforecast` ```python theme={null} pip install statsforecast -U ``` ## Load and explore the data For this example, we’ll use a subset of the [M5 Competition](https://www.sciencedirect.com/science/article/pii/S0169207021001187#:~:text=The%20objective%20of%20the%20M5,the%20uncertainty%20around%20these%20forecasts) dataset. Each time series represents the unit sales of a particular product in a given Walmart store. At this level (product-store), most of the data is intermittent. We first need to import the data. ```python theme={null} import pandas as pd ``` ```python theme={null} uids = [ 'FOODS_1_001_CA_1', 'FOODS_1_001_CA_2', 'FOODS_1_001_CA_3', 'FOODS_1_001_CA_4', 'FOODS_1_001_TX_1', 'FOODS_1_001_TX_2', 'FOODS_1_001_TX_3', 'FOODS_1_001_WI_1', ] df = pd.read_parquet( 'https://datasets-nixtla.s3.amazonaws.com/m5_y.parquet', filters=[('unique_id', 'in', uids)], ) ``` We can plot these series using the `plot_series` function from `utilsforecast.plotting`. This function has multiple parameters, and the required ones to generate the plots in this notebook are explained below. * `df`: A `pandas` dataframe with columns \[`unique_id`, `ds`, `y`]. * `forecasts_df`: A `pandas` dataframe with columns \[`unique_id`, `ds`] and models. * `plot_random`: Plots the time series randomly. * `max_insample_length`: The maximum number of train/insample observations to be plotted. * `engine`: The library used to generate the plots. It can also be `matplotlib` for static plots. ```python theme={null} from utilsforecast.plotting import plot_series ``` ```python theme={null} plot_series(df, plot_random=False, max_insample_length=100) ``` Here we only plotted the last 100 observations, but we can visualize the complete history by removing `max_insample_length`. From these plots, we can confirm that the data is indeed intermittent since it has multiple periods with zero sales. In fact, in all cases but one, the median value is zero. ```python theme={null} df.groupby('unique_id', observed=True)['y'].median() ``` ```text theme={null} unique_id FOODS_1_001_CA_1 0.0 FOODS_1_001_CA_2 1.0 FOODS_1_001_CA_3 0.0 FOODS_1_001_CA_4 0.0 FOODS_1_001_TX_1 0.0 FOODS_1_001_TX_2 0.0 FOODS_1_001_TX_3 0.0 FOODS_1_001_WI_1 0.0 Name: y, dtype: float32 ``` ## Train models for intermittent data Before training any model, we need to separate the data in a train and a test set. The M5 Competition used the last 28 days as test set, so we’ll do the same. ```python theme={null} valid_start = df['ds'].unique()[-28] train = df[df['ds'] < valid_start] test = df[df['ds'] >= valid_start] ``` StatsForecast has efficient implementations of multiple models for intermittent data. The complete list of models available is [here](../../src/core/models.html). In this notebook, we’ll use: * [Agregate-Dissagregate Intermittent Demand Approach (ADIDA)](../../src/core/models.html#adida) * [Croston Classic](../../src/core/models.html#crostonclassic) * [Intermittent Multiple Aggregation Prediction Algorithm (IMAPA)](../../src/core/models.html#imapa) * [Teunter-Syntetos-Babai (TSB)](../../src/core/models.html#tsb) To use these models, we first need to import them from `statsforecast.models` and then we need to instantiate them. ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import ( ADIDA, CrostonClassic, IMAPA, TSB ) # Create a list of models and instantiation parameters models = [ ADIDA(), CrostonClassic(), IMAPA(), TSB(alpha_d = 0.2, alpha_p = 0.2) ] ``` To instantiate a new StatsForecast object, we need the following parameters: * `models`: The list of models defined in the previous step. * `freq`: A string indicating the frequency of the data. See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). * `n_jobs`: An integer that indicates the number of jobs used in parallel processing. Use -1 to select all cores. ```python theme={null} sf = StatsForecast( models=models, freq='D', n_jobs=-1, ) ``` Now we’re ready to generate the forecast. To do this, we’ll use the `forecast` method, which requires the forecasting horizon (in this case, 28 days) as argument. The models for intermittent series that are currently available in StatsForecast can only generate point-forecasts. If prediction intervals are needed, then a [probabilisitic model](../../src/core/models.html) should be used. ```python theme={null} horizon = 28 forecasts = sf.forecast(df=train, h=horizon) forecasts.head() ``` | | unique\_id | ds | ADIDA | CrostonClassic | IMAPA | TSB | | - | -------------------- | ---------- | -------- | -------------- | -------- | -------- | | 0 | FOODS\_1\_001\_CA\_1 | 2016-05-23 | 0.791852 | 0.898247 | 0.705835 | 0.434313 | | 1 | FOODS\_1\_001\_CA\_1 | 2016-05-24 | 0.791852 | 0.898247 | 0.705835 | 0.434313 | | 2 | FOODS\_1\_001\_CA\_1 | 2016-05-25 | 0.791852 | 0.898247 | 0.705835 | 0.434313 | | 3 | FOODS\_1\_001\_CA\_1 | 2016-05-26 | 0.791852 | 0.898247 | 0.705835 | 0.434313 | | 4 | FOODS\_1\_001\_CA\_1 | 2016-05-27 | 0.791852 | 0.898247 | 0.705835 | 0.434313 | Finally, we’ll merge the forecast with the actual values. ```python theme={null} test = test.merge(forecasts, how='left', on=['unique_id', 'ds']) ``` ## Plot forecasts and compute accuracy We can generate plots using the `plot_series` function described above. ```python theme={null} plot_series(train, test, plot_random=False, max_insample_length=100) ``` To compute the accuracy of the forecasts, we’ll use the Mean Average Error (MAE), which is the sum of the absolute errors divided by the number of forecasts. ```python theme={null} from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae ``` ```python theme={null} evaluate(test, metrics=[mae], agg_fn='mean') ``` | | metric | ADIDA | CrostonClassic | IMAPA | TSB | | - | ------ | -------- | -------------- | -------- | -------- | | 0 | mae | 0.948729 | 0.944071 | 0.957256 | 1.023126 | Hence, on average, the forecasts are one unit off. ## References [Croston, J. D. (1972). Forecasting and stock control for intermittent demands. Journal of the Operational Research Society, 23(3), 289-303.](https://link.springer.com/article/10.1057/jors.1972.50) # MLFlow | StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/mlflow.html > Run Statsforecast with MLFlow. [MLFlow](https://github.com/mlflow/mlflow/) is an open source experiment tracking system that helps data scientists manage the model lifecycle from experimentation to production. An MLFlow integration for statsforecast is available in the [MLFlow](https://github.com/ml-toolkits/mlflavors) library that contains MLFlow support for popular machine learning libraries. ```python theme={null} from statsforecast.utils import generate_series ``` ```python theme={null} series = generate_series(5, min_length=50, max_length=50, equal_ends=True, n_static_features=1) series.head() ``` | | unique\_id | ds | y | static\_0 | | - | ---------- | ---------- | ---------- | --------- | | 0 | 0 | 2000-01-01 | 12.073897 | 43 | | 1 | 0 | 2000-01-02 | 59.734166 | 43 | | 2 | 0 | 2000-01-03 | 101.260794 | 43 | | 3 | 0 | 2000-01-04 | 143.987430 | 43 | | 4 | 0 | 2000-01-05 | 185.320406 | 43 | For the next part, `mlflow` and `mlflavors` are needed. Install them with: ```bash theme={null} pip install mlflow mlflavors ``` ## Model Logging ```python theme={null} import pandas as pd import mlflow from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error from statsforecast import StatsForecast from statsforecast.models import AutoARIMA import mlflavors import requests ``` ```python theme={null} ARTIFACT_PATH = "model" DATA_PATH = "./data" HORIZON = 7 LEVEL = [90] with mlflow.start_run() as run: series = generate_series(5, min_length=50, max_length=50, equal_ends=True, n_static_features=1) train_df = series.groupby('unique_id').head(43) test_df = series.groupby('unique_id').tail(7) X_test = test_df.drop(columns=["y"]) y_test = test_df[["y"]] models = [AutoARIMA(season_length=7)] sf = StatsForecast(models=models, freq="D", n_jobs=-1) sf.fit(df=train_df) # Evaluate model y_pred = sf.predict(h=HORIZON, X_df=X_test, level=LEVEL)["AutoARIMA"] metrics = { "mae": mean_absolute_error(y_test, y_pred), "mape": mean_absolute_percentage_error(y_test, y_pred), } print(f"Metrics: \n{metrics}") # Log metrics mlflow.log_metrics(metrics) # Log model using pickle serialization (default). mlflavors.statsforecast.log_model( statsforecast_model=sf, artifact_path=ARTIFACT_PATH, serialization_format="pickle", ) model_uri = mlflow.get_artifact_uri(ARTIFACT_PATH) print(f"\nMLflow run id:\n{run.info.run_id}") ``` ```text theme={null} Metrics: {'mae': 6.712853959225143, 'mape': 0.11719246764336884} MLflow run id: 0319bbd664424fcd88d6c532e3ecac77 ``` ```text theme={null} 2023/10/20 23:45:36 WARNING mlflow.utils.environment: Encountered an unexpected error while inferring pip requirements (model URI: /var/folders/w2/91_v34nx0xs2npnl3zsl9tmm0000gn/T/tmpt4686vpu/model/model.pkl, flavor: statsforecast), fall back to return ['statsforecast==1.6.0']. Set logging level to DEBUG to see the full traceback. ``` ## Viewing Experiment To view the newly created experiment and logged artifacts open the MLflow UI: ```bash theme={null} mlflow ui ``` ## Loading Statsforecast Model The `statsforecast` model can be loaded from the MLFlow registry using the `mlflow.statsforecast.load_model` function and used to generate predictions. ```python theme={null} loaded_model = mlflavors.statsforecast.load_model(model_uri=model_uri) results = loaded_model.predict(h=HORIZON, X_df=X_test, level=LEVEL) results.head() ``` | | ds | AutoARIMA | AutoARIMA-lo-90 | AutoARIMA-hi-90 | | ---------- | ---------- | ---------- | --------------- | --------------- | | unique\_id | | | | | | 0 | 2000-02-13 | 55.894432 | 44.343880 | 67.444984 | | 0 | 2000-02-14 | 97.818054 | 86.267502 | 109.368607 | | 0 | 2000-02-15 | 146.745422 | 135.194870 | 158.295975 | | 0 | 2000-02-16 | 188.888336 | 177.337784 | 200.438904 | | 0 | 2000-02-17 | 231.493637 | 219.943085 | 243.044189 | ## Loading Model with pyfunc [Pyfunc](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html) is another interface for MLFlow models that has utilities for loading and saving models. This code is equivalent in making predictions as above. ```python theme={null} loaded_pyfunc = mlflavors.statsforecast.pyfunc.load_model(model_uri=model_uri) # Convert test data to 2D numpy array so it can be passed to pyfunc predict using # a single-row Pandas DataFrame configuration argument X_test_array = X_test.to_numpy() # Create configuration DataFrame predict_conf = pd.DataFrame( [ { "X": X_test_array, "X_cols": X_test.columns, "X_dtypes": list(X_test.dtypes), "h": HORIZON, "level": LEVEL, } ] ) pyfunc_result = loaded_pyfunc.predict(predict_conf) pyfunc_result.head() ``` | | ds | AutoARIMA | AutoARIMA-lo-90 | AutoARIMA-hi-90 | | ---------- | ---------- | ---------- | --------------- | --------------- | | unique\_id | | | | | | 0 | 2000-02-13 | 55.894432 | 44.343880 | 67.444984 | | 0 | 2000-02-14 | 97.818054 | 86.267502 | 109.368607 | | 0 | 2000-02-15 | 146.745422 | 135.194870 | 158.295975 | | 0 | 2000-02-16 | 188.888336 | 177.337784 | 200.438904 | | 0 | 2000-02-17 | 231.493637 | 219.943085 | 243.044189 | ## Model Serving This section illustrates an example of serving the `pyfunc` flavor to a local REST API endpoint and subsequently requesting a prediction from the served model. To serve the model run the command below where you substitute the run id printed during execution training code. ```bash theme={null} mlflow models serve -m runs://model --env-manager local --host 127.0.0.1 ``` After running this, the code below can be ran to send a request. ```python theme={null} HORIZON = 7 LEVEL = [90, 95] # Define local host and endpoint url host = "127.0.0.1" url = f"http://{host}:5000/invocations" # Convert DateTime to string for JSON serialization X_test_pyfunc = X_test.copy() X_test_pyfunc["ds"] = X_test_pyfunc["ds"].dt.strftime(date_format="%Y-%m-%d") # Convert to list for JSON serialization X_test_list = X_test_pyfunc.to_numpy().tolist() # Convert index to list of strings for JSON serialization X_cols = list(X_test.columns) # Convert dtypes to string for JSON serialization X_dtypes = [str(dtype) for dtype in list(X_test.dtypes)] predict_conf = pd.DataFrame( [ { "X": X_test_list, "X_cols": X_cols, "X_dtypes": X_dtypes, "h": HORIZON, "level": LEVEL, } ] ) # Create dictionary with pandas DataFrame in the split orientation json_data = {"dataframe_split": predict_conf.to_dict(orient="split")} # Score model response = requests.post(url, json=json_data) ``` ```python theme={null} pd.DataFrame(response.json()['predictions']).head() ``` | | ds | AutoARIMA | AutoARIMA-lo-95 | AutoARIMA-lo-90 | AutoARIMA-hi-90 | AutoARIMA-hi-95 | | - | ------------------- | ---------- | --------------- | --------------- | --------------- | --------------- | | 0 | 2000-02-13T00:00:00 | 55.894432 | 42.131100 | 44.343880 | 67.444984 | 69.657768 | | 1 | 2000-02-14T00:00:00 | 97.818054 | 84.054718 | 86.267502 | 109.368607 | 111.581390 | | 2 | 2000-02-15T00:00:00 | 146.745422 | 132.982086 | 135.194870 | 158.295975 | 160.508759 | | 3 | 2000-02-16T00:00:00 | 188.888336 | 175.125015 | 177.337784 | 200.438904 | 202.651672 | | 4 | 2000-02-17T00:00:00 | 231.493637 | 217.730301 | 219.943085 | 243.044189 | 245.256973 | # Multiple seasonalities Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/multipleseasonalities.html > In this example we will show how to forecast data with multiple > seasonalities using an MSTL. > **Tip** > > For this task, StatsForecast’s MSTL is 68% more accurate and 600% > faster than [Prophet](https://facebook.github.io/prophet/) and > [NeuralProphet](https://neuralprophet.com/). (Reproduce experiments > [here](https://github.com/Nixtla/statsforecast/tree/main/experiments/mstl)) Multiple seasonal data refers to time series that have more than one clear seasonality. Multiple seasonality is traditionally present in data that is sampled at a low frequency. For example, hourly electricity data exhibits daily and weekly seasonality. That means that there are clear patterns of electricity consumption for specific hours of the day like 6:00pm vs 3:00am or for specific days like Sunday vs Friday. Traditional statistical models are not able to model more than one seasonal length. In this example, we will show how to model the two seasonalities efficiently using Multiple Seasonal-Trend decompositions with LOESS (`MSTL`). For this example, we will use hourly electricity load data from Pennsylvania, New Jersey, and Maryland (PJM). The original data can be found [here](https://github.com/jnagura/Energy-consumption-prediction-analysis). (Click here for info on [PJM](https://www.pjm.com/about-pjm)) First, we will load the data, then we will use the `StatsForecast.fit` and `StatsForecast.predict` methods to predict the next 24 hours. We will then decompose the different elements of the time series into trends and its multiple seasonalities. At the end, you will use the `StatsForecast.forecast` for production-ready forecasting. **Outline** 1. Install libraries 2. Load and explore the data 3. Fit a multiple-seasonality model 4. Decompose the series in trend and seasonality 5. Predict the next 24 hours 6. Optional: Forecast in production > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Install libraries We assume you have StatsForecast already installed. Check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html). Install the necessary packages using `pip install statsforecast` ```python theme={null} !pip install statsforecast ``` ## Load Data The input to StatsForecast is always a data frame in [long format](https://www.theanalysisfactor.com/wide-and-long-data/) with three columns: `unique_id`, `ds` and `y`: * The `unique_id` (string, int or category) represents an identifier for the series. * The `ds` (datestamp or int) column should be either an integer indexing time or a datestamp ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp. * The `y` (numeric) represents the measurement we wish to forecast. We will rename the You will read the data with pandas and change the necessary names. This step should take around 2s. ```python theme={null} import pandas as pd ``` ```python theme={null} df = pd.read_csv('https://raw.githubusercontent.com/panambY/Hourly_Energy_Consumption/master/data/PJM_Load_hourly.csv') df.columns = ['ds', 'y'] df.insert(0, 'unique_id', 'PJM_Load_hourly') df = df.sort_values(['unique_id', 'ds']).reset_index(drop=True) df.tail() ``` | | unique\_id | ds | y | | ----- | ----------------- | ------------------- | ------- | | 32891 | PJM\_Load\_hourly | 2001-12-31 20:00:00 | 36392.0 | | 32892 | PJM\_Load\_hourly | 2001-12-31 21:00:00 | 35082.0 | | 32893 | PJM\_Load\_hourly | 2001-12-31 22:00:00 | 33890.0 | | 32894 | PJM\_Load\_hourly | 2001-12-31 23:00:00 | 32590.0 | | 32895 | PJM\_Load\_hourly | 2002-01-01 00:00:00 | 31569.0 | StatsForecast can handle unsorted data, however, for plotting purposes, it is convenient to sort the data frame. Plot the series using the `plot` method from the `StatsForecast` class. This method prints up to 8 random series from the dataset and is useful for basic EDA. In this case, it will print just one series given that we have just one unique\_id. > **Note** > > The `StatsForecast.plot` method uses matplotlib as a default engine. > You can change to plotly by setting `engine="plotly"`. ```python theme={null} from statsforecast import StatsForecast ``` ```python theme={null} StatsForecast.plot(df) ``` The time series exhibits seasonal patterns. Moreover, the time series contains `32,896` observations, so it is necessary to use very computationally efficient methods. ## Fit an MSTL model The [MSTL](../../src/core/models.html#multipleseasonaltrend) (Multiple Seasonal-Trend decompositions using LOESS) model, originally developed by [Kasun Bandara, Rob J Hyndman and Christoph Bergmeir](https://arxiv.org/abs/2107.13462), decomposes the time series in multiple seasonalities using a Local Polynomial Regression (LOESS). Then it forecasts the trend using a non-seasonal model and each seasonality using a [SeasonalNaive](../../src/core/models.html#seasonalnaive) model. You can choose the non-seasonal model you want to use to forecast the trend component of the MSTL model. In this example, we will use an [AutoARIMA](../../src/core/models.html#autoarima). Import the models you need. ```python theme={null} from statsforecast.models import MSTL, AutoARIMA ``` First, we must define the model parameters. As mentioned before, the electricity load presents seasonalities every 24 hours (Hourly) and every 24 \* 7 (Daily) hours. Therefore, we will use `[24, 24 * 7]` for season length. The trend component will be forecasted with an `AutoARIMA` model. (You can also try with: `AutoTheta`, `AutoCES`, and `AutoETS`) ```python theme={null} # Create a list of models and instantiation parameters models = [MSTL( season_length=[24, 24 * 7], # seasonalities of the time series trend_forecaster=AutoARIMA() # model used to forecast trend )] ``` We fit the models by instantiating a new `StatsForecast` object with the following required parameters: * `models`: a list of models. Select the models you want from [models](../../src/core/models.html) and import them. * `freq`: a string indicating the frequency of the data. (See [panda’s available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases).) Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} sf = StatsForecast( models=models, # model used to fit each time series freq='h', # frequency of the data ) ``` > **Tip** > > StatsForecast also supports this optional parameter. > > * `n_jobs`: n\_jobs: int, number of jobs used in the parallel > processing, use -1 for all cores. (Default: 1) > > * `fallback_model`: a model to be used if a model fails. (Default: > none) Use the `fit` method to fit each model to each time series. In this case, we are just fitting one model to one series. Check this guide to learn how to [fit many models to many series](../getting-started/getting_started_complete.html). ```python theme={null} sf = sf.fit(df=df) ``` ## Decompose the series Once the model is fitted, access the decomposition using the `fitted_` attribute of `StatsForecast`. This attribute stores all relevant information of the fitted models for each of the time series. In this case, we are fitting a single model for a single time series, so by accessing the fitted\_ location \[0, 0] we will find the relevant information of our model. The `MSTL` class generates a `model_` attribute that contains the way the series was decomposed. ```python theme={null} sf.fitted_[0, 0].model_ ``` | | data | trend | seasonal24 | seasonal168 | remainder | | ----- | ------- | ------------ | ------------ | ----------- | ------------ | | 0 | 22259.0 | 25899.808157 | -4720.213546 | 581.308595 | 498.096794 | | 1 | 21244.0 | 25900.349395 | -5433.168901 | 571.780657 | 205.038849 | | 2 | 20651.0 | 25900.875973 | -5829.135728 | 557.142643 | 22.117112 | | 3 | 20421.0 | 25901.387631 | -5704.092794 | 597.696957 | -373.991794 | | 4 | 20713.0 | 25901.884103 | -5023.324375 | 922.564854 | -1088.124582 | | ... | ... | ... | ... | ... | ... | | 32891 | 36392.0 | 33329.031577 | 4254.112720 | 917.258336 | -2108.402633 | | 32892 | 35082.0 | 33355.083576 | 3625.077164 | 721.689136 | -2619.849876 | | 32893 | 33890.0 | 33381.108409 | 2571.794472 | 549.661529 | -2612.564409 | | 32894 | 32590.0 | 33407.105839 | 796.356548 | 361.956280 | -1975.418667 | | 32895 | 31569.0 | 33433.075723 | -1260.860917 | 279.777069 | -882.991876 | We will use matplotlib, to visualize the different components of the series. ```python theme={null} import matplotlib.pyplot as plt ``` ```python theme={null} sf.fitted_[0, 0].model_.tail(24 * 28).plot(subplots=True, grid=True) plt.tight_layout() plt.show() ``` We observe a clear upward trend (orange line) and seasonality repeating every day (24H) and every week (168H). ## Predict the next 24 hours > Probabilistic forecasting with levels To generate forecasts use the `predict` method. The `predict` method takes two arguments: forecasts the next `h` (for horizon) and `level`. * `h` (int): represents the forecast h steps into the future. In this case, 12 months ahead. * `level` (list of floats): this optional parameter is used for probabilistic forecasting. Set the `level` (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This step should take less than 1 second. ```python theme={null} forecasts = sf.predict(h=24, level=[90]) forecasts.head() ``` | | unique\_id | ds | MSTL | MSTL-lo-90 | MSTL-hi-90 | | - | ----------------- | ------------------- | ------------ | ------------ | ------------ | | 0 | PJM\_Load\_hourly | 2002-01-01 01:00:00 | 30215.608123 | 29842.185581 | 30589.030664 | | 1 | PJM\_Load\_hourly | 2002-01-01 02:00:00 | 29447.208519 | 28787.122830 | 30107.294207 | | 2 | PJM\_Load\_hourly | 2002-01-01 03:00:00 | 29132.786369 | 28221.353220 | 30044.219518 | | 3 | PJM\_Load\_hourly | 2002-01-01 04:00:00 | 29126.252713 | 27992.819671 | 30259.685756 | | 4 | PJM\_Load\_hourly | 2002-01-01 05:00:00 | 29604.606314 | 28273.426621 | 30935.786006 | You can plot the forecast by calling the `StatsForecast.plot` method and passing in your forecast dataframe. ```python theme={null} sf.plot(df, forecasts, max_insample_length=24 * 7) ``` ## Forecast in production If you want to gain speed in productive settings where you have multiple series or models we recommend using the `StatsForecast.forecast` method instead of `.fit` and `.predict`. The main difference is that the `.forecast` doest not store the fitted values and is highly scalable in distributed environments. The `forecast` method takes two arguments: forecasts next `h` (horizon) and `level`. * `h` (int): represents the forecast h steps into the future. In this case, 12 months ahead. * `level` (list of floats): this optional parameter is used for probabilistic forecasting. Set the `level` (or confidence percentile) of your prediction interval. For example, `level=[90]` means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. Depending on your computer, this step should take around 1min. (If you want to speed things up to a couple of seconds, remove the AutoModels like ARIMA and Theta) ```python theme={null} forecasts_df = sf.forecast(df=df, h=24, level=[90]) forecasts_df.head() ``` | | unique\_id | ds | MSTL | MSTL-lo-90 | MSTL-hi-90 | | - | ----------------- | ------------------- | ------------ | ------------ | ------------ | | 0 | PJM\_Load\_hourly | 2002-01-01 01:00:00 | 30215.608123 | 29842.185581 | 30589.030664 | | 1 | PJM\_Load\_hourly | 2002-01-01 02:00:00 | 29447.208519 | 28787.122830 | 30107.294207 | | 2 | PJM\_Load\_hourly | 2002-01-01 03:00:00 | 29132.786369 | 28221.353220 | 30044.219518 | | 3 | PJM\_Load\_hourly | 2002-01-01 04:00:00 | 29126.252713 | 27992.819671 | 30259.685756 | | 4 | PJM\_Load\_hourly | 2002-01-01 05:00:00 | 29604.606314 | 28273.426621 | 30935.786006 | ## References * [Bandara, Kasun & Hyndman, Rob & Bergmeir, Christoph. (2021). “MSTL: A Seasonal-Trend Decomposition Algorithm for Time Series with Multiple Seasonal Patterns”](https://arxiv.org/abs/2107.13462). ## Next Steps * Learn how to [use cross-validation to assess the robustness of your model](../getting-started/getting_started_complete.html#evaluate-the-model%E2%80%99s-performance) # Trajectory Simulation Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/simulation.html > This tutorial demonstrates how to generate sample trajectories > (simulated paths) using StatsForecast. > **Prerequisites** > > This tutorial assumes basic familiarity with StatsForecast. For a > minimal example visit the [Quick > Start](../getting-started/getting_started_short.html) ## Introduction While standard forecasting methods often produce a single point forecast or prediction intervals, some scenarios require understanding the full range of possible future paths. **Trajectory simulation** allows you to generate multiple possible future realizations of a time series based on the fitted model’s error distribution. This is particularly useful for: - Risk analysis and stress testing. - Scenario planning (e.g., “what if” analyses). - Calculating complex metrics based on future paths (e.g., probability of breach). By the end of this tutorial, you’ll be able to use the `simulate` method in `StatsForecast` to generate and visualize these paths using various error distributions. > **Important** > > Trajectory simulation is currently supported for `AutoARIMA` and other > statistical models that implement the `simulate` interface. **Outline:** 1. Install libraries 2. Load and explore the data 3. Basic simulation (Normal Distribution) 4. Automatic parameter inference 5. User-provided parameters 6. Comparing distributions 7. Handling large simulations > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Install libraries We assume that you have StatsForecast already installed. If not, check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html) ```python theme={null} %pip install -U statsforecast ``` ```python theme={null} %matplotlib inline import pandas as pd import numpy as np import matplotlib.pyplot as plt from statsforecast import StatsForecast from statsforecast.models import AutoARIMA ``` ## Load and explore the data We’ll use a subset of the hourly dataset from the [M4 Competition](https://www.sciencedirect.com/science/article/pii/S0169207019301128). ```python theme={null} df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet') df = df[df['unique_id'] == 'H1'] # Focus on one series for clarity df.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | ## Basic Simulation (Normal Distribution) A simulation is performed by calling the `simulate` method after fitting. By default, it samples from a Normal distribution using the model’s estimated variance from the residuals. ```python theme={null} # Initialize the model models = [AutoARIMA(season_length=24)] sf = StatsForecast(models=models, freq=1, n_jobs=1) # Simulate 100 paths for a horizon of 48 hours n_paths = 100 h = 48 # The simulation method handles the fitting (if needed) and the simulation sims = sf.simulate(df=df, h=h, n_paths=n_paths, seed=42) sims.head() ``` | | unique\_id | ds | sample\_id | AutoARIMA | | - | ---------- | --- | ---------- | ---------- | | 0 | H1 | 749 | 0 | 610.915688 | | 0 | H1 | 750 | 0 | 563.314544 | | 0 | H1 | 751 | 0 | 538.771083 | | 0 | H1 | 752 | 0 | 519.682203 | | 0 | H1 | 753 | 0 | 514.878641 | The output contains a `sample_id` column to distinguish between different trajectories. ## Automatic Parameter Inference StatsForecast can automatically infer distribution parameters from your model’s residuals. When you don’t specify `error_params`, the system uses Maximum Likelihood Estimation (MLE) to fit the distribution parameters to the residuals. This is particularly useful when you want the simulation to reflect the actual characteristics of your data’s errors. ### Supported Distributions * **‘normal’**: Standard normal distribution (default) * **‘t’**: Student’s t-distribution (heavy tails, good for financial data) * **‘bootstrap’**: Resample from empirical residuals (non-parametric) * **‘laplace’**: Laplace distribution (sharper peak, heavier tails) * **‘skew-normal’**: Skewed normal distribution (for asymmetric errors) * **‘ged’**: Generalized Error Distribution (flexible shape) Let’s demonstrate with different distributions. Note that parameters are automatically estimated from residuals. ```python theme={null} # Student's t-distribution with automatic parameter inference # The degrees of freedom, location, and scale are automatically estimated from residuals sims_t_auto = sf.simulate( df=df, h=h, n_paths=n_paths, error_distribution='t', # No error_params specified - parameters inferred automatically seed=42 ) # Laplace distribution with automatic parameter inference sims_laplace = sf.simulate( df=df, h=h, n_paths=n_paths, error_distribution='laplace', seed=42 ) # Skew-normal distribution with automatic parameter inference sims_skewnorm = sf.simulate( df=df, h=h, n_paths=n_paths, error_distribution='skew-normal', seed=42 ) ``` ## User-Provided Parameters Instead of relying on automatic inference, you can explicitly specify distribution parameters using the `error_params` dictionary. This gives you precise control over the simulation characteristics. This is useful when: - You have domain knowledge about the error distribution - You want to stress-test with extreme scenarios - You want to ensure consistency across different datasets ### Parameter Specifications * **‘t’**: `{'df': degrees_of_freedom}` - Controls tail heaviness (lower = heavier tails) * **‘skew-normal’**: `{'skewness': alpha}` - Controls asymmetry (negative = left skew, positive = right skew) * **‘ged’**: `{'shape': beta}` - Controls tail behavior (1 = Laplace, 2 = Normal, higher = lighter tails) ```python theme={null} # Student's t with very heavy tails (low df = more extreme values) sims_t_heavy = sf.simulate( df=df, h=h, n_paths=n_paths, error_distribution='t', error_params={'df': 3}, # Heavy tails for stress testing seed=42 ) # Skew-normal with right skew (for positively skewed errors) sims_skewnorm_custom = sf.simulate( df=df, h=h, n_paths=n_paths, error_distribution='skew-normal', error_params={'skewness': 3}, # Positive skew seed=42 ) # GED with Laplace-like behavior (shape=1) sims_ged = sf.simulate( df=df, h=h, n_paths=n_paths, error_distribution='ged', error_params={'shape': 1}, # Laplace-like (sharper peak) seed=42 ) # Bootstrap resampling from residuals (non-parametric) sims_boot = sf.simulate( df=df, h=h, n_paths=n_paths, error_distribution='bootstrap', seed=42 ) ``` ## Comparing Distributions Let’s visualize the differences between automatic inference and user-provided parameters. We’ll compare: 1. **Normal** - The baseline distribution 2. **t-distribution (automatic)** - Parameters estimated from residuals 3. **t-distribution (df=3)** - User-specified heavy tails for stress testing 4. **Bootstrap** - Non-parametric resampling from actual residuals ```python theme={null} def plot_sims(df, sims, title, color='blue'): fig, ax = plt.subplots(figsize=(10, 6)) # Last 100 historical points hist = df.tail(100) ax.plot(hist['ds'], hist['y'], color='black', label='Historical', linewidth=2) # Identify model column model_col = [c for c in sims.columns if c not in ['unique_id', 'ds', 'sample_id']][0] # Plot first 50 simulation paths for clarity for x in range(50): path = sims[sims['sample_id'] == x] ax.plot(path['ds'], path[model_col], color=color, alpha=0.1, linewidth=1) ax.set_title(title, fontsize=14, fontweight='bold') ax.set_xlabel('Time') ax.set_ylabel('Value') ax.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() # Compare different approaches print("1. Normal Distribution (Baseline)") plot_sims(df, sims, 'Normal Distribution', color='steelblue') print("\n2. t-Distribution with Automatic Parameter Inference") plot_sims(df, sims_t_auto, 't-Distribution (Automatic)', color='darkorange') print("\n3. t-Distribution with Heavy Tails (df=3, User-Specified)") plot_sims(df, sims_t_heavy, 't-Distribution (df=3, Heavy Tails)', color='darkred') print("\n4. Bootstrap (Non-parametric)") plot_sims(df, sims_boot, 'Bootstrap Resampling', color='forestgreen') ``` ```text theme={null} 1. Normal Distribution (Baseline) 2. t-Distribution with Automatic Parameter Inference 3. t-Distribution with Heavy Tails (df=3, User-Specified) 4. Bootstrap (Non-parametric) ``` ### Key Observations 1. **Normal distribution** provides symmetric paths around the mean - suitable when errors are well-behaved 2. **t-distribution (automatic)** adapts to the data’s error characteristics, producing paths that reflect the actual residual distribution 3. **t-distribution (df=3)** with user-specified parameters creates more extreme scenarios, useful for stress testing and risk analysis 4. **Bootstrap** uses the empirical error distribution directly, making no parametric assumptions **When to use automatic inference:** - You want simulations that reflect your data’s actual error characteristics - You don’t have prior knowledge about the error distribution - You want the model to learn from the residuals **When to provide custom parameters:** - You have domain expertise about the error distribution - You want to test specific scenarios (e.g., extreme market conditions) - You need reproducible behavior across different datasets - You want to explore “what-if” scenarios with controlled assumptions ### Quantitative Comparison Let’s examine the statistical properties of the simulated paths to understand how each distribution affects the forecasts. ```python theme={null} # Extract the model column name model_col = 'AutoARIMA' # Compare statistics across distributions comparison_data = [] for name, sim_data in [ ('Normal', sims), ('t (Auto)', sims_t_auto), ('t (df=3)', sims_t_heavy), ('Bootstrap', sims_boot), ]: values = sim_data[model_col].values comparison_data.append({ 'Distribution': name, 'Mean': f"{np.mean(values):.2f}", 'Std Dev': f"{np.std(values):.2f}", 'Min': f"{np.min(values):.2f}", 'Max': f"{np.max(values):.2f}", '5th percentile': f"{np.percentile(values, 5):.2f}", '95th percentile': f"{np.percentile(values, 95):.2f}", }) comparison_df = pd.DataFrame(comparison_data) print("Statistical Comparison of Simulated Trajectories:") print("=" * 80) print(comparison_df.to_string(index=False)) ``` ```text theme={null} Statistical Comparison of Simulated Trajectories: ================================================================================ Distribution Mean Std Dev Min Max 5th percentile 95th percentile Normal 670.15 161.78 307.35 1015.69 433.04 910.30 t (Auto) 669.82 164.69 306.78 1027.63 431.49 914.23 t (df=3) 667.69 164.39 314.33 1043.34 430.01 907.51 Bootstrap 669.94 165.51 324.97 1059.47 429.79 913.07 ``` ## References [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, The Statistical Forecasting Perspective”](https://otexts.com/fpp3/perspective.html). # Statistical, Machine Learning and Neural Forecasting methods | StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/statisticalneuralmethods.html > In this notebook, you will make forecasts for the M5 dataset choosing > the best model for each time series using cross validation. Statistical, Machine Learning, and Neural Forecasting Methods In this tutorial, we will explore the process of forecasting on the M5 dataset by utilizing the most suitable model for each time series. We’ll accomplish this through an essential technique known as cross-validation. This approach helps us in estimating the predictive performance of our models, and in selecting the model that yields the best performance for each time series. The M5 dataset comprises of hierarchical sales data, spanning five years, from Walmart. The aim is to forecast daily sales for the next 28 days. The dataset is broken down into the 50 states of America, with 10 stores in each state. In the realm of time series forecasting and analysis, one of the more complex tasks is identifying the model that is optimally suited for a specific group of series. Quite often, this selection process leans heavily on intuition, which may not necessarily align with the empirical reality of our dataset. In this tutorial, we aim to provide a more structured, data-driven approach to model selection for different groups of series within the M5 benchmark dataset. This dataset, well-known in the field of forecasting, allows us to showcase the versatility and power of our methodology. We will train an assortment of models from various forecasting paradigms: *[StatsForecast](https://github.com/Nixtla/statsforecast)* * Baseline models: These models are simple yet often highly effective for providing an initial perspective on the forecasting problem. We will use `SeasonalNaive` and `HistoricAverage` models for this category. * Intermittent models: For series with sporadic, non-continuous demand, we will utilize models like `CrostonOptimized`, `IMAPA`, and `ADIDA`. These models are particularly suited for handling zero-inflated series. * State Space Models: These are statistical models that use mathematical descriptions of a system to make predictions. The `AutoETS` model from the statsforecast library falls under this category. *[MLForecast](https://github.com/Nixtla/mlforecast)* Machine Learning: Leveraging ML models like `LightGBM`, `XGBoost`, and `LinearRegression` can be advantageous due to their capacity to uncover intricate patterns in data. We’ll use the MLForecast library for this purpose. *[NeuralForecast](https://github.com/Nixtla/neuralforecast)* Deep Learning: DL models, such as Transformers (`AutoTFT`) and Neural Networks (`AutoNHITS`), allow us to handle complex non-linear dependencies in time series data. We’ll utilize the NeuralForecast library for these models. Using the Nixtla suite of libraries, we’ll be able to drive our model selection process with data, ensuring we utilize the most suitable models for specific groups of series in our dataset. Outline: * Reading Data: In this initial step, we load our dataset into memory, making it available for our subsequent analysis and forecasting. It is important to understand the structure and nuances of the dataset at this stage. * Forecasting Using Statistical and Deep Learning Methods: We apply a wide range of forecasting methods from basic statistical techniques to advanced deep learning models. The aim is to generate predictions for the next 28 days based on our dataset. * Model Performance Evaluation on Different Windows: We assess the performance of our models on distinct windows. * Selecting the Best Model for a Group of Series: Using the performance evaluation, we identify the optimal model for each group of series. This step ensures that the chosen model is tailored to the unique characteristics of each group. * Filtering the Best Possible Forecast: Finally, we filter the forecasts generated by our chosen models to obtain the most promising predictions. This is our final output and represents the best possible forecast for each series according to our models. > **Warning** > > This tutorial was originally executed using a `c5d.24xlarge` EC2 > instance. ## Installing Libraries ```python theme={null} %%capture !pip install statsforecast mlforecast neuralforecast datasetforecast s3fs pyarrow ``` ## Download and prepare data The example uses the [M5 dataset](https://github.com/Mcompetitions/M5-methods/blob/master/M5-Competitors-Guide.pdf). It consists of `30,490` bottom time series. ```python theme={null} import pandas as pd ``` ```python theme={null} # Load the training target dataset from the provided URL Y_df = pd.read_parquet('https://m5-benchmarks.s3.amazonaws.com/data/train/target.parquet') # Rename columns to match the Nixtlaverse's expectations # The 'item_id' becomes 'unique_id' representing the unique identifier of the time series # The 'timestamp' becomes 'ds' representing the time stamp of the data points # The 'demand' becomes 'y' representing the target variable we want to forecast Y_df = Y_df.rename(columns={ 'item_id': 'unique_id', 'timestamp': 'ds', 'demand': 'y' }) # Convert the 'ds' column to datetime format to ensure proper handling of date-related operations in subsequent steps Y_df['ds'] = pd.to_datetime(Y_df['ds']) ``` For simplicity sake we will keep just one category ```python theme={null} Y_df = Y_df.query('unique_id.str.startswith("FOODS_3")').reset_index(drop=True) Y_df['unique_id'] = Y_df['unique_id'].astype(str) ``` # Basic Plotting Plot some series using the plot method from the `StatsForecast` class. This method prints 8 random series from the dataset and is useful for basic [EDA](../../src/core/core.html#plot). ```python theme={null} from statsforecast import StatsForecast ``` ```python theme={null} # Feature: plot random series for EDA StatsForecast.plot(Y_df) ``` ```python theme={null} # Feature: plot groups of series for EDA StatsForecast.plot(Y_df, unique_ids=["FOODS_3_432_TX_2"]) ``` ```python theme={null} # Feature: plot groups of series for EDA StatsForecast.plot(Y_df, unique_ids=["FOODS_3_432_TX_2"], engine ='matplotlib') ``` # Create forecasts with Stats, Ml and Neural methods. ## StatsForecast `StatsForecast` is a comprehensive library providing a suite of popular univariate time series forecasting models, all designed with a focus on high performance and scalability. Here’s what makes StatsForecast a powerful tool for time series forecasting: * **Collection of Local Models**: StatsForecast provides a diverse collection of local models that can be applied to each time series individually, allowing us to capture unique patterns within each series. * **Simplicity**: With StatsForecast, training, forecasting, and backtesting multiple models become a straightforward process, requiring only a few lines of code. This simplicity makes it a convenient tool for both beginners and experienced practitioners. * **Optimized for Speed**: The implementation of the models in StatsForecast is optimized for speed, ensuring that large-scale computations are performed efficiently, thereby reducing the overall time for model training and prediction. * **Horizontal Scalability**: One of the distinguishing features of StatsForecast is its ability to scale horizontally. It is compatible with distributed computing frameworks such as Spark, Dask, and Ray. This feature allows it to handle large datasets by distributing the computations across multiple nodes in a cluster, making it a go-to solution for large-scale time series forecasting tasks. `StatsForecast` receives a list of models to fit each time series. Since we are dealing with Daily data, it would be beneficial to use 7 as seasonality. ```python theme={null} # Import necessary models from the statsforecast library from statsforecast.models import ( # SeasonalNaive: A model that uses the previous season's data as the forecast SeasonalNaive, # Naive: A simple model that uses the last observed value as the forecast Naive, # HistoricAverage: This model uses the average of all historical data as the forecast HistoricAverage, # CrostonOptimized: A model specifically designed for intermittent demand forecasting CrostonOptimized, # ADIDA: Adaptive combination of Intermittent Demand Approaches, a model designed for intermittent demand ADIDA, # IMAPA: Intermittent Multiplicative AutoRegressive Average, a model for intermittent series that incorporates autocorrelation IMAPA, # AutoETS: Automated Exponential Smoothing model that automatically selects the best Exponential Smoothing model based on AIC AutoETS ) ``` We fit the models by instantiating a new StatsForecast object with the following parameters: * `models`: a list of models. Select the models you want from models and import them. * `freq`: a string indicating the frequency of the data. (See panda’s available frequencies.) * `n_jobs`: int, number of jobs used in the parallel processing, use -1 for all cores. * `fallback_model`: a model to be used if a model fails. Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame. ```python theme={null} horizon = 28 models = [ SeasonalNaive(season_length=7), Naive(), HistoricAverage(), CrostonOptimized(), ADIDA(), IMAPA(), AutoETS(season_length=7) ] ``` ```python theme={null} # Instantiate the StatsForecast class sf = StatsForecast( models=models, # A list of models to be used for forecasting freq='D', # The frequency of the time series data (in this case, 'D' stands for daily frequency) n_jobs=-1, # The number of CPU cores to use for parallel execution (-1 means use all available cores) ) ``` The forecast method takes two arguments: forecasts next h (horizon) and level. * `h` (int): represents the forecast h steps into the future. In this case, 12 months ahead. * `level` (list of floats): this optional parameter is used for probabilistic forecasting. Set the level (or confidence percentile) of your prediction interval. For example, level=\[90] means that the model expects the real value to be inside that interval 90% of the times. The forecast object here is a new data frame that includes a column with the name of the model and the y hat values, as well as columns for the uncertainty intervals. This block of code times how long it takes to run the forecasting function of the StatsForecast class, which predicts the next 28 days (h=28). The level is set to \[90], meaning it will compute the 90% prediction interval. The time is calculated in minutes and printed out at the end. ```python theme={null} from time import time # Get the current time before forecasting starts, this will be used to measure the execution time init = time() # Call the forecast method of the StatsForecast instance to predict the next 28 days (h=28) # Level is set to [90], which means that it will compute the 90% prediction interval fcst_df = sf.forecast(df=Y_df, h=28, level=[90]) # Get the current time after the forecasting ends end = time() # Calculate and print the total time taken for the forecasting in minutes print(f'Forecast Minutes: {(end - init) / 60}') ``` ```text theme={null} Forecast Minutes: 2.270755163828532 ``` ```python theme={null} fcst_df.head() ``` | | ds | SeasonalNaive | SeasonalNaive-lo-90 | SeasonalNaive-hi-90 | Naive | Naive-lo-90 | Naive-hi-90 | HistoricAverage | HistoricAverage-lo-90 | HistoricAverage-hi-90 | CrostonOptimized | ADIDA | IMAPA | AutoETS | AutoETS-lo-90 | AutoETS-hi-90 | | -------------------- | ---------- | ------------- | ------------------- | ------------------- | ----- | ----------- | ----------- | --------------- | --------------------- | --------------------- | ---------------- | -------- | -------- | -------- | ------------- | ------------- | | unique\_id | | | | | | | | | | | | | | | | | | FOODS\_3\_001\_CA\_1 | 2016-05-23 | 1.0 | -2.847174 | 4.847174 | 2.0 | 0.098363 | 3.901637 | 0.448738 | -1.009579 | 1.907055 | 0.345192 | 0.345477 | 0.347249 | 0.381414 | -1.028122 | 1.790950 | | FOODS\_3\_001\_CA\_1 | 2016-05-24 | 0.0 | -3.847174 | 3.847174 | 2.0 | -0.689321 | 4.689321 | 0.448738 | -1.009579 | 1.907055 | 0.345192 | 0.345477 | 0.347249 | 0.286933 | -1.124136 | 1.698003 | | FOODS\_3\_001\_CA\_1 | 2016-05-25 | 0.0 | -3.847174 | 3.847174 | 2.0 | -1.293732 | 5.293732 | 0.448738 | -1.009579 | 1.907055 | 0.345192 | 0.345477 | 0.347249 | 0.334987 | -1.077614 | 1.747588 | | FOODS\_3\_001\_CA\_1 | 2016-05-26 | 1.0 | -2.847174 | 4.847174 | 2.0 | -1.803274 | 5.803274 | 0.448738 | -1.009579 | 1.907055 | 0.345192 | 0.345477 | 0.347249 | 0.186851 | -1.227280 | 1.600982 | | FOODS\_3\_001\_CA\_1 | 2016-05-27 | 0.0 | -3.847174 | 3.847174 | 2.0 | -2.252190 | 6.252190 | 0.448738 | -1.009579 | 1.907055 | 0.345192 | 0.345477 | 0.347249 | 0.308112 | -1.107548 | 1.723771 | ## MLForecast `MLForecast` is a powerful library that provides automated feature creation for time series forecasting, facilitating the use of global machine learning models. It is designed for high performance and scalability. Key features of MLForecast include: * **Support for sklearn models**: MLForecast is compatible with models that follow the scikit-learn API. This makes it highly flexible and allows it to seamlessly integrate with a wide variety of machine learning algorithms. * **Simplicity**: With MLForecast, the tasks of training, forecasting, and backtesting models can be accomplished in just a few lines of code. This streamlined simplicity makes it user-friendly for practitioners at all levels of expertise. * **Optimized for speed:** MLForecast is engineered to execute tasks rapidly, which is crucial when handling large datasets and complex models. * **Horizontal Scalability:** MLForecast is capable of horizontal scaling using distributed computing frameworks such as Spark, Dask, and Ray. This feature enables it to efficiently process massive datasets by distributing the computations across multiple nodes in a cluster, making it ideal for large-scale time series forecasting tasks. ```python theme={null} from mlforecast import MLForecast from mlforecast.target_transforms import Differences from mlforecast.utils import PredictionIntervals from window_ops.expanding import expanding_mean ``` ```python theme={null} %%capture !pip install lightgbm xgboost ``` ```python theme={null} # Import the necessary models from various libraries # LGBMRegressor: A gradient boosting framework that uses tree-based learning algorithms from the LightGBM library from lightgbm import LGBMRegressor # XGBRegressor: A gradient boosting regressor model from the XGBoost library from xgboost import XGBRegressor # LinearRegression: A simple linear regression model from the scikit-learn library from sklearn.linear_model import LinearRegression ``` To use `MLForecast` for time series forecasting, we instantiate a new `MLForecast` object and provide it with various parameters to tailor the modeling process to our specific needs: * `models`: This parameter accepts a list of machine learning models you wish to use for forecasting. You can import your preferred models from scikit-learn, lightgbm and xgboost. * `freq`: This is a string indicating the frequency of your data (hourly, daily, weekly, etc.). The specific format of this string should align with pandas’ recognized frequency strings. * `target_transforms`: These are transformations applied to the target variable before model training and after model prediction. This can be useful when working with data that may benefit from transformations, such as log-transforms for highly skewed data. * `lags`: This parameter accepts specific lag values to be used as regressors. Lags represent how many steps back in time you want to look when creating features for your model. For example, if you want to use the previous day’s data as a feature for predicting today’s value, you would specify a lag of 1. * `lags_transforms`: These are specific transformations for each lag. This allows you to apply transformations to your lagged features. * `date_features`: This parameter specifies date-related features to be used as regressors. For instance, you might want to include the day of the week or the month as a feature in your model. * `num_threads`: This parameter controls the number of threads to use for parallelizing feature creation, helping to speed up this process when working with large datasets. All these settings are passed to the `MLForecast` constructor. Once the `MLForecast` object is initialized with these settings, we call its `fit` method and pass the historical data frame as the argument. The `fit` method trains the models on the provided historical data, readying them for future forecasting tasks. ```python theme={null} # Instantiate the MLForecast object mlf = MLForecast( models=[LGBMRegressor(), XGBRegressor(), LinearRegression()], # List of models for forecasting: LightGBM, XGBoost and Linear Regression freq='D', # Frequency of the data - 'D' for daily frequency lags=list(range(1, 7)), # Specific lags to use as regressors: 1 to 6 days lag_transforms = { 1: [expanding_mean], # Apply expanding mean transformation to the lag of 1 day }, date_features=['year', 'month', 'day', 'dayofweek', 'quarter', 'week'], # Date features to use as regressors ) ``` Just call the `fit` models to train the select models. In this case we are generating conformal prediction intervals. ```python theme={null} # Start the timer to calculate the time taken for fitting the models init = time() # Fit the MLForecast models to the data, with prediction intervals set using a window size of 28 days mlf.fit(Y_df, prediction_intervals=PredictionIntervals(window_size=28)) # Calculate the end time after fitting the models end = time() # Print the time taken to fit the MLForecast models, in minutes print(f'MLForecast Minutes: {(end - init) / 60}') ``` ```text theme={null} MLForecast Minutes: 2.2809854547182717 ``` After that, just call `predict` to generate forecasts. ```python theme={null} fcst_mlf_df = mlf.predict(28, level=[90]) ``` ```python theme={null} fcst_mlf_df.head() ``` | | unique\_id | ds | LGBMRegressor | XGBRegressor | LinearRegression | LGBMRegressor-lo-90 | LGBMRegressor-hi-90 | XGBRegressor-lo-90 | XGBRegressor-hi-90 | LinearRegression-lo-90 | LinearRegression-hi-90 | | - | -------------------- | ---------- | ------------- | ------------ | ---------------- | ------------------- | ------------------- | ------------------ | ------------------ | ---------------------- | ---------------------- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-05-23 | 0.549520 | 0.598431 | 0.359638 | -0.213915 | 1.312955 | -0.020050 | 1.216912 | 0.030000 | 0.689277 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-05-24 | 0.553196 | 0.337268 | 0.100361 | -0.251383 | 1.357775 | -0.201449 | 0.875985 | -0.216195 | 0.416917 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-05-25 | 0.599668 | 0.349604 | 0.175840 | -0.203974 | 1.403309 | -0.284416 | 0.983624 | -0.150593 | 0.502273 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-05-26 | 0.638097 | 0.322144 | 0.156460 | 0.118688 | 1.157506 | -0.085872 | 0.730160 | -0.273851 | 0.586771 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-05-27 | 0.763305 | 0.300362 | 0.328194 | -0.313091 | 1.839701 | -0.296636 | 0.897360 | -0.657089 | 1.313476 | ## NeuralForecast `NeuralForecast` is a robust collection of neural forecasting models that focuses on usability and performance. It includes a variety of model architectures, from classic networks such as Multilayer Perceptrons (MLP) and Recurrent Neural Networks (RNN) to novel contributions like N-BEATS, N-HITS, Temporal Fusion Transformers (TFT), and more. Key features of `NeuralForecast` include: * A broad collection of global models. Out of the box implementation of MLP, LSTM, RNN, TCN, DilatedRNN, NBEATS, NHITS, ESRNN, TFT, Informer, PatchTST and HINT. * A simple and intuitive interface that allows training, forecasting, and backtesting of various models in a few lines of code. * Support for GPU acceleration to improve computational speed. This machine doesn’t have GPU, but Google Colabs offers some for free. Using [Colab’s GPU to train NeuralForecast](../../../neuralforecast/docs/tutorials/intermittent_data.html). ```python theme={null} # Read the results from Colab fcst_nf_df = pd.read_parquet('https://m5-benchmarks.s3.amazonaws.com/data/forecast-nf.parquet') ``` ```python theme={null} fcst_nf_df.head() ``` | | unique\_id | ds | AutoNHITS | AutoNHITS-lo-90 | AutoNHITS-hi-90 | AutoTFT | AutoTFT-lo-90 | AutoTFT-hi-90 | | - | -------------------- | ---------- | --------- | --------------- | --------------- | ------- | ------------- | ------------- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-05-23 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-05-24 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-05-25 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 1.0 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-05-26 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-05-27 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 2.0 | ```python theme={null} # Merge the forecasts from StatsForecast and NeuralForecast fcst_df = fcst_df.merge(fcst_nf_df, how='left', on=['unique_id', 'ds']) # Merge the forecasts from MLForecast into the combined forecast dataframe fcst_df = fcst_df.merge(fcst_mlf_df, how='left', on=['unique_id', 'ds']) ``` ```python theme={null} fcst_df.head() ``` | | unique\_id | ds | SeasonalNaive | SeasonalNaive-lo-90 | SeasonalNaive-hi-90 | Naive | Naive-lo-90 | Naive-hi-90 | HistoricAverage | HistoricAverage-lo-90 | ... | AutoTFT-hi-90 | LGBMRegressor | XGBRegressor | LinearRegression | LGBMRegressor-lo-90 | LGBMRegressor-hi-90 | XGBRegressor-lo-90 | XGBRegressor-hi-90 | LinearRegression-lo-90 | LinearRegression-hi-90 | | - | -------------------- | ---------- | ------------- | ------------------- | ------------------- | ----- | ----------- | ----------- | --------------- | --------------------- | --- | ------------- | ------------- | ------------ | ---------------- | ------------------- | ------------------- | ------------------ | ------------------ | ---------------------- | ---------------------- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-05-23 | 1.0 | -2.847174 | 4.847174 | 2.0 | 0.098363 | 3.901637 | 0.448738 | -1.009579 | ... | 2.0 | 0.549520 | 0.598431 | 0.359638 | -0.213915 | 1.312955 | -0.020050 | 1.216912 | 0.030000 | 0.689277 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-05-24 | 0.0 | -3.847174 | 3.847174 | 2.0 | -0.689321 | 4.689321 | 0.448738 | -1.009579 | ... | 2.0 | 0.553196 | 0.337268 | 0.100361 | -0.251383 | 1.357775 | -0.201449 | 0.875985 | -0.216195 | 0.416917 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-05-25 | 0.0 | -3.847174 | 3.847174 | 2.0 | -1.293732 | 5.293732 | 0.448738 | -1.009579 | ... | 1.0 | 0.599668 | 0.349604 | 0.175840 | -0.203974 | 1.403309 | -0.284416 | 0.983624 | -0.150593 | 0.502273 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-05-26 | 1.0 | -2.847174 | 4.847174 | 2.0 | -1.803274 | 5.803274 | 0.448738 | -1.009579 | ... | 2.0 | 0.638097 | 0.322144 | 0.156460 | 0.118688 | 1.157506 | -0.085872 | 0.730160 | -0.273851 | 0.586771 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-05-27 | 0.0 | -3.847174 | 3.847174 | 2.0 | -2.252190 | 6.252190 | 0.448738 | -1.009579 | ... | 2.0 | 0.763305 | 0.300362 | 0.328194 | -0.313091 | 1.839701 | -0.296636 | 0.897360 | -0.657089 | 1.313476 | ## Forecast plots ```python theme={null} sf.plot(Y_df, fcst_df, max_insample_length=28 * 3) ``` Use the plot function to explore models and ID’s ```python theme={null} sf.plot(Y_df, fcst_df, max_insample_length=28 * 3, models=['CrostonOptimized', 'AutoNHITS', 'SeasonalNaive', 'LGBMRegressor']) ``` # Validate Model’s Performance The three libraries - `StatsForecast`, `MLForecast`, and `NeuralForecast` - offer out-of-the-box cross-validation capabilities specifically designed for time series. This allows us to evaluate the model’s performance using historical data to obtain an unbiased assessment of how well each model is likely to perform on unseen data.
From the course of Modern Forecasting in Practice
From the course of Modern Forecasting in Practice
## Cross Validation in StatsForecast The `cross_validation` method from the `StatsForecast` class accepts the following arguments: * `df`: A DataFrame representing the training data. * `h` (int): The forecast horizon, represented as the number of steps into the future that we wish to predict. For example, if we’re forecasting hourly data, `h=24` would represent a 24-hour forecast. * `step_size` (int): The step size between each cross-validation window. This parameter determines how often we want to run the forecasting process. * `n_windows` (int): The number of windows used for cross validation. This parameter defines how many past forecasting processes we want to evaluate. These parameters allow us to control the extent and granularity of our cross-validation process. By tuning these settings, we can balance between computational cost and the thoroughness of the cross-validation. ```python theme={null} init = time() cv_df = sf.cross_validation(df=Y_df, h=horizon, n_windows=3, step_size=horizon, level=[90]) end = time() print(f'CV Minutes: {(end - init) / 60}') ``` ```text theme={null} /home/ubuntu/statsforecast/statsforecast/ets.py:1041: RuntimeWarning: divide by zero encountered in double_scalars ``` ```text theme={null} CV Minutes: 5.206169327100118 ``` The cross\_validation\_df object is a new data frame that includes the following columns: * `unique_id` index: (If you dont like working with index just run forecasts\_cv\_df.resetindex()) * `ds`: datestamp or temporal index * `cutoff`: the last datestamp or temporal index for the n\_windows. If n\_windows=1, then one unique cutoff value, if n\_windows=2 then two unique cutoff values. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_df.head() ``` | | ds | cutoff | y | SeasonalNaive | SeasonalNaive-lo-90 | SeasonalNaive-hi-90 | Naive | Naive-lo-90 | Naive-hi-90 | HistoricAverage | HistoricAverage-lo-90 | HistoricAverage-hi-90 | CrostonOptimized | ADIDA | IMAPA | AutoETS | AutoETS-lo-90 | AutoETS-hi-90 | | -------------------- | ---------- | ---------- | --- | ------------- | ------------------- | ------------------- | ----- | ----------- | ----------- | --------------- | --------------------- | --------------------- | ---------------- | -------- | -------- | -------- | ------------- | ------------- | | unique\_id | | | | | | | | | | | | | | | | | | | | FOODS\_3\_001\_CA\_1 | 2016-02-29 | 2016-02-28 | 0.0 | 2.0 | -1.878885 | 5.878885 | 0.0 | -1.917011 | 1.917011 | 0.449111 | -1.021813 | 1.920036 | 0.618472 | 0.618375 | 0.617998 | 0.655286 | -0.765731 | 2.076302 | | FOODS\_3\_001\_CA\_1 | 2016-03-01 | 2016-02-28 | 1.0 | 0.0 | -3.878885 | 3.878885 | 0.0 | -2.711064 | 2.711064 | 0.449111 | -1.021813 | 1.920036 | 0.618472 | 0.618375 | 0.617998 | 0.568595 | -0.853966 | 1.991155 | | FOODS\_3\_001\_CA\_1 | 2016-03-02 | 2016-02-28 | 1.0 | 0.0 | -3.878885 | 3.878885 | 0.0 | -3.320361 | 3.320361 | 0.449111 | -1.021813 | 1.920036 | 0.618472 | 0.618375 | 0.617998 | 0.618805 | -0.805298 | 2.042908 | | FOODS\_3\_001\_CA\_1 | 2016-03-03 | 2016-02-28 | 0.0 | 1.0 | -2.878885 | 4.878885 | 0.0 | -3.834023 | 3.834023 | 0.449111 | -1.021813 | 1.920036 | 0.618472 | 0.618375 | 0.617998 | 0.455891 | -0.969753 | 1.881534 | | FOODS\_3\_001\_CA\_1 | 2016-03-04 | 2016-02-28 | 0.0 | 1.0 | -2.878885 | 4.878885 | 0.0 | -4.286568 | 4.286568 | 0.449111 | -1.021813 | 1.920036 | 0.618472 | 0.618375 | 0.617998 | 0.591197 | -0.835987 | 2.018380 | ## MLForecast The `cross_validation` method from the `MLForecast` class takes the following arguments. * `data`: training data frame * `window_size` (int): represents h steps into the future that are being forecasted. In this case, 24 hours ahead. * `step_size` (int): step size between each window. In other words: how often do you want to run the forecasting processes. * `n_windows` (int): number of windows used for cross-validation. In other words: what number of forecasting processes in the past do you want to evaluate. * `prediction_intervals`: class to compute conformal intervals. ```python theme={null} init = time() cv_mlf_df = mlf.cross_validation( data=Y_df, window_size=horizon, n_windows=3, step_size=horizon, level=[90], ) end = time() print(f'CV Minutes: {(end - init) / 60}') ``` ```text theme={null} /home/ubuntu/miniconda/envs/statsforecast/lib/python3.10/site-packages/mlforecast/forecast.py:576: UserWarning: Excuting `cross_validation` after `fit` can produce unexpected errors /home/ubuntu/miniconda/envs/statsforecast/lib/python3.10/site-packages/mlforecast/forecast.py:468: UserWarning: Please rerun the `fit` method passing a proper value to prediction intervals to compute them. /home/ubuntu/miniconda/envs/statsforecast/lib/python3.10/site-packages/mlforecast/forecast.py:468: UserWarning: Please rerun the `fit` method passing a proper value to prediction intervals to compute them. /home/ubuntu/miniconda/envs/statsforecast/lib/python3.10/site-packages/mlforecast/forecast.py:468: UserWarning: Please rerun the `fit` method passing a proper value to prediction intervals to compute them. ``` ```text theme={null} CV Minutes: 2.961174162228902 ``` The cross\_validation\_df object is a new data frame that includes the following columns: * `unique_id` index: (If you dont like working with index just run forecasts\_cv\_df.resetindex()) * `ds`: datestamp or temporal index * `cutoff`: the last datestamp or temporal index for the n\_windows. If n\_windows=1, then one unique cutoff value, if n\_windows=2 then two unique cutoff values. * `y`: true value * `"model"`: columns with the model’s name and fitted value. ```python theme={null} cv_mlf_df.head() ``` | | unique\_id | ds | cutoff | y | LGBMRegressor | XGBRegressor | LinearRegression | | - | -------------------- | ---------- | ---------- | --- | ------------- | ------------ | ---------------- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-02-29 | 2016-02-28 | 0.0 | 0.435674 | 0.556261 | -0.312492 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-03-01 | 2016-02-28 | 1.0 | 0.639676 | 0.625806 | -0.041924 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-03-02 | 2016-02-28 | 1.0 | 0.792989 | 0.659650 | 0.263699 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-03-03 | 2016-02-28 | 0.0 | 0.806868 | 0.535121 | 0.482491 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-03-04 | 2016-02-28 | 0.0 | 0.829106 | 0.313353 | 0.677326 | ## NeuralForecast This machine doesn’t have GPU, but Google Colabs offers some for free. Using [Colab’s GPU to train NeuralForecast](../../../neuralforecast/docs/tutorials/intermittent_data.html). ```python theme={null} cv_nf_df = pd.read_parquet('https://m5-benchmarks.s3.amazonaws.com/data/cross-validation-nf.parquet') ``` ```python theme={null} cv_nf_df.head() ``` | | unique\_id | ds | cutoff | AutoNHITS | AutoNHITS-lo-90 | AutoNHITS-hi-90 | AutoTFT | AutoTFT-lo-90 | AutoTFT-hi-90 | y | | - | -------------------- | ---------- | ---------- | --------- | --------------- | --------------- | ------- | ------------- | ------------- | --- | | 0 | FOODS\_3\_001\_CA\_1 | 2016-02-29 | 2016-02-28 | 0.0 | 0.0 | 2.0 | 1.0 | 0.0 | 2.0 | 0.0 | | 1 | FOODS\_3\_001\_CA\_1 | 2016-03-01 | 2016-02-28 | 0.0 | 0.0 | 2.0 | 1.0 | 0.0 | 2.0 | 1.0 | | 2 | FOODS\_3\_001\_CA\_1 | 2016-03-02 | 2016-02-28 | 0.0 | 0.0 | 2.0 | 1.0 | 0.0 | 2.0 | 1.0 | | 3 | FOODS\_3\_001\_CA\_1 | 2016-03-03 | 2016-02-28 | 0.0 | 0.0 | 2.0 | 1.0 | 0.0 | 2.0 | 0.0 | | 4 | FOODS\_3\_001\_CA\_1 | 2016-03-04 | 2016-02-28 | 0.0 | 0.0 | 2.0 | 1.0 | 0.0 | 2.0 | 0.0 | ## Merge cross validation forecasts ```python theme={null} cv_df = cv_df.merge(cv_nf_df.drop(columns=['y']), how='left', on=['unique_id', 'ds', 'cutoff']) cv_df = cv_df.merge(cv_mlf_df.drop(columns=['y']), how='left', on=['unique_id', 'ds', 'cutoff']) ``` ## Plots CV ```python theme={null} cutoffs = cv_df['cutoff'].unique() ``` ```python theme={null} for cutoff in cutoffs: img = sf.plot( Y_df, cv_df.query('cutoff == @cutoff').drop(columns=['y', 'cutoff']), max_insample_length=28 * 5, unique_ids=['FOODS_3_001_CA_1'], ) img.show() ``` ### Aggregate Demand ```python theme={null} agg_cv_df = cv_df.loc[:,~cv_df.columns.str.contains('hi|lo')].groupby(['ds', 'cutoff']).sum(numeric_only=True).reset_index() agg_cv_df.insert(0, 'unique_id', 'agg_demand') ``` ```python theme={null} agg_Y_df = Y_df.groupby(['ds']).sum(numeric_only=True).reset_index() agg_Y_df.insert(0, 'unique_id', 'agg_demand') ``` ```python theme={null} for cutoff in cutoffs: img = sf.plot( agg_Y_df, agg_cv_df.query('cutoff == @cutoff').drop(columns=['y', 'cutoff']), max_insample_length=28 * 5, ) img.show() ``` ## Evaluation per series and CV window In this section, we will evaluate the performance of each model for each time series and each cross validation window. Since we have many combinations, we will use `dask` to parallelize the evaluation. The parallelization will be done using `fugue`. ```python theme={null} from typing import List, Callable from distributed import Client from fugue import transform from fugue_dask import DaskExecutionEngine from datasetsforecast.losses import mse, mae, smape ``` The `evaluate` function receives a unique combination of a time series and a window, and calculates different `metrics` for each model in `df`. ```python theme={null} def evaluate(df: pd.DataFrame, metrics: List[Callable]) -> pd.DataFrame: eval_ = {} models = df.loc[:, ~df.columns.str.contains('unique_id|y|ds|cutoff|lo|hi')].columns for model in models: eval_[model] = {} for metric in metrics: eval_[model][metric.__name__] = metric(df['y'], df[model]) eval_df = pd.DataFrame(eval_).rename_axis('metric').reset_index() eval_df.insert(0, 'cutoff', df['cutoff'].iloc[0]) eval_df.insert(0, 'unique_id', df['unique_id'].iloc[0]) return eval_df ``` ```python theme={null} str_models = cv_df.loc[:, ~cv_df.columns.str.contains('unique_id|y|ds|cutoff|lo|hi')].columns str_models = ','.join([f"{model}:float" for model in str_models]) cv_df['cutoff'] = cv_df['cutoff'].astype(str) cv_df['unique_id'] = cv_df['unique_id'].astype(str) ``` Let’s create a `dask` client. ```python theme={null} client = Client() # without this, dask is not in distributed mode # fugue.dask.dataframe.default.partitions determines the default partitions for a new DaskDataFrame engine = DaskExecutionEngine({"fugue.dask.dataframe.default.partitions": 96}) ``` The `transform` function takes the `evaluate` functions and applies it to each combination of time series (`unique_id`) and cross validation window (`cutoff`) using the `dask` client we created before. ```python theme={null} evaluation_df = transform( cv_df.loc[:, ~cv_df.columns.str.contains('lo|hi')], evaluate, engine="dask", params={'metrics': [mse, mae, smape]}, schema=f"unique_id:str,cutoff:str,metric:str, {str_models}", as_local=True, partition={'by': ['unique_id', 'cutoff']} ) ``` ```text theme={null} /home/ubuntu/miniconda/envs/statsforecast/lib/python3.10/site-packages/distributed/client.py:3109: UserWarning: Sending large graph of size 49.63 MiB. This may cause some slowdown. Consider scattering data ahead of time and using futures. ``` ```python theme={null} evaluation_df.head() ``` | | unique\_id | cutoff | metric | SeasonalNaive | Naive | HistoricAverage | CrostonOptimized | ADIDA | IMAPA | AutoETS | AutoNHITS | AutoTFT | LGBMRegressor | XGBRegressor | LinearRegression | | - | -------------------- | ---------- | ------ | ------------- | --------- | --------------- | ---------------- | ---------- | ---------- | ---------- | --------- | --------- | ------------- | ------------ | ---------------- | | 0 | FOODS\_3\_003\_WI\_3 | 2016-02-28 | mse | 1.142857 | 1.142857 | 0.816646 | 0.816471 | 1.142857 | 1.142857 | 1.142857 | 1.142857 | 1.142857 | 0.832010 | 1.020361 | 0.887121 | | 1 | FOODS\_3\_003\_WI\_3 | 2016-02-28 | mae | 0.571429 | 0.571429 | 0.729592 | 0.731261 | 0.571429 | 0.571429 | 0.571429 | 0.571429 | 0.571429 | 0.772788 | 0.619949 | 0.685413 | | 2 | FOODS\_3\_003\_WI\_3 | 2016-02-28 | smape | 71.428574 | 71.428574 | 158.813507 | 158.516235 | 200.000000 | 200.000000 | 200.000000 | 71.428574 | 71.428574 | 145.901947 | 188.159164 | 178.883743 | | 3 | FOODS\_3\_013\_CA\_3 | 2016-04-24 | mse | 4.000000 | 6.214286 | 2.406764 | 3.561202 | 2.267853 | 2.267600 | 2.268677 | 2.750000 | 2.125000 | 2.160508 | 2.370228 | 2.289606 | | 4 | FOODS\_3\_013\_CA\_3 | 2016-04-24 | mae | 1.500000 | 2.142857 | 1.214286 | 1.340446 | 1.214286 | 1.214286 | 1.214286 | 1.107143 | 1.142857 | 1.140084 | 1.157548 | 1.148813 | ```python theme={null} # Calculate the mean metric for each cross validation window evaluation_df.groupby(['cutoff', 'metric']).mean(numeric_only=True) ``` | | | SeasonalNaive | Naive | HistoricAverage | CrostonOptimized | ADIDA | IMAPA | AutoETS | AutoNHITS | AutoTFT | LGBMRegressor | XGBRegressor | LinearRegression | | ---------- | ------ | ------------- | --------- | --------------- | ---------------- | ---------- | ---------- | ---------- | --------- | --------- | ------------- | ------------ | ---------------- | | cutoff | metric | | | | | | | | | | | | | | 2016-02-28 | mae | 1.744289 | 2.040496 | 1.730704 | 1.633017 | 1.527965 | 1.528772 | 1.497553 | 1.434938 | 1.485419 | 1.688403 | 1.514102 | 1.576320 | | | mse | 14.510710 | 19.080585 | 12.858994 | 11.785032 | 11.114497 | 11.100909 | 10.347847 | 10.010982 | 10.964664 | 10.436206 | 10.968788 | 10.792831 | | | smape | 85.202042 | 87.719086 | 125.418488 | 124.749908 | 127.591858 | 127.704102 | 127.790672 | 79.132614 | 80.983368 | 118.489983 | 140.420578 | 127.043137 | | 2016-03-27 | mae | 1.795973 | 2.106449 | 1.754029 | 1.662087 | 1.570701 | 1.572741 | 1.535301 | 1.432412 | 1.502393 | 1.712493 | 1.600193 | 1.601612 | | | mse | 14.810259 | 26.044472 | 12.804104 | 12.020620 | 12.083861 | 12.120033 | 11.315013 | 9.445867 | 10.762877 | 10.723589 | 12.924312 | 10.943772 | | | smape | 87.407471 | 89.453247 | 123.587196 | 123.460030 | 123.428459 | 123.538521 | 123.612991 | 79.926781 | 82.013168 | 116.089699 | 138.885941 | 127.304871 | | 2016-04-24 | mae | 1.785983 | 1.990774 | 1.762506 | 1.609268 | 1.527627 | 1.529721 | 1.501820 | 1.447401 | 1.505127 | 1.692946 | 1.541845 | 1.590985 | | | mse | 13.476350 | 16.234917 | 13.151311 | 10.647048 | 10.072225 | 10.062395 | 9.393439 | 9.363891 | 10.436214 | 10.347073 | 10.774202 | 10.608137 | | | smape | 89.238815 | 90.685867 | 121.124947 | 119.721245 | 120.325401 | 120.345284 | 120.649582 | 81.402748 | 83.614029 | 113.334198 | 136.755234 | 124.618622 | Results showed in previous experiments. | model | MSE | | :---------------- | ----: | | MQCNN | 10.09 | | DeepAR-student\_t | 10.11 | | DeepAR-lognormal | 30.20 | | DeepAR | 9.13 | | NPTS | 11.53 | Top 3 models: DeepAR, AutoNHITS, AutoETS. ### Distribution of errors ```python theme={null} %%capture !pip install seaborn ``` ```python theme={null} import matplotlib.pyplot as plt import seaborn as sns ``` ```python theme={null} evaluation_df_melted = pd.melt(evaluation_df, id_vars=['unique_id', 'cutoff', 'metric'], var_name='model', value_name='error') ``` #### SMAPE ```python theme={null} sns.violinplot(evaluation_df_melted.query('metric=="smape"'), x='error', y='model') ``` ### Choose models for groups of series Feature: * A unified dataframe with forecasts for all different models * Easy Ensamble * E.g. Average predictions * Or MinMax (Choosing is ensembling) ```python theme={null} # Choose the best model for each time series, metric, and cross validation window evaluation_df['best_model'] = evaluation_df.idxmin(axis=1, numeric_only=True) # count how many times a model wins per metric and cross validation window count_best_model = evaluation_df.groupby(['cutoff', 'metric', 'best_model']).size().rename('n').to_frame().reset_index() # plot results sns.barplot(count_best_model, x='n', y='best_model', hue='metric') ``` ### Et pluribus unum: an inclusive forecasting Pie. ```python theme={null} # For the mse, calculate how many times a model wins eval_series_df = evaluation_df.query('metric == "mse"').groupby(['unique_id']).mean(numeric_only=True) eval_series_df['best_model'] = eval_series_df.idxmin(axis=1) counts_series = eval_series_df.value_counts('best_model') plt.pie(counts_series, labels=counts_series.index, autopct='%.0f%%') plt.show() ``` ```python theme={null} sf.plot(Y_df, cv_df.drop(columns=['cutoff', 'y']), max_insample_length=28 * 6, models=['AutoNHITS'], unique_ids=eval_series_df.query('best_model == "AutoNHITS"').index[:8]) ``` # Choose Forecasting method for different groups of series ```python theme={null} # Merge the best model per time series dataframe # and filter the forecasts based on that dataframe # for each time series fcst_df = pd.melt(fcst_df.set_index('unique_id'), id_vars=['ds'], var_name='model', value_name='forecast', ignore_index=False) fcst_df = fcst_df.join(eval_series_df[['best_model']]) fcst_df[['model', 'pred-interval']] = fcst_df['model'].str.split('-', expand=True, n=1) fcst_df = fcst_df.query('model == best_model') fcst_df['name'] = [f'forecast-{x}' if x is not None else 'forecast' for x in fcst_df['pred-interval']] fcst_df = pd.pivot_table(fcst_df, index=['unique_id', 'ds'], values=['forecast'], columns=['name']).droplevel(0, axis=1).reset_index() ``` ```python theme={null} sf.plot(Y_df, fcst_df, max_insample_length=28 * 3) ``` # Technical Debt * Train the statistical models in the full dataset. * Increase the number of `num_samples` in the neural auto models. * Include other models such as `Theta`, `ARIMA`, `RNN`, `LSTM`, … # Further materials * [Available Models StatsForecast](../../src/core/models.html) * [Available Models NeuralForecast](../../../neuralforecast/models.html) * [Scalers and Loss Functions](../../../neuralforecast/losses.pytorch.html) * [Getting Started NeuralForecast](../../../neuralforecast/tutorials/getting_started_complete.html) * [Hierarchical Reconciliation](../../../hierarchicalforecast/examples/tourismsmall.html) * [Distributed ML Forecast (trees)](../../../mlforecast/docs/getting-started/quick_start_distributed.html) * [Using StatsForecast to train millions of time series](https://www.anyscale.com/blog/how-nixtla-uses-ray-to-accurately-predict-more-than-a-million-time-series) * [Intermittent Demand Forecasting With Nixtla on Databricks](https://www.databricks.com/blog/2022/12/06/intermittent-demand-forecasting-nixtla-databricks.html) # Probabilistic Forecasting | StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/uncertaintyintervals.html > In this example, we’ll implement prediction intervals > **Prerequisites** > > This tutorial assumes basic familiarity with StatsForecast. For a > minimal example visit the [Quick > Start](../getting-started/getting_started_short.html) ## Introduction When we generate a forecast, we usually produce a single value known as the point forecast. This value, however, doesn’t tell us anything about the uncertainty associated with the forecast. To have a measure of this uncertainty, we need **prediction intervals**. A prediction interval is a range of values that the forecast can take with a given probability. Hence, a 95% prediction interval should contain a range of values that include the actual future value with probability 95%. Probabilistic forecasting aims to generate the full forecast distribution. Point forecasting, on the other hand, usually returns the mean or the median or said distribution. However, in real-world scenarios, it is better to forecast not only the most probable future outcome, but many alternative outcomes as well. [StatsForecast](../../index.html) has many models that can generate point forecasts. It also has probabilistic models than generate the same point forecasts and their prediction intervals. These models are stochastic data generating processes that can produce entire forecast distributions. By the end of this tutorial, you’ll have a good understanding of the probabilistic models available in StatsForecast and will be able to use them to generate point forecasts and prediction intervals. Furthermore, you’ll also learn how to generate plots with the historical data, the point forecasts, and the prediction intervals. > **Important** > > Although the terms are often confused, prediction intervals are not > the same as [confidence > intervals](https://robjhyndman.com/hyndsight/intervals/). > **Warning** > > In practice, most prediction intervals are too narrow since models do > not account for all sources of uncertainty. A discussion about this > can be found [here](https://robjhyndman.com/hyndsight/narrow-pi/). **Outline:** 1. Install libraries 2. Load and explore the data 3. Train models 4. Plot prediction intervals > **Tip** > > You can use Colab to run this Notebook interactively > > > Open In Colab > ## Install libraries We assume that you have StatsForecast already installed. If not, check this guide for instructions on [how to install StatsForecast](../getting-started/installation.html) Install the necessary packages using `pip install statsforecast` ```python theme={null} %pip install -U statsforecast ``` ## Load and explore the data For this example, we’ll use the hourly dataset from the [M4 Competition](https://www.sciencedirect.com/science/article/pii/S0169207019301128). We first need to download the data from a URL and then load it as a `pandas` dataframe. Notice that we’ll load the train and the test data separately. We’ll also rename the `y` column of the test data as `y_test`. ```python theme={null} import pandas as pd ``` ```python theme={null} train = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly.csv') test = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly-test.csv').rename(columns={'y': 'y_test'}) ``` ```python theme={null} train.head() ``` | | unique\_id | ds | y | | - | ---------- | -- | ----- | | 0 | H1 | 1 | 605.0 | | 1 | H1 | 2 | 586.0 | | 2 | H1 | 3 | 586.0 | | 3 | H1 | 4 | 559.0 | | 4 | H1 | 5 | 511.0 | ```python theme={null} test.head() ``` | | unique\_id | ds | y\_test | | - | ---------- | --- | ------- | | 0 | H1 | 701 | 619.0 | | 1 | H1 | 702 | 565.0 | | 2 | H1 | 703 | 532.0 | | 3 | H1 | 704 | 495.0 | | 4 | H1 | 705 | 481.0 | Since the goal of this notebook is to generate prediction intervals, we’ll only use the first 8 series of the dataset to reduce the total computational time. ```python theme={null} n_series = 8 uids = train['unique_id'].unique()[:n_series] # select first n_series of the dataset train = train.query('unique_id in @uids') test = test.query('unique_id in @uids') ``` We can plot these series using the `statsforecast.plot` method from the [StatsForecast](../../src/core/core.html#statsforecast) class. This method has multiple parameters, and the required ones to generate the plots in this notebook are explained below. * `df`: A `pandas` dataframe with columns \[`unique_id`, `ds`, `y`]. * `forecasts_df`: A `pandas` dataframe with columns \[`unique_id`, `ds`] and models. * `plot_random`: bool = `True`. Plots the time series randomly. * `models`: List\[str]. A list with the models we want to plot. * `level`: List\[float]. A list with the prediction intervals we want to plot. * `engine`: str = `plotly`. It can also be `matplotlib`. `plotly` generates interactive plots, while `matplotlib` generates static plots. ```python theme={null} from statsforecast import StatsForecast ``` ```python theme={null} StatsForecast.plot(train, test, plot_random=False) ``` ## Train models StatsForecast can train multiple [models](../../src/core/models.html) on different time series efficiently. Most of these models can generate a probabilistic forecast, which means that they can produce both point forecasts and prediction intervals. For this example, we’ll use [AutoETS](../../src/core/models.html#autoets) and the following baseline models: * [HistoricAverage](../../src/core/models.html#historicaverage) * [Naive](../../src/core/models.html#naive) * [RandomWalkWithDrift](../../src/core/models.html#randomwalkwithdrift) * [SeasonalNaive](../../src/core/models.html#seasonalnaive) To use these models, we first need to import them from `statsforecast.models` and then we need to instantiate them. Given that we’re working with hourly data, we need to set `seasonal_length=24` in the models that requiere this parameter. ```python theme={null} from statsforecast.models import ( AutoETS, HistoricAverage, Naive, RandomWalkWithDrift, SeasonalNaive ) ``` ```python theme={null} # Create a list of models and instantiation parameters models = [ AutoETS(season_length=24), HistoricAverage(), Naive(), RandomWalkWithDrift(), SeasonalNaive(season_length=24) ] ``` To instantiate a new StatsForecast object, we need the following parameters: * `df`: The dataframe with the training data. * `models`: The list of models defined in the previous step. * `freq`: A string indicating the frequency of the data. See [pandas’ available frequencies](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases). * `n_jobs`: An integer that indicates the number of jobs used in parallel processing. Use -1 to select all cores. ```python theme={null} sf = StatsForecast( models=models, freq=1, n_jobs=-1 ) ``` Now we’re ready to generate the point forecasts and the prediction intervals. To do this, we’ll use the `forecast` method, which takes two arguments: * `h`: An integer that represent the forecasting horizon. In this case, we’ll forecast the next 48 hours. * `level`: A list of floats with the confidence levels of the prediction intervals. For example, `level=[95]` means that the range of values should include the actual future value with probability 95%. ```python theme={null} levels = [80, 90, 95, 99] # confidence levels of the prediction intervals forecasts = sf.forecast(df=train, h=48, level=levels) forecasts.head() ``` | | unique\_id | ds | AutoETS | AutoETS-lo-99 | AutoETS-lo-95 | AutoETS-lo-90 | AutoETS-lo-80 | AutoETS-hi-80 | AutoETS-hi-90 | AutoETS-hi-95 | ... | RWD-hi-99 | SeasonalNaive | SeasonalNaive-lo-80 | SeasonalNaive-lo-90 | SeasonalNaive-lo-95 | SeasonalNaive-lo-99 | SeasonalNaive-hi-80 | SeasonalNaive-hi-90 | SeasonalNaive-hi-95 | SeasonalNaive-hi-99 | | - | ---------- | --- | ---------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | --- | ---------- | ------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | 0 | H1 | 701 | 631.889598 | 533.371822 | 556.926831 | 568.978861 | 582.874079 | 680.905116 | 694.800335 | 706.852365 | ... | 789.416619 | 691.0 | 613.351903 | 591.339747 | 572.247484 | 534.932739 | 768.648097 | 790.660253 | 809.752516 | 847.067261 | | 1 | H1 | 702 | 559.750830 | 460.738592 | 484.411824 | 496.524343 | 510.489302 | 609.012359 | 622.977317 | 635.089836 | ... | 833.254152 | 618.0 | 540.351903 | 518.339747 | 499.247484 | 461.932739 | 695.648097 | 717.660253 | 736.752516 | 774.067261 | | 2 | H1 | 703 | 519.235476 | 419.731233 | 443.522100 | 455.694808 | 469.729161 | 568.741792 | 582.776145 | 594.948853 | ... | 866.990616 | 563.0 | 485.351903 | 463.339747 | 444.247484 | 406.932739 | 640.648097 | 662.660253 | 681.752516 | 719.067261 | | 3 | H1 | 704 | 486.973364 | 386.979536 | 410.887460 | 423.120060 | 437.223465 | 536.723263 | 550.826668 | 563.059268 | ... | 895.510095 | 529.0 | 451.351903 | 429.339747 | 410.247484 | 372.932739 | 606.648097 | 628.660253 | 647.752516 | 685.067261 | | 4 | H1 | 705 | 464.697366 | 364.216339 | 388.240749 | 400.532950 | 414.705071 | 514.689661 | 528.861782 | 541.153983 | ... | 920.702904 | 504.0 | 426.351903 | 404.339747 | 385.247484 | 347.932739 | 581.648097 | 603.660253 | 622.752516 | 660.067261 | We’ll now merge the forecasts and their prediction intervals with the test set. This will allow us generate the plots of each probabilistic model. ```python theme={null} test = test.merge(forecasts, how='left', on=['unique_id', 'ds']) ``` ## Plot prediction intervals To plot the point and the prediction intervals, we’ll use the `statsforecast.plot` method again. Notice that now we also need to specify the model and the levels that we want to plot. ### AutoETS ```python theme={null} sf.plot(train, test, plot_random=False, models=['AutoETS'], level=levels) ``` ### Historic Average ```python theme={null} sf.plot(train, test, plot_random=False, models=['HistoricAverage'], level=levels) ``` ### Naive ```python theme={null} sf.plot(train, test, plot_random=False, models=['Naive'], level=levels) ``` ### Random Walk with Drift ```python theme={null} sf.plot(train, test, plot_random=False, models=['RWD'], level=levels) ``` ### Seasonal Naive ```python theme={null} sf.plot(train, test, plot_random=False, models=['SeasonalNaive'], level=levels) ``` From these plots, we can conclude that the uncertainty around each forecast varies according to the model that is being used. For the same time series, one model can predict a wider range of possible future values than others. ## References [Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, The Statistical Forecasting Perspective”](https://otexts.com/fpp3/perspective.html). # Statistical ⚡️ Forecast Source: https://nixtlaverse.nixtla.io/statsforecast/index.html Lightning fast forecasting with statistical and econometric models ## Installation You can install `StatsForecast` with: ```python theme={null} pip install statsforecast ``` or ```python theme={null} conda install -c conda-forge statsforecast ``` Vist our [Installation Guide](https://nixtlaverse.nixtla.io/statsforecast/docs/getting-started/installation.html) for further instructions. ## Quick Start **Minimal Example** ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoARIMA from statsforecast.utils import AirPassengersDF df = AirPassengersDF sf = StatsForecast( models=[AutoARIMA(season_length=12)], freq='ME', ) sf.fit(df) sf.predict(h=12, level=[95]) ``` **Get Started [quick guide](https://nixtlaverse.nixtla.io/statsforecast/docs/getting-started/getting_started_short.html)** **Follow this [end-to-end walkthrough](https://nixtlaverse.nixtla.io/statsforecast/docs/getting-started/getting_started_complete.html) for best practices.** ## Why? Current Python alternatives for statistical models are slow, inaccurate and don't scale well. So we created a library that can be used to forecast in production environments or as benchmarks. `StatsForecast` includes an extensive battery of models that can efficiently fit millions of time series. ## Features * Fastest and most accurate implementations of `AutoARIMA`, `AutoETS`, `AutoCES`, `MSTL` and `Theta` in Python. * Out-of-the-box compatibility with Spark, Dask, and Ray. * Probabilistic Forecasting and Confidence Intervals. * Support for exogenous Variables and static covariates. * Anomaly Detection. * Familiar sklearn syntax: `.fit` and `.predict`. ## Highlights * Inclusion of `exogenous variables` and `prediction intervals` for ARIMA. * 20x [faster](https://github.com/Nixtla/statsforecast/tree/main/experiments/arima) than `pmdarima`. * 1.5x faster than `R`. * 500x faster than `Prophet`. * 4x [faster](https://github.com/Nixtla/statsforecast/tree/main/experiments/ets) than `statsmodels`. * 1,000,000 series in [30 min](https://github.com/Nixtla/statsforecast/tree/main/experiments/ray) with [ray](https://github.com/ray-project/ray). * Replace FB-Prophet in two lines of code and gain speed and accuracy. Check the experiments [here](https://github.com/Nixtla/statsforecast/tree/main/experiments/arima_prophet_adapter). * Fit 10 benchmark models on **1,000,000** series in [under **5 min**](https://github.com/Nixtla/statsforecast/tree/main/experiments/benchmarks_at_scale/). Missing something? Please open an issue or write us in [![Slack](https://img.shields.io/badge/Slack-4A154B?\&logo=slack\&logoColor=white)](https://join.slack.com/t/nixtlaworkspace/shared_invite/zt-135dssye9-fWTzMpv2WBthq8NK0Yvu6A) ## Examples and Guides 📚 [End to End Walkthrough](https://nixtlaverse.nixtla.io/statsforecast/docs/getting-started/getting_started_complete.html): Model training, evaluation and selection for multiple time series 🔎 [Anomaly Detection](https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/anomalydetection.html): detect anomalies for time series using in-sample prediction intervals. 👩‍🔬 [Cross Validation](https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/crossvalidation.html): robust model’s performance evaluation. ❄️ [Multiple Seasonalities](https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/multipleseasonalities.html): how to forecast data with multiple seasonalities using an MSTL. 🔌 [Predict Demand Peaks](https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/electricitypeakforecasting.html): electricity load forecasting for detecting daily peaks and reducing electric bills. 📈 [Intermittent Demand](https://nixtlaverse.nixtla.io/statsforecast/docs/tutorials/intermittentdata.html): forecast series with very few non-zero observations. 🌡️ [Exogenous Regressors](https://nixtlaverse.nixtla.io/statsforecast/docs/how-to-guides/exogenous.html): like weather or prices ## Models ### Automatic Forecasting Automatic forecasting tools search for the best parameters and select the best possible model for a group of time series. These tools are useful for large collections of univariate time series. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | Exogenous features | | :-------------------------------------------------------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | :----------------: | | [AutoARIMA](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#autoarima) | ✅ | ✅ | ✅ | ✅ | ✅ | | [AutoETS](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#autoets) | ✅ | ✅ | ✅ | ✅ | | | [AutoCES](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#autoces) | ✅ | ✅ | ✅ | ✅ | | | [AutoTheta](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#autotheta) | ✅ | ✅ | ✅ | ✅ | | | [AutoMFLES](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#automfles) | ✅ | ✅ | ✅ | ✅ | ✅ | | [AutoTBATS](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#autotbats) | ✅ | ✅ | ✅ | ✅ | | ### ARIMA Family These models exploit the existing autocorrelations in the time series. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | Exogenous features | | :------------------------------------------------------------------------------------------------ | :------------: | :--------------------: | :--------------------: | :-------------------------: | :----------------: | | [ARIMA](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#arima) | ✅ | ✅ | ✅ | ✅ | ✅ | | [AutoRegressive](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#autoregressive) | ✅ | ✅ | ✅ | ✅ | ✅ | ### Theta Family Fit two theta lines to a deseasonalized time series, using different techniques to obtain and combine the two theta lines to produce the final forecasts. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | Exogenous features | | :-------------------------------------------------------------------------------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | :----------------: | | [Theta](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#theta) | ✅ | ✅ | ✅ | ✅ | ✅ | | [OptimizedTheta](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#optimizedtheta) | ✅ | ✅ | ✅ | ✅ | | | [DynamicTheta](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#dynamictheta) | ✅ | ✅ | ✅ | ✅ | | | [DynamicOptimizedTheta](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#dynamicoptimizedtheta) | ✅ | ✅ | ✅ | ✅ | | ### Multiple Seasonalities Suited for signals with more than one clear seasonality. Useful for low-frequency data like electricity and logs. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | Exogenous features | | :------------------------------------------------------------------------------ | :------------: | :--------------------: | :--------------------: | :-------------------------: | :--------------------------: | | [MSTL](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#mstl) | ✅ | ✅ | ✅ | ✅ | If trend forecaster supports | | [MFLES](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#mfles) | ✅ | ✅ | ✅ | ✅ | ✅ | | [TBATS](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#tbats) | ✅ | ✅ | ✅ | ✅ | | ### GARCH and ARCH Models Suited for modeling time series that exhibit non-constant volatility over time. The ARCH model is a particular case of GARCH. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | Exogenous features | | :------------------------------------------------------------------------------ | :------------: | :--------------------: | :--------------------: | :-------------------------: | :----------------: | | [GARCH](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#garch) | ✅ | ✅ | ✅ | ✅ | | | [ARCH](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#arch) | ✅ | ✅ | ✅ | ✅ | | ### Baseline Models Classical models for establishing baseline. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | Exogenous features | | :-------------------------------------------------------------------------------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | :----------------: | | [HistoricAverage](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#historicaverage) | ✅ | ✅ | ✅ | ✅ | | | [Naive](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#naive) | ✅ | ✅ | ✅ | ✅ | | | [RandomWalkWithDrift](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#randomwalkwithdrift) | ✅ | ✅ | ✅ | ✅ | | | [SeasonalNaive](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#seasonalnaive) | ✅ | ✅ | ✅ | ✅ | | | [WindowAverage](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#windowaverage) | ✅ | | | | | | [SeasonalWindowAverage](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#seasonalwindowaverage) | ✅ | | | | | ### Exponential Smoothing Uses a weighted average of all past observations where the weights decrease exponentially into the past. Suitable for data with clear trend and/or seasonality. Use the `SimpleExponential` family for data with no clear trend or seasonality. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | Exogenous features | | :---------------------------------------------------------------------------------------------------------------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | :----------------: | | [SimpleExponentialSmoothing](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#simpleexponentialsmoothing) | ✅ | | ✅ | | | | [SimpleExponentialSmoothingOptimized](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#simpleexponentialsmoothingoptimized) | ✅ | | ✅ | | | | [SeasonalExponentialSmoothing](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#seasonalexponentialsmoothing) | ✅ | | ✅ | | | | [SeasonalExponentialSmoothingOptimized](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#seasonalexponentialsmoothingoptimized) | ✅ | | ✅ | | | | [Holt](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#holt) | ✅ | ✅ | ✅ | ✅ | | | [HoltWinters](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#holtwinters) | ✅ | ✅ | ✅ | ✅ | | ### Sparse or Inttermitent Suited for series with very few non-zero observations | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | Exogenous features | | :---------------------------------------------------------------------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | :----------------: | | [ADIDA](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#adida) | ✅ | | ✅ | ✅ | | | [CrostonClassic](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#crostonclassic) | ✅ | | ✅ | ✅ | | | [CrostonOptimized](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#crostonoptimized) | ✅ | | ✅ | ✅ | | | [CrostonSBA](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#crostonsba) | ✅ | | ✅ | ✅ | | | [IMAPA](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#imapa) | ✅ | | ✅ | ✅ | | | [TSB](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#tsb) | ✅ | | ✅ | ✅ | | ## 🔨 How to contribute See [CONTRIBUTING.md](https://github.com/Nixtla/statsforecast/blob/main/CONTRIBUTING.md). ## Citing ```bibtex theme={null} @misc{garza2022statsforecast, author={Azul Garza, Max Mergenthaler Canseco, Cristian Challú, Kin G. Olivares}, title = {{StatsForecast}: Lightning fast forecasting with statistical and econometric models}, year={2022}, howpublished={{PyCon} Salt Lake City, Utah, US 2022}, url={https://github.com/Nixtla/statsforecast} } ``` ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
azul
azul

💻 🚧
José Morales
José Morales

💻 🚧
Sugato Ray
Sugato Ray

💻
Jeff Tackes
Jeff Tackes

🐛
darinkist
darinkist

🤔
Alec Helyar
Alec Helyar

💬
Dave Hirschfeld
Dave Hirschfeld

💬
mergenthaler
mergenthaler

💻
Kin
Kin

💻
Yasslight90
Yasslight90

🤔
asinig
asinig

🤔
Philip Gillißen
Philip Gillißen

💻
Sebastian Hagn
Sebastian Hagn

🐛 📖
Han Wang
Han Wang

💻
Ben Jeffrey
Ben Jeffrey

🐛
Beliavsky
Beliavsky

📖
Mariana Menchero García
Mariana Menchero García

💻
Nikhil Gupta
Nikhil Gupta

🐛
JD
JD

🐛
josh attenberg
josh attenberg

💻
JeroenPeterBos
JeroenPeterBos

💻
Jeroen Van Der Donckt
Jeroen Van Der Donckt

💻
Roymprog
Roymprog

📖
Nelson Cárdenas Bolaño
Nelson Cárdenas Bolaño

📖
Kyle Schmaus
Kyle Schmaus

💻
Akmal Soliev
Akmal Soliev

💻
Nick To
Nick To

💻
Kevin Kho
Kevin Kho

💻
Yiben Huang
Yiben Huang

📖
Andrew Gross
Andrew Gross

📖
taniishkaaa
taniishkaaa

📖
Manuel Calzolari
Manuel Calzolari

💻
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! # Core Methods Source: https://nixtlaverse.nixtla.io/statsforecast/src/core/core.html Methods for Fit, Predict, Forecast (fast), Cross Validation and plotting The core methods of `StatsForecast` provide a comprehensive interface for fitting, predicting, forecasting, and evaluating statistical forecasting models on large sets of time series. ## Overview The main methods include: * `StatsForecast.fit` - Fit statistical models * `StatsForecast.predict` - Predict using fitted models * `StatsForecast.forecast` - Memory-efficient predictions without storing models * `StatsForecast.cross_validation` - Temporal cross-validation * `StatsForecast.plot` - Visualization of forecasts and historical data ## StatsForecast Class ### `StatsForecast` Bases: [\_StatsForecast](#statsforecast.core._StatsForecast) The `StatsForecast` class allows you to efficiently fit multiple `StatsForecast` models for large sets of time series. It operates on a DataFrame `df` with at least three columns: ids, times, and targets. The class has a memory-efficient `StatsForecast.forecast` method that avoids storing partial model outputs, while the `StatsForecast.fit` and `StatsForecast.predict` methods with the Scikit-learn interface store the fitted models. The `StatsForecast` class offers parallelization utilities with Dask, Spark, and Ray back-ends. See distributed computing example [here](https://github.com/Nixtla/statsforecast/tree/main/experiments/ray). #### `StatsForecast.fit` ```python theme={null} fit(df, prediction_intervals=None, id_col='unique_id', time_col='ds', target_col='y') ``` Fit statistical models to time series data. Fits all models specified in the constructor to each time series in the input DataFrame. The fitted models are stored internally and can be used later with the `predict` method. This follows the scikit-learn fit/predict interface. **Parameters:** | Name | Type | Description | Default | | ---------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | [DataFrame](#utilsforecast.compat.DataFrame) | Input DataFrame containing time series data. Must have columns for series identifiers, timestamps, and target values. Can optionally include exogenous features. | *required* | | `prediction_intervals` | [ConformalIntervals](#statsforecast.utils.ConformalIntervals) | Configuration for calibrating prediction intervals using Conformal Prediction. If provided, the models will be prepared to generate prediction intervals. | None | | `id_col` | [str](#str) | Name of the column containing unique identifiers for each time series. | 'unique\_id' | | `time_col` | [str](#str) | Name of the column containing timestamps or time indices. Values can be timestamps (datetime) or integers. | 'ds' | | `target_col` | [str](#str) | Name of the column containing the target variable to forecast. | 'y' | **Returns:** | Name | Type | Description | | --------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `StatsForecast` | [StatsForecast](#statsforecast.core.StatsForecast) | Returns self with fitted models stored in the `fitted_` attribute. This allows for method chaining. | #### `StatsForecast.predict` ```python theme={null} predict(h, X_df=None, level=None) ``` Generate forecasts using previously fitted models. Uses the models fitted via the `fit` method to generate predictions for the specified forecast horizon. This follows the scikit-learn fit/predict interface. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon, the number of time steps ahead to predict. | *required* | | `X_df` | [DataFrame](#utilsforecast.compat.DataFrame) | DataFrame containing future exogenous variables. Required if any models use exogenous features. Must have the same structure as training data and include future values for all time series and forecast horizon. | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels between 0 and 100 for prediction intervals (e.g., \[80, 95] for 80% and 95% intervals). If provided with models configured for prediction intervals, the output will include lower and upper bounds. | None | **Returns:** | Type | Description | | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [DataFrame](#utilsforecast.compat.DataFrame) | DataFrame with forecasts for each model. Contains the series identifiers, future timestamps, and one column per model with point predictions. If `level` is specified, includes additional columns for prediction interval bounds (e.g., 'model-lo-95', 'model-hi-95'). | #### `StatsForecast.fit_predict` ```python theme={null} fit_predict(h, df, X_df=None, level=None, prediction_intervals=None, id_col='unique_id', time_col='ds', target_col='y') ``` Fit models and generate predictions in a single step. Combines the `fit` and `predict` methods in a single operation. The fitted models are stored internally in the `fitted_` attribute for later use, making this method suitable when you need both training and immediate predictions. **Parameters:** | Name | Type | Description | Default | | ---------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `h` | [int](#int) | Forecast horizon, the number of time steps ahead to predict. | *required* | | `df` | [DataFrame](#utilsforecast.compat.DataFrame) | Input DataFrame containing time series data. Must have columns for series identifiers, timestamps, and target values. Can optionally include exogenous features. | *required* | | `X_df` | [DataFrame](#utilsforecast.compat.DataFrame) | DataFrame containing future exogenous variables. Required if any models use exogenous features. Must include future values for all time series and forecast horizon. | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels between 0 and 100 for prediction intervals (e.g., \[80, 95]). Required if `prediction_intervals` is specified. | None | | `prediction_intervals` | [ConformalIntervals](#statsforecast.utils.ConformalIntervals) | Configuration for calibrating prediction intervals using Conformal Prediction. | None | | `id_col` | [str](#str) | Name of the column containing unique identifiers for each time series. | 'unique\_id' | | `time_col` | [str](#str) | Name of the column containing timestamps or time indices. Values can be timestamps (datetime) or integers. | 'ds' | | `target_col` | [str](#str) | Name of the column containing the target variable to forecast. | 'y' | **Returns:** | Type | Description | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [DataFrame](#utilsforecast.compat.DataFrame) | DataFrame with forecasts containing series identifiers, future timestamps, and predictions from each model. Includes prediction intervals if `level` is specified. | #### `StatsForecast.forecast` ```python theme={null} forecast(h, df, X_df=None, level=None, fitted=False, prediction_intervals=None, id_col='unique_id', time_col='ds', target_col='y') ``` Generate forecasts with memory-efficient model training. This is the primary forecasting method that trains models and generates predictions without storing fitted model objects. It is more memory-efficient than `fit_predict` when you don't need to inspect or reuse the fitted models. Models are trained and used for forecasting within each time series, then discarded. **Parameters:** | Name | Type | Description | Default | | ---------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `h` | [int](#int) | Forecast horizon, the number of time steps ahead to predict. | *required* | | `df` | [DataFrame](#utilsforecast.compat.DataFrame) | Input DataFrame containing time series data. Must have columns for series identifiers, timestamps, and target values. Can optionally include exogenous features for training. | *required* | | `X_df` | [DataFrame](#utilsforecast.compat.DataFrame) | DataFrame containing future exogenous variables. Required if any models use exogenous features. Must include future values for all time series and forecast horizon. | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels between 0 and 100 for prediction intervals (e.g., \[80, 95]). | None | | `fitted` | [bool](#bool) | If True, stores in-sample (fitted) predictions which can be retrieved using `forecast_fitted_values()`. | False | | `prediction_intervals` | [ConformalIntervals](#statsforecast.utils.ConformalIntervals) | Configuration for calibrating prediction intervals using Conformal Prediction. | None | | `id_col` | [str](#str) | Name of the column containing unique identifiers for each time series. | 'unique\_id' | | `time_col` | [str](#str) | Name of the column containing timestamps or time indices. Values can be timestamps (datetime) or integers. | 'ds' | | `target_col` | [str](#str) | Name of the column containing the target variable to forecast. | 'y' | **Returns:** | Type | Description | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [DataFrame](#utilsforecast.compat.DataFrame) | DataFrame with forecasts containing series identifiers, future timestamps, and predictions from each model. Includes prediction intervals if `level` is specified. | #### `StatsForecast.cross_validation` ```python theme={null} cross_validation(h, df, n_windows=1, step_size=1, test_size=None, input_size=None, level=None, fitted=False, refit=True, prediction_intervals=None, id_col='unique_id', time_col='ds', target_col='y') ``` Perform temporal cross-validation for model evaluation. Evaluates model performance across multiple time windows using a time series cross-validation approach. This method trains models on expanding or rolling windows and generates forecasts for each validation period, providing robust assessment of forecast accuracy and generalization. **Parameters:** | Name | Type | Description | Default | | ---------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `h` | [int](#int) | Forecast horizon for each validation window. | *required* | | `df` | [DataFrame](#utilsforecast.compat.DataFrame) | Input DataFrame containing time series data with columns for series identifiers, timestamps, and target values. | *required* | | `n_windows` | [int](#int) | Number of validation windows to create. Cannot be specified together with `test_size`. | 1 | | `step_size` | [int](#int) | Number of time steps between consecutive validation windows. Smaller values create overlapping windows. | 1 | | `test_size` | [int](#int) | Total size of the test period. If provided, `n_windows` is computed automatically. Overrides `n_windows` if specified. | None | | `input_size` | [int](#int) | Maximum number of training observations to use for each window. If None, uses expanding windows with all available history. If specified, uses rolling windows of fixed size. | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels between 0 and 100 for prediction intervals (e.g., \[80, 95]). | None | | `fitted` | [bool](#bool) | If True, stores in-sample predictions for each window, accessible via `cross_validation_fitted_values()`. | False | | `refit` | [bool](#bool) or [int](#int) | Controls model refitting frequency. If True, refits models for every window. If False, fits once and uses the forward method. If an integer n, refits every n windows. Models must implement the `forward` method when refit is not True. | True | | `prediction_intervals` | [ConformalIntervals](#statsforecast.utils.ConformalIntervals) | Configuration for calibrating prediction intervals using Conformal Prediction. Requires `level` to be specified. | None | | `id_col` | [str](#str) | Name of the column containing unique identifiers for each time series. | 'unique\_id' | | `time_col` | [str](#str) | Name of the column containing timestamps or time indices. | 'ds' | | `target_col` | [str](#str) | Name of the column containing the target variable. | 'y' | **Returns:** | Type | Description | | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [DataFrame](#utilsforecast.compat.DataFrame) | DataFrame with cross-validation results including series identifiers, cutoff dates (last training observation), forecast dates, actual values, and predictions from each model for all windows. | #### `StatsForecast.plot` ```python theme={null} plot(df, forecasts_df=None, unique_ids=None, plot_random=True, models=None, level=None, max_insample_length=None, plot_anomalies=False, engine='matplotlib', id_col='unique_id', time_col='ds', target_col='y', resampler_kwargs=None) ``` Visualize time series data with forecasts and prediction intervals. Creates plots showing historical data, forecasts, and optional prediction intervals for time series. Supports multiple plotting engines and interactive visualization. **Parameters:** | Name | Type | Description | Default | | --------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | [DataFrame](#utilsforecast.compat.DataFrame) | Input DataFrame containing historical time series data with columns for series identifiers, timestamps, and target values. | *required* | | `forecasts_df` | [DataFrame](#utilsforecast.compat.DataFrame) | DataFrame with forecast results from `forecast()` or `cross_validation()`. Should contain series identifiers, timestamps, and model predictions. | None | | `unique_ids` | [List](#typing.List)\[[str](#str)] or [ndarray](#numpy.ndarray) | Specific series identifiers to plot. If None and `plot_random` is True, series are selected randomly. | None | | `plot_random` | [bool](#bool) | Whether to randomly select series to plot when `unique_ids` is not specified. | True | | `models` | [List](#typing.List)\[[str](#str)] | Names of specific models to include in the plot. If None, plots all models present in `forecasts_df`. | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels to plot as shaded regions around forecasts (e.g., \[80, 95]). Only applicable if prediction intervals are present in `forecasts_df`. | None | | `max_insample_length` | [int](#int) | Maximum number of historical observations to display. Useful for focusing on recent history when series are long. | None | | `plot_anomalies` | [bool](#bool) | If True, highlights observations that fall outside prediction intervals as anomalies. | False | | `engine` | [str](#str) | Plotting library to use. Options are 'matplotlib' (static plots), 'plotly' (interactive plots), or 'plotly-resampler' (interactive with downsampling for large datasets). | 'matplotlib' | | `id_col` | [str](#str) | Name of the column containing series identifiers. | 'unique\_id' | | `time_col` | [str](#str) | Name of the column containing timestamps. | 'ds' | | `target_col` | [str](#str) | Name of the column containing the target variable. | 'y' | | `resampler_kwargs` | [Dict](#typing.Dict) | Additional keyword arguments passed to the plotly-resampler constructor when `engine='plotly-resampler'`. For further customization (e.g., 'show\_dash'), call this method, store the returned object, and add arguments to its `show_dash` method. | None | **Returns:** | Type | Description | | ------------------------------------------------------------------------------ | ----------- | | Plotting object from the selected engine (matplotlib Figure, plotly Figure, or | | | FigureResampler object), which can be further customized or displayed. | | #### `StatsForecast.save` ```python theme={null} save(path=None, max_size=None, trim=False) ``` Save the StatsForecast instance to disk using pickle. Serializes the StatsForecast object including all fitted models and configuration to a file for later use. The saved object can be loaded with the `load()` method to restore the exact state for making predictions. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `path` | [str](#str) or [Path](#pathlib.Path) | File path where the object will be saved. If None, creates a filename in the current directory using the format 'StatsForecast\_YYYY-MM-DD\_HH-MM-SS.pkl' with the current UTC timestamp. | None | | `max_size` | [str](#str) | Maximum allowed size for the serialized object. Should be specified as a number followed by a unit: 'B', 'KB', 'MB', or 'GB' (e.g., '100MB', '1.5GB'). If the object exceeds this size, an OSError is raised. | None | | `trim` | [bool](#bool) | If True, removes fitted values from `forecast()` and `cross_validation()` before saving to reduce file size. These values are not needed for generating new predictions. | False | #### `StatsForecast.load` ```python theme={null} load(path) ``` Load a previously saved StatsForecast instance from disk. Deserializes a StatsForecast object that was saved using the `save()` method, restoring all fitted models and configuration. The loaded object is ready to generate predictions immediately. **Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------- | | `path` | [str](#str) or [Path](#pathlib.Path) | File path to the saved StatsForecast pickle file. Must point to a file created by the `save()` method. | *required* | **Returns:** | Name | Type | Description | | --------------- | ---- | ---------------------------------------------------------------------------------------------------------------- | | `StatsForecast` | | The deserialized StatsForecast instance with all fitted models and configuration restored, ready for prediction. | ## Usage Examples ### Basic Forecasting ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoARIMA, Naive from statsforecast.utils import generate_series # Generate example data panel_df = generate_series(n_series=9, equal_ends=False, engine='pandas') # Instantiate StatsForecast class fcst = StatsForecast( models=[AutoARIMA(), Naive()], freq='D', n_jobs=1, verbose=True ) # Efficiently predict fcsts_df = fcst.forecast(df=panel_df, h=4, fitted=True) ``` ### Cross-Validation ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import Naive from statsforecast.utils import AirPassengersDF as panel_df # Instantiate StatsForecast class fcst = StatsForecast( models=[Naive()], freq='D', n_jobs=1, verbose=True ) # Perform cross-validation cv_df = fcst.cross_validation(df=panel_df, h=14, n_windows=2) ``` ### Prediction Intervals ```python theme={null} import pandas as pd import numpy as np from statsforecast import StatsForecast from statsforecast.models import SeasonalNaive, AutoARIMA from statsforecast.utils import AirPassengers as ap # Prepare data ap_df = pd.DataFrame({'ds': np.arange(ap.size), 'y': ap}) ap_df['unique_id'] = 0 # Forecast with prediction intervals sf = StatsForecast( models=[ SeasonalNaive(season_length=12), AutoARIMA(season_length=12) ], freq=1, n_jobs=1 ) ap_ci = sf.forecast(df=ap_df, h=12, level=(80, 95)) # Plot with confidence intervals sf.plot(ap_df, ap_ci, level=[80], engine="matplotlib") ``` ### Conformal Prediction Intervals ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoARIMA from statsforecast.utils import ConformalIntervals sf = StatsForecast( models=[ AutoARIMA(season_length=12), AutoARIMA( season_length=12, prediction_intervals=ConformalIntervals(n_windows=2, h=12), alias='ConformalAutoARIMA' ), ], freq=1, n_jobs=1 ) ap_ci = sf.forecast(df=ap_df, h=12, level=(80, 95)) ``` ## Advanced Features ### Integer Datestamps The `StatsForecast` class can work with integer datestamps instead of datetime objects: ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import HistoricAverage from statsforecast.utils import AirPassengers as ap import pandas as pd import numpy as np # Create dataframe with integer datestamps int_ds_df = pd.DataFrame({'ds': np.arange(1, len(ap) + 1), 'y': ap}) int_ds_df.insert(0, 'unique_id', 'AirPassengers') # Use freq=1 for integer datestamps fcst = StatsForecast(models=[HistoricAverage()], freq=1) forecast = fcst.forecast(df=int_ds_df, h=7) ``` ### External Regressors Every column after `y` is considered an external regressor and will be passed to models that support them: ```python theme={null} from statsforecast import StatsForecast from statsforecast.utils import generate_series import pandas as pd # Create data with external regressors series_xreg = generate_series(10_000, equal_ends=True) series_xreg['intercept'] = 1 series_xreg['dayofweek'] = series_xreg['ds'].dt.dayofweek series_xreg = pd.get_dummies(series_xreg, columns=['dayofweek'], drop_first=True) # Split train/validation dates = sorted(series_xreg['ds'].unique()) valid_start = dates[-14] train_mask = series_xreg['ds'] < valid_start series_train = series_xreg[train_mask] series_valid = series_xreg[~train_mask] X_valid = series_valid.drop(columns=['y']) # Forecast with external regressors fcst = StatsForecast(models=[your_model], freq='D') xreg_res = fcst.forecast(df=series_train, h=14, X_df=X_valid) ``` ## Distributed Computing The `StatsForecast` class offers parallelization utilities with Dask, Spark and Ray backends for distributed computing. See the [distributed computing examples](https://github.com/Nixtla/statsforecast/tree/main/experiments/ray) for more information. # Fugue Backend Source: https://nixtlaverse.nixtla.io/statsforecast/src/core/distributed.fugue.html The `FugueBackend` class enables distributed computation for StatsForecast using [Fugue](https://github.com/fugue-project/fugue), which provides a unified interface for Spark, Dask, and Ray backends without requiring code rewrites. ## Overview With FugueBackend, you can: * Distribute forecasting and cross-validation across clusters * Switch between Spark, Dask, and Ray without changing your code * Scale to large datasets with parallel processing * Maintain the same API as the standard StatsForecast interface ## API Reference ### `FugueBackend` ```python theme={null} FugueBackend(engine=None, conf=None, **transform_kwargs) ``` Bases: [ParallelBackend](#statsforecast.core.ParallelBackend) FugueBackend for Distributed Computation. [Source code](https://github.com/Nixtla/statsforecast/blob/main/statsforecast/distributed/fugue.py). This class uses [Fugue](https://github.com/fugue-project/fugue) backend capable of distributing computation on Spark, Dask and Ray without any rewrites. **Parameters:** | Name | Type | Description | Default | | -------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------- | | `engine` | [ExecutionEngine](#statsforecast.distributed.fugue.ExecutionEngine) | A selection between Spark, Dask, and Ray. | None | | `conf` | [Config](#statsforecast.distributed.fugue.Config) | Engine configuration. | None | | `**transform_kwargs` | [Any](#typing.Any) | Additional kwargs for Fugue's transform method. | |
Notes A short introduction to Fugue, with examples on how to scale pandas code to Spark, Dask or Ray is available [here](https://fugue-tutorials.readthedocs.io/tutorials/quick_look/ten_minutes.html).
#### `FugueBackend.forecast` ```python theme={null} forecast(*, df, freq, models, fallback_model, X_df, h, level, fitted, prediction_intervals, id_col, time_col, target_col) ``` Memory Efficient core.StatsForecast predictions with FugueBackend. This method uses Fugue's transform function, in combination with `core.StatsForecast`'s forecast to efficiently fit a list of StatsForecast models. **Parameters:** | Name | Type | Description | Default | | ---------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `df` | [DataFrame](#fugue.DataFrame) | Input DataFrame containing time series data. Must have columns for series identifiers, timestamps, and target values. Can optionally include exogenous features. | *required* | | `freq` | [str](#str) or [int](#int) | Frequency of the time series data. Must be a valid pandas or polars offset alias (e.g., 'D' for daily, 'M' for monthly, 'H' for hourly), or an integer representing the number of observations per cycle. | *required* | | `models` | [List](#typing.List)\[[Any](#typing.Any)] | List of instantiated StatsForecast model objects. Each model should implement the forecast interface. Models must have unique names, which can be set using the `alias` parameter. | *required* | | `fallback_model` | [Any](#typing.Any) | Model to use when a primary model fails during fitting or forecasting. Only works with the `forecast` and `cross_validation` methods. If None, exceptions from failing models will be raised. | *required* | | `X_df` | [DataFrame](#fugue.DataFrame) | DataFrame containing future exogenous variables. Required if any models use exogenous features. Must include future values for all time series and forecast horizon. | *required* | | `h` | [int](#int) | Forecast horizon, the number of time steps ahead to predict. | *required* | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels between 0 and 100 for prediction intervals (e.g., \[80, 95] for 80% and 95% intervals). | *required* | | `fitted` | [bool](#bool) | If True, stores in-sample (fitted) predictions which can be retrieved using `forecast_fitted_values()`. | *required* | | `prediction_intervals` | [ConformalIntervals](#statsforecast.utils.ConformalIntervals) | Configuration for calibrating prediction intervals using Conformal Prediction. | *required* | | `id_col` | [str](#str) | Name of the column containing unique identifiers for each time series. Defaults to 'unique\_id'. | *required* | | `time_col` | [str](#str) | Name of the column containing timestamps or time indices. Values can be timestamps (datetime) or integers. Defaults to 'ds'. | *required* | | `target_col` | [str](#str) | Name of the column containing the target variable to forecast. Defaults to 'y'. | *required* | **Returns:** | Type | Description | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | [Any](#typing.Any) | pandas.DataFrame: DataFrame with `models` columns for point predictions and probabilistic predictions for all fitted `models` |
References * For more information check the [Fugue's transform](https://fugue-tutorials.readthedocs.io/tutorials/beginner/transform.html) tutorial. * The [core.StatsForecast's forecast](./core.html#statsforecast-forecast)method documentation. * Or the list of available [StatsForecast's models](./models.html).
#### `FugueBackend.cross_validation` ```python theme={null} cross_validation(*, df, freq, models, fallback_model, h, n_windows, step_size, test_size, input_size, level, refit, fitted, prediction_intervals, id_col, time_col, target_col) ``` Temporal Cross-Validation with core.StatsForecast and FugueBackend. This method uses Fugue's transform function, in combination with `core.StatsForecast`'s cross-validation to efficiently fit a list of StatsForecast models through multiple training windows, in either chained or rolled manner. `StatsForecast.models`' speed along with Fugue's distributed computation allow to overcome this evaluation technique high computational costs. Temporal cross-validation provides better model's generalization measurements by increasing the test's length and diversity. **Parameters:** | Name | Type | Description | Default | | ---------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `df` | [DataFrame](#fugue.DataFrame) | Input DataFrame containing time series data with columns for series identifiers, timestamps, and target values. | *required* | | `freq` | [str](#str) or [int](#int) | Frequency of the time series data. Must be a valid pandas or polars offset alias (e.g., 'D' for daily, 'M' for monthly, 'H' for hourly), or an integer representing the number of observations per cycle. | *required* | | `models` | [List](#typing.List)\[[Any](#typing.Any)] | List of instantiated StatsForecast model objects. Each model should implement the forecast interface. Models must have unique names, which can be set using the `alias` parameter. | *required* | | `fallback_model` | [Any](#typing.Any) | Model to use when a primary model fails during fitting or forecasting. Only works with the `forecast` and `cross_validation` methods. If None, exceptions from failing models will be raised. | *required* | | `h` | [int](#int) | Forecast horizon for each validation window. | *required* | | `n_windows` | [int](#int) | Number of validation windows to create. Cannot be specified together with `test_size`. | *required* | | `step_size` | [int](#int) | Number of time steps between consecutive validation windows. Smaller values create overlapping windows. | *required* | | `test_size` | [int](#int) | Total size of the test period. If provided, `n_windows` is computed automatically. Overrides `n_windows` if specified. | *required* | | `input_size` | [int](#int) | Maximum number of training observations to use for each window. If None, uses expanding windows with all available history. If specified, uses rolling windows of fixed size. | *required* | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels between 0 and 100 for prediction intervals (e.g., \[80, 95]). | *required* | | `refit` | [bool](#bool) or [int](#int) | Controls model refitting frequency. If True, refits models for every window. If False, fits once and uses the forward method. If an integer n, refits every n windows. Models must implement the `forward` method when refit is not True. | *required* | | `fitted` | [bool](#bool) | If True, stores in-sample predictions for each window, accessible via `cross_validation_fitted_values()`. | *required* | | `prediction_intervals` | [ConformalIntervals](#statsforecast.utils.ConformalIntervals) | Configuration for calibrating prediction intervals using Conformal Prediction. Requires `level` to be specified. | *required* | | `id_col` | [str](#str) | Name of the column containing unique identifiers for each time series. Defaults to 'unique\_id'. | *required* | | `time_col` | [str](#str) | Name of the column containing timestamps or time indices. Defaults to 'ds'. | *required* | | `target_col` | [str](#str) | Name of the column containing the target variable. Defaults to 'y'. | *required* | **Returns:** | Type | Description | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | [Any](#typing.Any) | pandas.DataFrame: DataFrame, with `models` columns for point predictions and probabilistic predictions for all fitted `models`. |
References * The [core.StatsForecast's cross validation](./core.html#statsforecast-cross_validation) method documentation. * [Rob J. Hyndman and George Athanasopoulos (2018). "Forecasting principles and practice, Temporal Cross-Validation"](https://otexts.com/fpp3/tscv.html).
## Quick Start ### Basic Usage with Spark ```python theme={null} from statsforecast.core import StatsForecast from statsforecast.models import AutoARIMA, AutoETS from statsforecast.utils import generate_series from pyspark.sql import SparkSession # Generate example data n_series = 4 horizon = 7 series = generate_series(n_series) # Create Spark session spark = SparkSession.builder.getOrCreate() # Convert unique_id to string and create Spark DataFrame series['unique_id'] = series['unique_id'].astype(str) sdf = spark.createDataFrame(series) # Use StatsForecast with Spark DataFrame (automatically uses FugueBackend) sf = StatsForecast( models=[AutoETS(season_length=7)], freq='D', ) # Returns a Spark DataFrame results = sf.cross_validation( df=sdf, h=horizon, step_size=24, n_windows=2, level=[90] ) results.show() ``` ### Basic Forecasting ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoETS from statsforecast.utils import generate_series # Generate data series = generate_series(n_series=4) # Standard usage (pandas/polars) sf = StatsForecast( models=[AutoETS(season_length=7)], freq='D', ) # Forecast with pandas DataFrame sf.cross_validation( df=series, h=7, step_size=24, n_windows=2, level=[90] ).head() ``` ## Dask Distributed Example Here's a complete example using Dask for distributed predictions: ```python theme={null} import dask.dataframe as dd from dask.distributed import Client from fugue_dask import DaskExecutionEngine from statsforecast import StatsForecast from statsforecast.models import Naive from statsforecast.utils import generate_series # Generate synthetic panel data df = generate_series(10) df['unique_id'] = df['unique_id'].astype(str) df = dd.from_pandas(df, npartitions=10) # Instantiate Dask client and execution engine dask_client = Client() engine = DaskExecutionEngine(dask_client=dask_client) # Create StatsForecast instance sf = StatsForecast(models=[Naive()], freq='D') ``` ### Distributed Forecast The FugueBackend automatically handles distributed forecasting when you pass a Dask/Spark/Ray DataFrame: ```python theme={null} # Distributed predictions forecast_df = sf.forecast(df=df, h=12).compute() # With fitted values sf = StatsForecast(models=[Naive()], freq='D') forecast_df = sf.forecast(df=df, h=12, fitted=True).compute() fitted_df = sf.forecast_fitted_values().compute() ``` ### Distributed Cross-Validation Perform distributed temporal cross-validation across your cluster: ```python theme={null} # Distributed cross-validation cv_results = sf.cross_validation( df=df, h=12, n_windows=2 ).compute() ``` ## How It Works 1. **Automatic Detection**: When you pass a Spark, Dask, or Ray DataFrame to StatsForecast methods, the FugueBackend is automatically used. 2. **Data Partitioning**: Data is partitioned by `unique_id`, allowing parallel processing across different time series. 3. **Distributed Execution**: Each partition is processed independently using the standard StatsForecast logic. 4. **Result Aggregation**: Results are collected and returned in the same format as the input (Spark/Dask/Ray DataFrame). ## Supported Backends * **Apache Spark**: For large-scale distributed processing * **Dask**: For flexible distributed computing with Python * **Ray**: For modern distributed machine learning workloads ## Notes * Ensure your cluster has sufficient resources for the number of time series and models * The `unique_id` column should be string type for distributed operations * Use `.compute()` on Dask DataFrames to materialize results * Use `.show()` or `.collect()` on Spark DataFrames to view results ## See Also * [Core StatsForecast Methods](./core.html) * [Distributed Computing Examples](https://github.com/Nixtla/statsforecast/tree/main/experiments/ray) * [Fugue Documentation](https://fugue-tutorials.readthedocs.io/) # Models Source: https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html Models currently supported by StatsForecast StatsForecast offers a wide variety of statistical forecasting models grouped into the following categories: * **Auto Forecast**: Automatic forecasting tools that search for the best parameters and select the best possible model. Useful for large collections of univariate time series. Includes: AutoARIMA, AutoETS, AutoTheta, AutoCES, AutoMFLES, AutoTBATS. * **ARIMA Family**: AutoRegressive Integrated Moving Average models for capturing autocorrelations in time series data. * **Exponential Smoothing**: Uses weighted averages of past observations where weights decrease exponentially into the past. Suitable for data with clear trend and/or seasonality. * **Baseline Models**: Classical models for establishing baselines: HistoricAverage, Naive, RandomWalkWithDrift, SeasonalNaive, WindowAverage, SeasonalWindowAverage. * **Sparse or Intermittent**: Models suited for series with very few non-zero observations: ADIDA, CrostonClassic, CrostonOptimized, CrostonSBA, IMAPA, TSB. * **Multiple Seasonalities**: Models suited for signals with more than one clear seasonality. Useful for low-frequency data like electricity and logs: MSTL, MFLES, TBATS. * **Theta Models**: Fit two theta lines to a deseasonalized time series using different techniques: Theta, OptimizedTheta, DynamicTheta, DynamicOptimizedTheta. * **ARCH/GARCH Family**: Models for time series exhibiting non-constant volatility over time. Commonly used in finance. * **Machine Learning**: Wrapper for scikit-learn models to be used with StatsForecast. ## Automatic Forecasting ### AutoARIMA #### `AutoARIMA` ```python theme={null} AutoARIMA(d=None, D=None, max_p=5, max_q=5, max_P=2, max_Q=2, max_order=5, max_d=2, max_D=1, start_p=2, start_q=2, start_P=1, start_Q=1, stationary=False, seasonal=True, ic='aicc', stepwise=True, nmodels=94, trace=False, approximation=False, method=None, truncate=None, test='kpss', test_kwargs=None, seasonal_test='seas', seasonal_test_kwargs=None, allowdrift=True, allowmean=True, blambda=None, biasadj=False, season_length=1, distribution='normal', alias='AutoARIMA', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) AutoARIMA model. Automatically selects the best ARIMA (AutoRegressive Integrated Moving Average) model using an information criterion. Default is Akaike Information Criterion (AICc). **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------ | | `d` | [Optional](#typing.Optional)\[[int](#int)] | Order of first-differencing. | None | | `D` | [Optional](#typing.Optional)\[[int](#int)] | Order of seasonal-differencing. | None | | `max_p` | int, default=5 | Max autorregresives p. | 5 | | `max_q` | int, default=5 | Max moving averages q. | 5 | | `max_P` | int, default=2 | Max seasonal autorregresives P. | 2 | | `max_Q` | int, default=2 | Max seasonal moving averages Q. | 2 | | `max_order` | int, default=5 | Max p+q+P+Q value if not stepwise selection. | 5 | | `max_d` | int, default=2 | Max non-seasonal differences. | 2 | | `max_D` | int, default=1 | Max seasonal differences. | 1 | | `start_p` | int, default=2 | Starting value of p in stepwise procedure. | 2 | | `start_q` | int, default=2 | Starting value of q in stepwise procedure. | 2 | | `start_P` | int, default=1 | Starting value of P in stepwise procedure. | 1 | | `start_Q` | int, default=1 | Starting value of Q in stepwise procedure. | 1 | | `stationary` | bool, default=False | If True, restricts search to stationary models. | False | | `seasonal` | bool, default=True | If False, restricts search to non-seasonal models. | True | | `ic` | str, default="aicc" | Information criterion to be used in model selection. | 'aicc' | | `stepwise` | bool, default=True | If True, will do stepwise selection (faster). | True | | `nmodels` | int, default=94 | Number of models considered in stepwise search. | 94 | | `trace` | bool, default=False | If True, the searched ARIMA models is reported. | False | | `approximation` | Optional\[bool], default=False | If True, conditional sums-of-squares estimation, final MLE. | False | | `method` | [Optional](#typing.Optional)\[[str](#str)] | Fitting method between maximum likelihood or sums-of-squares. | None | | `truncate` | [Optional](#typing.Optional)\[[bool](#bool)] | Observations truncated series used in model selection. | None | | `test` | str, default="kpss" | Unit root test to use. See `ndiffs` for details. | 'kpss' | | `test_kwargs` | [Optional](#typing.Optional)\[[str](#str)] | Unit root test additional arguments. | None | | `seasonal_test` | str, default="seas" | Selection method for seasonal differences. | 'seas' | | `seasonal_test_kwargs` | [Optional](#typing.Optional)\[[dict](#dict)] | Seasonal unit root test arguments. | None | | `allowdrift` | bool, default=True | If True, drift models terms considered. | True | | `allowmean` | bool, default=True | If True, non-zero mean models considered. | True | | `blambda` | [Optional](#typing.Optional)\[[float](#float)] | Box-Cox transformation parameter. | None | | `biasadj` | bool, default=False | Use adjusted back-transformed mean Box-Cox. | False | | `season_length` | int, default=1 | Number of observations per unit of time. Ex: 24 Hourly data. | 1 | | `alias` | str, default="AutoARIMA" | Custom name of the model. | 'AutoARIMA' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None |
Notes This implementation is a mirror of Hyndman's [forecast::auto.arima](https://github.com/robjhyndman/forecast).
References [Rob J. Hyndman, Yeasmin Khandakar (2008). "Automatic Time Series Forecasting: The forecast package for R"](https://www.jstatsoft.org/article/view/v027i03).
##### `AutoARIMA.fit` ```python theme={null} fit(y, X=None) ``` Fit the AutoARIMA model. Fit an AutoARIMA to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ----------- | ---- | ----------------------- | | `AutoARIMA` | | AutoARIMA fitted model. | ##### `AutoARIMA.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted AutoArima. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoARIMA.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted AutoArima insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoARIMA.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient AutoARIMA predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenpus of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x) optional exogenous. | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | bool, default=False | Whether or not returns insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### AutoETS #### `AutoETS` ```python theme={null} AutoETS(season_length=1, model='ZZZ', damped=None, phi=None, alias='AutoETS', prediction_intervals=None, distribution='normal') ``` Bases: [\_TS](#statsforecast.models._TS) Automatic Error, Trend, Seasonal Model. Automatically selects the best ETS (Error, Trend, Seasonality) model using an information criterion. Default is Akaike Information Criterion (AICc), while particular models are estimated using maximum likelihood. The state-space equations can be determined based on their $M$ multiplicative, $A$ additive, $Z$ optimized or $N$ ommited components. The `model` string parameter defines the ETS equations: E in \[$M, A, Z$], T in \[$N, A, M, Z$], and S in \[$N, A, M, Z$]. For example when model='ANN' (additive error, no trend, and no seasonality), ETS will explore only a simple exponential smoothing. If the component is selected as 'Z', it operates as a placeholder to ask the AutoETS model to figure out the best parameter. **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | `season_length` | int, default=1 | Number of observations per unit of time. Ex: 24 Hourly data. | 1 | | `model` | str, default="ZZZ" | Controlling state-space-equations. | 'ZZZ' | | `damped` | [bool](#bool) | A parameter that 'dampens' the trend. | None | | `phi` | [float](#float) | Smoothing parameter for trend damping. Only used when `damped=True`. | None | | `alias` | str, default="AutoETS" | Custom name of the model. | 'AutoETS' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None |
Notes This implementation is a mirror of Hyndman's [forecast::ets](https://github.com/robjhyndman/forecast).
References * [Rob J. Hyndman, Yeasmin Khandakar (2008). "Automatic Time Series Forecasting: The forecast package for R"](https://www.jstatsoft.org/article/view/v027i03). * [Hyndman, Rob, et al (2008). "Forecasting with exponential smoothing: the state space approach"](https://robjhyndman.com/expsmooth/).
##### `AutoETS.fit` ```python theme={null} fit(y, X=None) ``` Fit the Exponential Smoothing model. Fit an Exponential Smoothing model to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | --------- | ---- | ----------------------------------- | | `AutoETS` | | Exponential Smoothing fitted model. | ##### `AutoETS.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted Exponential Smoothing. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenpus of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoETS.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted Exponential Smoothing insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoETS.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient Exponential Smoothing predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenpus of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | bool, default=False | Whether or not returns insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### AutoCES #### `AutoCES` ```python theme={null} AutoCES(season_length=1, model='Z', alias='CES', prediction_intervals=None, distribution='normal') ``` Bases: [\_TS](#statsforecast.models._TS) Complex Exponential Smoothing model. Automatically selects the best Complex Exponential Smoothing model using an information criterion. Default is Akaike Information Criterion (AICc), while particular models are estimated using maximum likelihood. The state-space equations can be determined based on their $S$ simple, $P$ parial, $Z$ optimized or $N$ ommited components. The `model` string parameter defines the kind of CES model: $N$ for simple CES (withous seasonality), $S$ for simple seasonality (lagged CES), $P$ for partial seasonality (without complex part), $F$ for full seasonality (lagged CES with real and complex seasonal parts). If the component is selected as 'Z', it operates as a placeholder to ask the AutoCES model to figure out the best parameter. **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `season_length` | int, default=1 | Number of observations per unit of time. Ex: 24 Hourly data. | 1 | | `model` | str, default="Z" | Controlling state-space-equations. | 'Z' | | `alias` | str, default="CES" | Custom name of the model. | 'CES' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None |
References * [Svetunkov, Ivan & Kourentzes, Nikolaos. (2015). "Complex Exponential Smoothing".](https://onlinelibrary.wiley.com/doi/full/10.1002/nav.22074).
##### `AutoCES.fit` ```python theme={null} fit(y, X=None) ``` Fit the Complex Exponential Smoothing model. Fit the Complex Exponential Smoothing model to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | --------- | ---- | ------------------------------------------- | | `AutoCES` | | Complex Exponential Smoothing fitted model. | ##### `AutoCES.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted Exponential Smoothing. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoCES.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted Exponential Smoothing insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoCES.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient Complex Exponential Smoothing predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenpus of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | bool, default=False | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### AutoTheta #### `AutoTheta` ```python theme={null} AutoTheta(season_length=1, decomposition_type='multiplicative', model=None, alias='AutoTheta', prediction_intervals=None, distribution='normal') ``` Bases: [\_TS](#statsforecast.models._TS) AutoTheta model. Automatically selects the best Theta (Standard Theta Model ('STM'), Optimized Theta Model ('OTM'), Dynamic Standard Theta Model ('DSTM'), Dynamic Optimized Theta Model ('DOTM')) model using mse. **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `season_length` | int, default=1 | Number of observations per unit of time. Ex: 24 Hourly data. | 1 | | `decomposition_type` | str, default="multiplicative" | Sesonal decomposition type, 'multiplicative' (default) or 'additive'. | 'multiplicative' | | `model` | [Optional](#typing.Optional)\[[str](#str)] | Controlling Theta Model. By default searchs the best model. | None | | `alias` | str, default="AutoTheta" | Custom name of the model. | 'AutoTheta' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None |
References * [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). "Models for optimising the theta method and their relationship to state space models". International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243)
##### `AutoTheta.fit` ```python theme={null} fit(y, X=None) ``` Fit the AutoTheta model. Fit an AutoTheta model to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ----------- | ---- | ----------------------- | | `AutoTheta` | | AutoTheta fitted model. | ##### `AutoTheta.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted AutoTheta. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoTheta.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted AutoTheta insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoTheta.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient AutoTheta predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | bool, default=False | Whether or not returns insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### AutoMFLES #### `AutoMFLES` ```python theme={null} AutoMFLES(test_size, season_length=None, n_windows=2, config=None, step_size=None, metric='smape', verbose=False, prediction_intervals=None, alias='AutoMFLES') ``` Bases: [\_TS](#statsforecast.models._TS) AutoMFLES **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------ | | `test_size` | [int](#int) | Forecast horizon used during cross validation. | *required* | | `season_length` | int or list of int, optional, default=None | Number of observations per unit of time. Ex: 24 Hourly data. | None | | `n_windows` | int, default=2 | Number of windows used for cross validation. | 2 | | `config` | dict, optional, default=None | Mapping from parameter name (from the init arguments of MFLES) to a list of values to try. If `None`, will use defaults. | None | | `step_size` | int, optional, default=None | Step size between each cross validation window. If `None` will be set to test\_size. | None | | `metric` | str, default='smape' | Metric used to select the best model. Possible options are: 'smape', 'mape', 'mse' and 'mae'. | 'smape' | | `verbose` | bool, default=False | Print debugging information. | False | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. This is required for generating future prediction intervals. | None | | `alias` | str, default='AutoMFLES' | Custom name of the model. | 'AutoMFLES' | ##### `AutoMFLES.fit` ```python theme={null} fit(y, X=None) ``` Fit the model **Parameters:** | Name | Type | Description | Default | | ---- | ----------------------------------------------- | --------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | array-like, optional, default=None | Exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ----------- | --------------------------------------------------------- | ------------------------ | | `AutoMFLES` | [AutoMFLES](#statsforecast.models.AutoMFLES) | Fitted AutoMFLES object. | ##### `AutoMFLES.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted AutoMFLES. **Parameters:** | Name | Type | Description | Default | | ------- | ----------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | array-like, optional, default=None | Exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[int](#int)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `dict` | [Dict](#typing.Dict)\[[str](#str), [Any](#typing.Any)] | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoMFLES.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted AutoMFLES insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | ----------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[int](#int)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `dict` | [Dict](#typing.Dict)\[[str](#str), [Any](#typing.Any)] | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoMFLES.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient AutoMFLES predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ----------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[int](#int)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | bool, default=False | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `dict` | [Dict](#typing.Dict)\[[str](#str), [Any](#typing.Any)] | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### AutoTBATS #### `AutoTBATS` ```python theme={null} AutoTBATS(season_length, use_boxcox=None, bc_lower_bound=0.0, bc_upper_bound=1.0, use_trend=None, use_damped_trend=None, use_arma_errors=True, alias='AutoTBATS') ``` Bases: [\_TS](#statsforecast.models._TS) AutoTBATS model. Automatically selects the best TBATS model from all feasible combinations of the parameters use\_boxcox, use\_trend, use\_damped\_trend, and use\_arma\_errors. Selection is made using the AIC. Default value for use\_arma\_errors is True since this enables the evaluation of models with and without ARMA errors.
References * [De Livera, A. M., Hyndman, R. J., & Snyder, R. D. (2011). Forecasting time series with complex seasonal patterns using exponential smoothing. Journal of the American statistical association, 106(496), 1513-1527.](https://citeseerx.ist.psu.edu/document?repid=rep1\&type=pdf\&doi=f3de25596ab60ef0e886366826bf58a02b35a44f) * [De Livera, Alysha M (2017). Modeling time series with complex seasonal patterns using exponential smoothing. Monash University. Thesis.](https://doi.org/10.4225/03/589299681de3d)
**Parameters:** | Name | Type | Description | Default | | ------------------ | ------------------------------- | ------------------------------------------------------------------------------------ | ------------------------ | | `seasonal_periods` | int or list of int | Number of observations per unit of time. Ex: 24 Hourly data. | *required* | | `use_boxcox` | bool, default=None | Whether or not to use a Box-Cox transformation. By default tries both. | None | | `bc_lower_bound` | float, default=0.0 | Lower bound for the Box-Cox transformation. | 0.0 | | `bc_upper_bound` | float, default=1.0 | Upper bound for the Box-Cox transformation. | 1.0 | | `use_trend` | bool, default=None | Whether or not to use a trend component. By default tries both. | None | | `use_damped_trend` | bool, default=None | Whether or not to dampen the trend component. By default tries both. | None | | `use_arma_errors` | bool, default=True | Whether or not to use a ARMA errors. Default is True and this evaluates both models. | True | | `alias` | [str](#str) | Custom name of the model. | 'AutoTBATS' | ##### `AutoTBATS.fit` ```python theme={null} fit(y, X=None) ``` Fit TBATS model. Fit TBATS model to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | ------------------------------------------------ | --------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | numpy.array, optional, default=None | Ignored | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------ | | `self` | | TBATS model. | ##### `AutoTBATS.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted TBATS model. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoTBATS.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted TBATS model predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoTBATS.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient TBATS model. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | -------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not returns insample predictions. | False | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ## ARIMA Family ### ARIMA #### `ARIMA` ```python theme={null} ARIMA(order=(0, 0, 0), season_length=1, seasonal_order=(0, 0, 0), include_mean=True, include_drift=False, include_constant=None, blambda=None, biasadj=False, method='CSS-ML', fixed=None, distribution='normal', alias='ARIMA', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) ARIMA model. AutoRegressive Integrated Moving Average model.
References * [Rob J. Hyndman, Yeasmin Khandakar (2008). "Automatic Time Series Forecasting: The forecast package for R"](https://www.jstatsoft.org/article/view/v027i03).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | `order` | tuple, default=(0, 0, 0) | A specification of the non-seasonal part of the ARIMA model: the three components (p, d, q) are the AR order, the degree of differencing, and the MA order. | (0, 0, 0) | | `season_length` | int, default=1 | Number of observations per unit of time. Ex: 24 Hourly data. | 1 | | `seasonal_order` | tuple, default=(0, 0, 0) | A specification of the seasonal part of the ARIMA model. (P, D, Q) for the AR order, the degree of differencing, the MA order. | (0, 0, 0) | | `include_mean` | bool, default=True | Should the ARIMA model include a mean term? The default is True for undifferenced series, False for differenced ones (where a mean would not affect the fit nor predictions). | True | | `include_drift` | bool, default=False | Should the ARIMA model include a linear drift term? (i.e., a linear regression with ARIMA errors is fitted.) | False | | `include_constant` | bool, optional, default=None | If True, then includ\_mean is set to be True for undifferenced series and include\_drift is set to be True for differenced series. Note that if there is more than one difference taken, no constant is included regardless of the value of this argument. This is deliberate as otherwise quadratic and higher order polynomial trends would be induced. | None | | `blambda` | float, optional, default=None | Box-Cox transformation parameter. | None | | `biasadj` | bool, default=False | Use adjusted back-transformed mean Box-Cox. | False | | `method` | str, default='CSS-ML' | Fitting method: maximum likelihood or minimize conditional sum-of-squares. The default (unless there are missing values) is to use conditional-sum-of-squares to find starting values, then maximum likelihood. | 'CSS-ML' | | `fixed` | dict, optional, default=None | Dictionary containing fixed coefficients for the arima model. Example: `{'ar1': 0.5, 'ma2': 0.75}`. For autoregressive terms use the `ar{i}` keys. For its seasonal version use `sar{i}`. For moving average terms use the `ma{i}` keys. For its seasonal version use `sma{i}`. For intercept and drift use the `intercept` and `drift` keys. For exogenous variables use the `ex_{i}` keys. | None | | `alias` | [str](#str) | Custom name of the model. | 'ARIMA' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ##### `ARIMA.fit` ```python theme={null} fit(y, X=None) ``` Fit the model to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------- | | `self` | | Fitted model. | ##### `ARIMA.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted model. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `ARIMA.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `ARIMA.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory efficient predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x) optional exogenous. | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not returns insample predictions. | False | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### AutoRegressive #### `AutoRegressive` ```python theme={null} AutoRegressive(lags, include_mean=True, include_drift=False, blambda=None, biasadj=False, method='CSS-ML', fixed=None, alias='AutoRegressive', prediction_intervals=None) ``` Bases: [ARIMA](#statsforecast.models.ARIMA) Simple Autoregressive model. **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `lags` | [int](#int) or [list](#list) | Number of lags to include in the model. If an int is passed then all lags up to `lags` are considered. If a list, only the elements of the list are considered as lags. | *required* | | `include_mean` | bool, default=True | Should the AutoRegressive model include a mean term? The default is True for undifferenced series, False for differenced ones (where a mean would not affect the fit nor predictions). | True | | `include_drift` | bool, default=False | Should the AutoRegressive model include a linear drift term? (i.e., a linear regression with AutoRegressive errors is fitted.) | False | | `blambda` | float, optional, default=None | Box-Cox transformation parameter. | None | | `biasadj` | bool, default=False | Use adjusted back-transformed mean Box-Cox. | False | | `method` | str, default='CSS-ML' | Fitting method: maximum likelihood or minimize conditional sum-of-squares. The default (unless there are missing values) is to use conditional-sum-of-squares to find starting values, then maximum likelihood. | 'CSS-ML' | | `fixed` | dict, optional, default=None | Dictionary containing fixed coefficients for the AutoRegressive model. Example: `{'ar1': 0.5, 'ar5': 0.75}`. For autoregressive terms use the `ar{i}` keys. | None | | `alias` | [str](#str) | Custom name of the model. | 'AutoRegressive' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ##### `AutoRegressive.fit` ```python theme={null} fit(y, X=None) ``` Fit the model to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------- | | `self` | | Fitted model. | ##### `AutoRegressive.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted model. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoRegressive.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `AutoRegressive.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory efficient predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x) optional exogenous. | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not returns insample predictions. | False | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ## Exponential Smoothing ### SimpleExponentialSmoothing #### `SimpleExponentialSmoothing` ```python theme={null} SimpleExponentialSmoothing(alpha, alias='SES', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) SimpleExponentialSmoothing model. Uses a weighted average of all past observations where the weights decrease exponentially into the past. Suitable for data with no clear trend or seasonality. Assuming there are $t$ observations, the one-step forecast is given by: $\\hat{y}_{t+1} = \\alpha y_t + (1-\\alpha) \\hat{y}_{t-1}$ The rate $0 \\leq \\alpha \\leq 1$ at which the weights decrease is called the smoothing parameter. When $\\alpha = 1$, SES is equal to the naive method.
References * [Charles C Holt (1957). “Forecasting seasonals and trends by exponentially weighted moving averages”](https://doi.org/10.1016/j.ijforecast).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `alpha` | [float](#float) | Smoothing parameter. | *required* | | `alias` | [str](#str) | Custom name of the model. | 'SES' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ##### `SimpleExponentialSmoothing.fit` ```python theme={null} fit(y, X=None) ``` Fit the SimpleExponentialSmoothing model. Fit an SimpleExponentialSmoothing to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ------ | ---- | ---------------------------------------- | | `self` | | SimpleExponentialSmoothing fitted model. | ##### `SimpleExponentialSmoothing.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted SimpleExponentialSmoothing. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `SimpleExponentialSmoothing.predict_in_sample` ```python theme={null} predict_in_sample() ``` Access fitted SimpleExponentialSmoothing insample predictions. **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions. | ##### `SimpleExponentialSmoothing.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient SimpleExponentialSmoothing predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### SimpleExponentialSmoothingOptimized #### `SimpleExponentialSmoothingOptimized` ```python theme={null} SimpleExponentialSmoothingOptimized(alias='SESOpt', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) SimpleExponentialSmoothing model. Uses a weighted average of all past observations where the weights decrease exponentially into the past. Suitable for data with no clear trend or seasonality. Assuming there are $t$ observations, the one-step forecast is given by: $\\hat{y}_{t+1} = \\alpha y_t + (1-\\alpha) \\hat{y}_{t-1}$ The smoothing parameter $\\alpha^\*$ is optimized by square error minimization.
References * [Charles C Holt (1957). “Forecasting seasonals and trends by exponentially weighted moving averages”](https://doi.org/10.1016/j.ijforecast).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | | `alias` | [str](#str) | Custom name of the model. | 'SESOpt' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. This is required for generating future prediction intervals. | None | ##### `SimpleExponentialSmoothingOptimized.fit` ```python theme={null} fit(y, X=None) ``` Fit the SimpleExponentialSmoothingOptimized model. Fit an SimpleExponentialSmoothingOptimized to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ------------------------------------- | ---- | ------------------------------------------------- | | `SimpleExponentialSmoothingOptimized` | | SimpleExponentialSmoothingOptimized fitted model. | ##### `SimpleExponentialSmoothingOptimized.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted SimpleExponentialSmoothingOptimized. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `SimpleExponentialSmoothingOptimized.predict_in_sample` ```python theme={null} predict_in_sample() ``` Access fitted SimpleExponentialSmoothingOptimized insample predictions. **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions. | ##### `SimpleExponentialSmoothingOptimized.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient SimpleExponentialSmoothingOptimized predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### SeasonalExponentialSmoothing #### `SeasonalExponentialSmoothing` ```python theme={null} SeasonalExponentialSmoothing(season_length, alpha, alias='SeasonalES', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) SeasonalExponentialSmoothing model. Uses a weighted average of all past observations where the weights decrease exponentially into the past. Suitable for data with no clear trend or seasonality. Assuming there are $t$ observations and season $s$, the one-step forecast is given by: $\\hat{y}_{t+1,s} = \\alpha y_t + (1-\\alpha) \\hat{y}_{t-1,s}$
Notes This method is an extremely simplified of Holt-Winter's method where the trend and level are set to zero. And a single seasonal smoothing parameter $\\alpha$ is shared across seasons.
References * [Charles. C. Holt (1957). "Forecasting seasonals and trends by exponentially weighted moving averages", ONR Research Memorandum, Carnegie Institute of Technology 52.](https://www.sciencedirect.com/science/article/abs/pii/S0169207003001134). * [Peter R. Winters (1960). "Forecasting sales by exponentially weighted moving averages". Management Science](https://pubsonline.informs.org/doi/abs/10.1287/mnsc.6.3.324).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `alpha` | [float](#float) | Smoothing parameter. | *required* | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 24 Hourly data. | *required* | | `alias` | [str](#str) | Custom name of the model. | 'SeasonalES' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. This is required for generating future prediction intervals. | None | ##### `SeasonalExponentialSmoothing.fit` ```python theme={null} fit(y, X=None) ``` Fit the SeasonalExponentialSmoothing model. Fit an SeasonalExponentialSmoothing to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ------------------------------ | ---- | ------------------------------------------ | | `SeasonalExponentialSmoothing` | | SeasonalExponentialSmoothing fitted model. | ##### `SeasonalExponentialSmoothing.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted SeasonalExponentialSmoothing. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `SeasonalExponentialSmoothing.predict_in_sample` ```python theme={null} predict_in_sample() ``` Access fitted SeasonalExponentialSmoothing insample predictions. **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions. | ##### `SeasonalExponentialSmoothing.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient SeasonalExponentialSmoothing predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not returns insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### SeasonalExponentialSmoothingOptimized #### `SeasonalExponentialSmoothingOptimized` ```python theme={null} SeasonalExponentialSmoothingOptimized(season_length, alias='SeasESOpt', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) SeasonalExponentialSmoothingOptimized model. Uses a weighted average of all past observations where the weights decrease exponentially into the past. Suitable for data with no clear trend or seasonality. Assuming there are $t$ observations and season $s$, the one-step forecast is given by: $\\hat{y}_{t+1,s} = \\alpha y_t + (1-\\alpha) \\hat{y}_{t-1,s}$ The smoothing parameter $\\alpha^\*$ is optimized by square error minimization. **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------ | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 24 Hourly data. | *required* | | `alias` | [str](#str) | Custom name of the model. | 'SeasESOpt' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. This is required for generating future prediction intervals. | None |
References * [Charles. C. Holt (1957). "Forecasting seasonals and trends by exponentially weighted moving averages", ONR Research Memorandum, Carnegie Institute of Technology 52.](https://www.sciencedirect.com/science/article/abs/pii/S0169207003001134). * [Peter R. Winters (1960). "Forecasting sales by exponentially weighted moving averages". Management Science](https://pubsonline.informs.org/doi/abs/10.1287/mnsc.6.3.324).
Notes * This method is an extremely simplified of Holt-Winter's method where the trend and level are set to zero. * And a single seasonal smoothing parameter $\\alpha$ is shared across seasons.
##### `SeasonalExponentialSmoothingOptimized.fit` ```python theme={null} fit(y, X=None) ``` Fit the SeasonalExponentialSmoothingOptimized model. Fit an SeasonalExponentialSmoothingOptimized to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | --------------------------------------- | ---- | --------------------------------------------------- | | `SeasonalExponentialSmoothingOptimized` | | SeasonalExponentialSmoothingOptimized fitted model. | ##### `SeasonalExponentialSmoothingOptimized.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted SeasonalExponentialSmoothingOptimized. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `SeasonalExponentialSmoothingOptimized.predict_in_sample` ```python theme={null} predict_in_sample() ``` Access fitted SeasonalExponentialSmoothingOptimized insample predictions. **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions. | ##### `SeasonalExponentialSmoothingOptimized.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient SeasonalExponentialSmoothingOptimized predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### Holt #### `Holt` ```python theme={null} Holt(season_length=1, error_type='A', alias='Holt', prediction_intervals=None) ``` Bases: [AutoETS](#statsforecast.models.AutoETS) Holt's method. Also known as double exponential smoothing, Holt's method is an extension of exponential smoothing for series with a trend. This implementation returns the corresponding `ETS` model with additive (A) or multiplicative (M) errors (so either 'AAN' or 'MAN').
References * [Rob J. Hyndman and George Athanasopoulos (2018). "Forecasting principles and practice, Methods with trend"](https://otexts.com/fpp3/holt.html).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------- | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 12 Monthly data. | 1 | | `error_type` | [str](#str) | The type of error of the ETS model. Can be additive (A) or multiplicative (M). | 'A' | | `alias` | [str](#str) | Custom name of the model. | 'Holt' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ### HoltWinters #### `HoltWinters` ```python theme={null} HoltWinters(season_length=1, error_type='A', alias='HoltWinters', prediction_intervals=None) ``` Bases: [AutoETS](#statsforecast.models.AutoETS) Holt-Winters' method. Also known as triple exponential smoothing, Holt-Winters' method is an extension of exponential smoothing for series that contain both trend and seasonality. This implementation returns the corresponding `ETS` model with additive (A) or multiplicative (M) errors (so either 'AAA' or 'MAM').
References * [Rob J. Hyndman and George Athanasopoulos (2018). "Forecasting principles and practice, Methods with seasonality"](https://otexts.com/fpp3/holt-winters.html).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------- | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 12 Monthly data. | 1 | | `error_type` | [str](#str) | The type of error of the ETS model. Can be additive (A) or multiplicative (M). | 'A' | | `alias` | [str](#str) | Custom name of the model. | 'HoltWinters' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ## Baseline Models ### HistoricAverage #### `HistoricAverage` ```python theme={null} HistoricAverage(alias='HistoricAverage', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) HistoricAverage model. Also known as mean method. Uses a simple average of all past observations. Assuming there are $t$ observations, the one-step forecast is given by: ```math theme={null} \hat{y}_{t+1} = \frac{1}{t} \sum_{j=1}^t y_j ```
References * [Rob J. Hyndman and George Athanasopoulos (2018). "Forecasting principles and practice, Simple Methods"](https://otexts.com/fpp3/simple-methods.html).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | | `alias` | [str](#str) | Custom name of the model. | 'HistoricAverage' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ##### `HistoricAverage.fit` ```python theme={null} fit(y, X=None) ``` Fit the HistoricAverage model. Fit an HistoricAverage to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ------ | ---- | ----------------------------- | | `self` | | HistoricAverage fitted model. | r ##### `HistoricAverage.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted HistoricAverage. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `HistoricAverage.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted HistoricAverage insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions. | ##### `HistoricAverage.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient HistoricAverage predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ------------------ | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional insample exogenous of shape (t, n\_x). Defaults to None. | None | | `X_future` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. Defaults to False. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### Naive #### `Naive` ```python theme={null} Naive(alias='Naive', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) Naive model. All forecasts have the value of the last observation: $\\hat{y}\_{t+1} = y_t$ for all $t$
References [Rob J. Hyndman and George Athanasopoulos (2018). "forecasting principles and practice, Simple Methods"](https://otexts.com/fpp3/simple-methods.html).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `alias` | [str](#str) | Custom name of the model. Defaults to "Naive". | 'Naive' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. Defaults to None. | None | ##### `Naive.fit` ```python theme={null} fit(y, X=None) ``` Fit the Naive model. Fit an Naive to a time series (numpy.array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------- | | `self` | | Naive fitted model. | ##### `Naive.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted Naive. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `Naive.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted Naive insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions. | ##### `Naive.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient Naive predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n,). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### RandomWalkWithDrift #### `RandomWalkWithDrift` ```python theme={null} RandomWalkWithDrift(alias='RWD', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) RandomWalkWithDrift model. A variation of the naive method allows the forecasts to change over time. The amout of change, called drift, is the average change seen in the historical data. ```math theme={null} \hat{y}_{t+1} = y_t+\frac{1}{t-1}\sum_{j=1}^t (y_j-y_{j-1}) = y_t+ \frac{y_t-y_1}{t-1} ``` From the previous equation, we can see that this is equivalent to extrapolating a line between the first and the last observation.
References * [Rob J. Hyndman and George Athanasopoulos (2018). "forecasting principles and practice, Simple Methods"](https://otexts.com/fpp3/simple-methods.html).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `alias` | [str](#str) | Custom name of the model. | 'RWD' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ##### `RandomWalkWithDrift.fit` ```python theme={null} fit(y, X=None) ``` Fit the RandomWalkWithDrift model. Fit an RandomWalkWithDrift to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | ---------------------------------- | --------------------------------- | ---------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------- | | `self` | | RandomWalkWithDrift fitted model. | r ##### `RandomWalkWithDrift.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted RandomWalkWithDrift. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `RandomWalkWithDrift.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted RandomWalkWithDrift insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `RandomWalkWithDrift.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient RandomWalkWithDrift predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n,). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### SeasonalNaive #### `SeasonalNaive` ```python theme={null} SeasonalNaive(season_length, alias='SeasonalNaive', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) Seasonal naive model. A method similar to the naive, but uses the last known observation of the same period (e.g. the same month of the previous year) in order to capture seasonal variations.
References * [Rob J. Hyndman and George Athanasopoulos (2018). "forecasting principles and practice, Simple Methods"](https://otexts.com/fpp3/simple-methods.html#seasonal-na%C3%AFve-method).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 24 Hourly data. | *required* | | `alias` | [str](#str) | Custom name of the model. | 'SeasonalNaive' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ##### `SeasonalNaive.fit` ```python theme={null} fit(y, X=None) ``` Fit the SeasonalNaive model. Fit an SeasonalNaive to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------- | | `self` | | SeasonalNaive fitted model. | r ##### `SeasonalNaive.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted Naive. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `SeasonalNaive.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted SeasonalNaive insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | r ##### `SeasonalNaive.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient SeasonalNaive predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### WindowAverage #### `WindowAverage` ```python theme={null} WindowAverage(window_size, alias='WindowAverage', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) WindowAverage model. Uses the average of the last $k$ observations, with $k$ the length of the window. Wider windows will capture global trends, while narrow windows will reveal local trends. The length of the window selected should take into account the importance of past observations and how fast the series changes.
References * [Rob J. Hyndman and George Athanasopoulos (2018). "forecasting principles and practice, Simple Methods"](https://otexts.com/fpp3/simple-methods.html).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | `window_size` | [int](#int) | Size of truncated series on which average is estimated. | *required* | | `alias` | [str](#str) | Custom name of the model. | 'WindowAverage' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. This is required for generating future prediction intervals. | None | r ##### `WindowAverage.fit` ```python theme={null} fit(y, X=None) ``` Fit the WindowAverage model. Fit an WindowAverage to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------- | | `self` | | WindowAverage fitted model. | ##### `WindowAverage.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted WindowAverage. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#numpy.array) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `WindowAverage.predict_in_sample` ```python theme={null} predict_in_sample() ``` Access fitted WindowAverage insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ---------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | *required* | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `WindowAverage.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient WindowAverage predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------------------ | --------------------------------------------------- | ------------------ | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### SeasonalWindowAverage #### `SeasonalWindowAverage` ```python theme={null} SeasonalWindowAverage(season_length, window_size, alias='SeasWA', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) SeasonalWindowAverage model. An average of the last $k$ observations of the same period, with $k$ the length of the window.
References * [Rob J. Hyndman and George Athanasopoulos (2018). "forecasting principles and practice, Simple Methods"](https://otexts.com/fpp3/simple-methods.html).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 24 Hourly data. | *required* | | `window_size` | [int](#int) | Size of truncated series on which average is estimated. | *required* | | `alias` | [str](#str) | Custom name of the model. | 'SeasWA' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. This is required for generating future prediction intervals. | None | r ##### `SeasonalWindowAverage.fit` ```python theme={null} fit(y, X=None) ``` Fit the SeasonalWindowAverage model. Fit an SeasonalWindowAverage to a time series (numpy array) `y` and optionally exogenous variables (numpy array) `X`. **Parameters:** | Name | Type | Description | Default | | ---- | --------------------------------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (t, ). | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ----------------------- | ---- | ----------------------------------- | | `SeasonalWindowAverage` | | SeasonalWindowAverage fitted model. | ##### `SeasonalWindowAverage.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted SeasonalWindowAverage. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional insample exogenous of shape (t, n\_x). | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `SeasonalWindowAverage.predict_in_sample` ```python theme={null} predict_in_sample() ``` Access fitted SeasonalWindowAverage insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------- | ---------- | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. | *required* | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `SeasonalWindowAverage.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient SeasonalWindowAverage predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------- | ------------------ | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (n,). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional insample exogenous of shape (t, n\_x). Defaults to None. | None | | `X_future` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels for prediction intervals. Defaults to None. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. Defaults to False. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ## Sparse or Intermittent Models ### ADIDA #### `ADIDA` ```python theme={null} ADIDA(alias='ADIDA', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) ADIDA model. Aggregate-Dissagregate Intermittent Demand Approach: Uses temporal aggregation to reduce the number of zero observations. Once the data has been agregated, it uses the optimized SES to generate the forecasts at the new level. It then breaks down the forecast to the original level using equal weights. ADIDA specializes on sparse or intermittent series are series with very few non-zero observations. They are notoriously hard to forecast, and so, different methods have been developed especifically for them.
References * [Nikolopoulos, K., Syntetos, A. A., Boylan, J. E., Petropoulos, F., & Assimakopoulos, V. (2011). An aggregate–disaggregate intermittent demand approach (ADIDA) to forecasting: an empirical proposition and analysis. Journal of the Operational Research Society, 62(3), 544-554.](https://researchportal.bath.ac.uk/en/publications/an-aggregate-disaggregate-intermittent-demand-approach-adida-to-f).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `alias` | [str](#str) | Custom name of the model. Defaults to "ADIDA". | 'ADIDA' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. Defaults to None. | None | ##### `ADIDA.fit` ```python theme={null} fit(y, X=None) ``` Fit the ADIDA model. Fit an ADIDA to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | --------------------------------------------------------------------- | ----------------------------------------------- | ----------------- | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (t, ). | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous variables. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ---- | ------------------- | | `ADIDA` | | ADIDA fitted model. | ##### `ADIDA.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted ADIDA. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `ADIDA.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted ADIDA insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `ADIDA.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient ADIDA predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ------------------ | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (n,). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional insample exogenous of shape (t, n\_x). Defaults to None. | None | | `X_future` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. Defaults to False. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### CrostonClassic #### `CrostonClassic` ```python theme={null} CrostonClassic(alias='CrostonClassic', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) CrostonClassic model. A method to forecast time series that exhibit intermittent demand. It decomposes the original time series into a non-zero demand size $z_t$ and inter-demand intervals $p_t$. Then the forecast is given by: ```math theme={null} \hat{y}_t = \frac{\hat{z}_t}{\hat{p}_t} ``` where $\\hat{z}\_t$ and $\\hat{p}\_t$ are forecasted using SES. The smoothing parameter of both components is set equal to 0.1
References * [Croston, J. D. (1972). Forecasting and stock control for intermittent demands. Journal of the Operational Research Society, 23(3), 289-303.](https://link.springer.com/article/10.1057/jors.1972.50)
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `alias` | [str](#str) | Custom name of the model. Defaults to "CrostonClassic". | 'CrostonClassic' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. Defaults to None. | None | ##### `CrostonClassic.fit` ```python theme={null} fit(y, X=None) ``` Fit the CrostonClassic model. Fit an CrostonClassic to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | --------------------------------------------------------------------- | ----------------------------------------------- | ----------------- | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (t, ). | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous variables. Defaults to None. | None | **Returns:** | Name | Type | Description | | ---------------- | ---- | ---------------------------- | | `CrostonClassic` | | CrostonClassic fitted model. | ##### `CrostonClassic.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted CrostonClassic. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `CrostonClassic.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted CrostonClassic insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `CrostonClassic.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient CrostonClassic predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ------------------ | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional insample exogenous of shape (t, n\_x). Defaults to None. | None | | `X_future` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | | `fitted` | [bool](#bool) | Whether or not returns insample predictions. Defaults to False. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### CrostonOptimized #### `CrostonOptimized` ```python theme={null} CrostonOptimized(alias='CrostonOptimized', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) CrostonOptimized model. A method to forecast time series that exhibit intermittent demand. It decomposes the original time series into a non-zero demand size $z_t$ and inter-demand intervals $p_t$. Then the forecast is given by: ```math theme={null} \hat{y}_t = \frac{\hat{z}_t}{\hat{p}_t} ``` A variation of the classic Croston's method where the smooting paramater is optimally selected from the range $[0.1,0.3]$. Both the non-zero demand $z_t$ and the inter-demand intervals $p_t$ are smoothed separately, so their smoothing parameters can be different.
References * [Croston, J. D. (1972). Forecasting and stock control for intermittent demands. Journal of the Operational Research Society, 23(3), 289-303.](https://link.springer.com/article/10.1057/jors.1972.50).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | | `alias` | [str](#str) | Custom name of the model. Defaults to "CrostonOptimized". | 'CrostonOptimized' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. This is required for generating future prediction intervals. Defaults to None. | None | ##### `CrostonOptimized.fit` ```python theme={null} fit(y, X=None) ``` Fit the CrostonOptimized model. Fit an CrostonOptimized to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | --------------------------------------------------------------------- | ----------------------------------------------- | ----------------- | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (t, ). | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous variables. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------------------ | ---- | ------------------------------ | | `CrostonOptimized` | | CrostonOptimized fitted model. | ##### `CrostonOptimized.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted CrostonOptimized. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional insample exogenous of shape (t, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `CrostonOptimized.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted CrostonOptimized insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `CrostonOptimized.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient CrostonOptimized predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ------------------ | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional insample exogenous of shape (t, n\_x). Defaults to None. | None | | `X_future` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | | `fitted` | [bool](#bool) | Whether or not returns insample predictions. Defaults to False. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### CrostonSBA #### `CrostonSBA` ```python theme={null} CrostonSBA(alias='CrostonSBA', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) CrostonSBA model. A method to forecast time series that exhibit intermittent demand. It decomposes the original time series into a non-zero demand size $z_t$ and inter-demand intervals $p_t$. Then the forecast is given by: ```math theme={null} \hat{y}_t = \frac{\hat{z}_t}{\hat{p}_t} ``` A variation of the classic Croston's method that uses a debiasing factor, so that the forecast is given by: ```math theme={null} \hat{y}_t = 0.95 \frac{\hat{z}_t}{\hat{p}_t} ```
References * [Croston, J. D. (1972). Forecasting and stock control for intermittent demands. Journal of the Operational Research Society, 23(3), 289-303.](https://link.springer.com/article/10.1057/jors.1972.50).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `alias` | [str](#str) | Custom name of the model. Defaults to "CrostonSBA". | 'CrostonSBA' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. Defaults to None. | None | ##### `CrostonSBA.fit` ```python theme={null} fit(y, X=None) ``` Fit the CrostonSBA model. Fit an CrostonSBA to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | --------------------------------------------------------------------- | ----------------------------------------------- | ----------------- | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (t, ). | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous variables. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------------ | ---- | ------------------------ | | `CrostonSBA` | | CrostonSBA fitted model. | ##### `CrostonSBA.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted CrostonSBA. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `CrostonSBA.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted CrostonSBA insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------- | ----------------- | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `CrostonSBA.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient CrostonSBA predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ------------------ | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional insample exogenous of shape (t, n\_x). Defaults to None. | None | | `X_future` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. Defaults to False. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### IMAPA #### `IMAPA` ```python theme={null} IMAPA(alias='IMAPA', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) IMAPA model. Intermittent Multiple Aggregation Prediction Algorithm: Similar to ADIDA, but instead of using a single aggregation level, it considers multiple in order to capture different dynamics of the data. Uses the optimized SES to generate the forecasts at the new levels and then combines them using a simple average.
References * [Syntetos, A. A., & Boylan, J. E. (2021). Intermittent demand forecasting: Context, methods and applications. John Wiley & Sons.](https://www.ifors.org/intermittent-demand-forecasting-context-methods-and-applications/).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `alias` | [str](#str) | Custom name of the model. Defaults to "IMAPA". | 'IMAPA' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. Defaults to None. | None | ##### `IMAPA.fit` ```python theme={null} fit(y, X=None) ``` Fit the IMAPA model. Fit an IMAPA to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | --------------------------------------------------------------------- | ----------------------------------------------- | ----------------- | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (t, ). | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous variables. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------- | ---- | ------------------- | | `IMAPA` | | IMAPA fitted model. | ##### `IMAPA.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted IMAPA. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `IMAPA.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted IMAPA insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `IMAPA.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient IMAPA predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ------------------ | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional insample exogenous of shape (t, n\_x). Defaults to None. | None | | `X_future` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. Defaults to False. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### TSB #### `TSB` ```python theme={null} TSB(alpha_d, alpha_p, alias='TSB', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) TSB model. Teunter-Syntetos-Babai: A modification of Croston's method that replaces the inter-demand intervals with the demand probability $d_t$, which is defined as follows. ```math theme={null} d_t = \begin{cases} 1 & \text{if demand occurs at time t} \\ 0 & \text{otherwise.} \end{cases} ``` Hence, the forecast is given by ```math theme={null} \hat{y}_t= \hat{d}_t\hat{z_t} ``` Both $d_t$ and $z_t$ are forecasted using SES. The smooting paramaters of each may differ, like in the optimized Croston's method.
References * [Teunter, R. H., Syntetos, A. A., & Babai, M. Z. (2011). Intermittent demand: Linking forecasting to inventory obsolescence. European Journal of Operational Research, 214(3), 606-615.](https://www.sciencedirect.com/science/article/abs/pii/S0377221711004437)
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `alpha_d` | [float](#float) | Smoothing parameter for demand. | *required* | | `alpha_p` | [float](#float) | Smoothing parameter for probability. | *required* | | `alias` | [str](#str) | Custom name of the model. Defaults to "TSB". | 'TSB' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. This is required for generating future prediction intervals. Defaults to None. | None | ##### `TSB.fit` ```python theme={null} fit(y, X=None) ``` Fit the TSB model. Fit an TSB to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | --------------------------------------------------------------------- | ----------------------------------------------- | ----------------- | | `y` | [ndarray](#numpy.ndarray) | Clean time series of shape (t, ). | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous variables. Defaults to None. | None | **Returns:** | Name | Type | Description | | ----- | ---- | ----------------- | | `TSB` | | TSB fitted model. | ##### `TSB.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted TSB. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [Optional](#typing.Optional)\[[ndarray](#numpy.ndarray)] | Optional exogenous of shape (h, n\_x). Defaults to None. | None | | `level` | [Optional](#typing.Optional)\[[List](#typing.List)\[[int](#int)]] | Confidence levels (0-100) for prediction intervals. Defaults to None. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `TSB.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted TSB insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `TSB.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient TSB predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | -------------------------------------------- | ----------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `fitted` | [bool](#bool) | Whether or not returns insample predictions. | False | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ## Multiple Seasonalities ### MSTL #### `MSTL` ```python theme={null} MSTL(season_length, trend_forecaster=AutoETS(model='ZZN'), stl_kwargs=None, alias='MSTL', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) MSTL model. The MSTL (Multiple Seasonal-Trend decomposition using LOESS) decomposes the time series in multiple seasonalities using LOESS. Then forecasts the trend using a custom non-seaonal model and each seasonality using a SeasonalNaive model.
References * [Bandara, Kasun & Hyndman, Rob & Bergmeir, Christoph. (2021). "MSTL: A Seasonal-Trend Decomposition Algorithm for Time Series with Multiple Seasonal Patterns".](https://arxiv.org/abs/2107.13462).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `season_length` | [Union](#typing.Union)\[[int](#int), [List](#typing.List)\[[int](#int)]] | Number of observations per unit of time. For multiple seasonalities use a list. | *required* | | `trend_forecaster` | model, default=AutoETS(model='ZZN') | StatsForecast model used to forecast the trend component. | [AutoETS](#statsforecast.models.AutoETS)(model='ZZN') | | `stl_kwargs` | [dict](#dict) | Extra arguments to pass to [`statsmodels.tsa.seasonal.STL`](https://www.statsmodels.org/dev/generated/statsmodels.tsa.seasonal.STL.html#statsmodels.tsa.seasonal.STL). The `period` and `seasonal` arguments are reserved. | None | | `alias` | [str](#str) | Custom name of the model. | 'MSTL' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ##### `MSTL.fit` ```python theme={null} fit(y, X=None) ``` Fit the MSTL model. Fit MSTL to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------ | | `self` | | MSTL fitted model. | ##### `MSTL.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted MSTL. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `MSTL.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted MSTL insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `MSTL.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient MSTL predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### MFLES #### `MFLES` ```python theme={null} MFLES(season_length=None, fourier_order=None, max_rounds=50, ma=None, alpha=1.0, decay=-1.0, changepoints=True, n_changepoints=0.25, seasonal_lr=0.9, trend_lr=0.9, exogenous_lr=1.0, residuals_lr=1.0, cov_threshold=0.7, moving_medians=False, min_alpha=0.05, max_alpha=1.0, trend_penalty=True, multiplicative=None, smoother=False, robust=None, verbose=False, prediction_intervals=None, alias='MFLES') ``` Bases: [\_TS](#statsforecast.models._TS) MFLES model. A method to forecast time series based on Gradient Boosted Time Series Decomposition which treats traditional decomposition as the base estimator in the boosting process. Unlike normal gradient boosting, slight learning rates are applied at the component level (trend/seasonality/exogenous). The method derives its name from some of the underlying estimators that can enter into the boosting procedure, specifically: a simple Median, Fourier functions for seasonality, a simple/piecewise Linear trend, and Exponential Smoothing. **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `season_length` | int or list of int | Number of observations per unit of time. Ex: 24 Hourly data. Default None. | None | | `fourier_order` | [int](#int) | How many fourier sin/cos pairs to create, the larger the number the more complex of a seasonal pattern can be fitted. A lower number leads to smoother results. This is auto-set based on seasonal\_period. Default None. | None | | `max_rounds` | [int](#int) | The max number of boosting rounds. The boosting will auto-stop but depending on other parameters such as rs\_lr you may want more rounds. Generally more rounds means a smoother fit. Default 50. | 50 | | `ma` | [int](#int) | The moving average order to use, this is auto-set based on internal logic. Passing 4 would fit a 4 period moving average on the residual component. Default None. | None | | `alpha` | [float](#float) | The alpha which is used in fitting the underlying LASSO when using piecewise functions. Default 1.0. | 1.0 | | `decay` | [float](#float) | Effects the slopes of the piecewise-linear basis function. Default -1.0. | -1.0 | | `changepoints` | [boolean](#boolean) | Whether to fit for changepoints if all other logic allows for it. If False, MFLES will not ever fit a piecewise trend. Default True. | True | | `n_changepoints` | [int](#int) or [float](#float) | Number (if int) or proportion (if float) of changepoint knots to place. The default of 0.25 will place 0.25 \* (series length) number of knots. Default 0.25. | 0.25 | | `seasonal_lr` | [float](#float) | A shrinkage parameter (0 \< seasonal\_lr \<= 1) which penalizes the seasonal fit. A value of 0.9 will flatly multiply the seasonal fit by 0.9 each boosting round, this can be used to allow more signal to the exogenous component. Default 0.9. | 0.9 | | `trend_lr` | [float](#float) | A shrinkage parameter (0 \< trend\_lr \<= 1) which penalizes the linear trend fit A value of 0.9 will flatly multiply the linear fit by 0.9 each boosting round, this can be used to allow more signal to the seasonality or exogenous components. Default 0.9. | 0.9 | | `exogenous_lr` | [float](#float) | The shrinkage parameter (0 \< exogenous\_lr \<= 1) which controls how much of the exogenous signal is carried to the next round. Default 1.0. | 1.0 | | `residuals_lr` | [float](#float) | A shrinkage parameter (0 \< residuals\_lr \<= 1) which penalizes the residual smoothing. A value of 0.9 will flatly multiply the residual fit by 0.9 each boosting round, this can be used to allow more signal to the seasonality or linear components. Default 1.0. | 1.0 | | `cov_threshold` | [float](#float) | The deseasonalized cov is used to auto-set some logic, lowering the cov\_threshold will result in simpler and less complex residual smoothing. If you pass something like 1000 then there will be no safeguards applied. Default 0.7. | 0.7 | | `moving_medians` | [bool](#bool) | The default behavior is to fit an initial median to the time series. If True, then it will fit a median per seasonal period. Default False. | False | | `min_alpha` | [float](#float) | The minimum alpha in the SES ensemble. Default 0.05. | 0.05 | | `max_alpha` | [float](#float) | The maximum alpha used in the SES ensemble. Default 1.0. | 1.0 | | `trend_penalty` | [bool](#bool) | Whether to apply a simple penalty to the linear trend component, very useful for dealing with the potentially dangerous piecewise trend. Default True. | True | | `multiplicative` | [bool](#bool) | Auto-set based on internal logic. If True, it will simply take the log of the time series. Default None. | None | | `smoother` | [bool](#bool) | If True, then a simple exponential ensemble will be used rather than auto settings. Default False. | False | | `robust` | [bool](#bool) | If True then MFLES will fit using more reserved methods, i.e. not using piecewise trend or moving average residual smoother. Auto-set based on internal logic. Default None. | None | | `verbose` | [bool](#bool) | Print debugging information. Default False. | False | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. This is required for generating future prediction intervals. | None | | `alias` | [str](#str) | Custom name of the model. Default 'MFLES'. | 'MFLES' | ##### `MFLES.fit` ```python theme={null} fit(y, X=None) ``` Fit the model **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | ------------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Exogenous of shape (t, n\_x). Default None. | None | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------- | -------------------- | | `self` | [MFLES](#statsforecast.models.MFLES) | Fitted MFLES object. | ##### `MFLES.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted MFLES. **Parameters:** | Name | Type | Description | Default | | ------- | ----------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Exogenous of shape (h, n\_x). Default None. | None | | `level` | [List](#typing.List)\[[int](#int)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `MFLES.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted SklearnModel insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | ----------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[int](#int)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `MFLES.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient MFLES predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ----------------------------------------------- | ------------------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[int](#int)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. Default False. | False | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### TBATS #### `TBATS` ```python theme={null} TBATS(season_length, use_boxcox=True, bc_lower_bound=0.0, bc_upper_bound=1.0, use_trend=True, use_damped_trend=False, use_arma_errors=False, alias='TBATS') ``` Bases: [AutoTBATS](#statsforecast.models.AutoTBATS) Trigonometric Box-Cox transform, ARMA errors, Trend and Seasonal components (TBATS) model. TBATS is an innovations state space model framework used for forecasting time series with multiple seasonalities. It uses a Box-Cox tranformation, ARMA errors, and a trigonometric representation of the seasonal patterns based on Fourier series. The name TBATS is an acronym for the key features of the model: Trigonometric, Box-Cox transform, ARMA errors, Trend, and Seasonal components.
References * [De Livera, A. M., Hyndman, R. J., & Snyder, R. D. (2011). Forecasting time series with complex seasonal patterns using exponential smoothing. Journal of the American statistical association, 106(496), 1513-1527.](https://citeseerx.ist.psu.edu/document?repid=rep1\&type=pdf\&doi=f3de25596ab60ef0e886366826bf58a02b35a44f) * [De Livera, Alysha M (2017). Modeling time series with complex seasonal patterns using exponential smoothing. Monash University. Thesis.](https://doi.org/10.4225/03/589299681de3d)
**Parameters:** | Name | Type | Description | Default | | ------------------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------------------- | | `season_length` | [Union](#typing.Union)\[[int](#int), [List](#typing.List)\[[int](#int)]] | Number of observations per unit of time. Ex: 24 Hourly data. | *required* | | `use_boxcox` | [Optional](#typing.Optional)\[[bool](#bool)] | Whether or not to use a Box-Cox transformation. Default True. | True | | `bc_lower_bound` | [float](#float) | Lower bound for the Box-Cox transformation. Default 0.0. | 0.0 | | `bc_upper_bound` | [float](#float) | Upper bound for the Box-Cox transformation. Default 1.0. | 1.0 | | `use_trend` | [Optional](#typing.Optional)\[[bool](#bool)] | Whether or not to use a trend component. Default True. | True | | `use_damped_trend` | [Optional](#typing.Optional)\[[bool](#bool)] | Whether or not to dampen the trend component. Default False. | False | | `use_arma_errors` | [bool](#bool) | Whether or not to use a ARMA errors. Default False. | False | | `alias` | [str](#str) | Custom name of the model. Default 'TBATS'. | 'TBATS' | ## Theta Family ### Theta #### `Theta` ```python theme={null} Theta(season_length=1, decomposition_type='multiplicative', alias='Theta', prediction_intervals=None) ``` Bases: [AutoTheta](#statsforecast.models.AutoTheta) Standard Theta Method.
References * [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). "Models for optimising the theta method and their relationship to state space models". International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243)
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 24 Hourly data. Default 1. | 1 | | `decomposition_type` | [str](#str) | Sesonal decomposition type, 'multiplicative' (default) or 'additive'. Default 'multiplicative'. | 'multiplicative' | | `alias` | [str](#str) | Custom name of the model. Default 'Theta'. | 'Theta' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. Default None. | None | ### OptimizedTheta #### `OptimizedTheta` ```python theme={null} OptimizedTheta(season_length=1, decomposition_type='multiplicative', alias='OptimizedTheta', prediction_intervals=None) ``` Bases: [AutoTheta](#statsforecast.models.AutoTheta) Optimized Theta Method.
References * [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). "Models for optimising the theta method and their relationship to state space models". International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243)
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 24 Hourly data. Default 1. | 1 | | `decomposition_type` | [str](#str) | Sesonal decomposition type, 'multiplicative' (default) or 'additive'. Default 'multiplicative'. | 'multiplicative' | | `alias` | [str](#str) | Custom name of the model. Default 'OptimizedTheta'. | 'OptimizedTheta' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. Default None. | None | ### DynamicTheta #### `DynamicTheta` ```python theme={null} DynamicTheta(season_length=1, decomposition_type='multiplicative', alias='DynamicTheta', prediction_intervals=None) ``` Bases: [AutoTheta](#statsforecast.models.AutoTheta) Dynamic Standard Theta Method.
References * [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). "Models for optimising the theta method and their relationship to state space models". International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243)
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 24 Hourly data. | 1 | | `decomposition_type` | [str](#str) | Sesonal decomposition type, 'multiplicative' (default) or 'additive'. | 'multiplicative' | | `alias` | [str](#str) | Custom name of the model. | 'DynamicTheta' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ### DynamicOptimizedTheta #### `DynamicOptimizedTheta` ```python theme={null} DynamicOptimizedTheta(season_length=1, decomposition_type='multiplicative', alias='DynamicOptimizedTheta', prediction_intervals=None) ``` Bases: [AutoTheta](#statsforecast.models.AutoTheta) Dynamic Optimized Theta Method.
References * [Jose A. Fiorucci, Tiago R. Pellegrini, Francisco Louzada, Fotios Petropoulos, Anne B. Koehler (2016). "Models for optimising the theta method and their relationship to state space models". International Journal of Forecasting](https://www.sciencedirect.com/science/article/pii/S0169207016300243)
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 24 Hourly data. | 1 | | `decomposition_type` | [str](#str) | Sesonal decomposition type, 'multiplicative' (default) or 'additive'. | 'multiplicative' | | `alias` | [str](#str) | Custom name of the model. | 'DynamicOptimizedTheta' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ## ARCH/GARCH Family ### GARCH #### `GARCH` ```python theme={null} GARCH(p=1, q=1, alias='GARCH', prediction_intervals=None) ``` Bases: [\_TS](#statsforecast.models._TS) Generalized Autoregressive Conditional Heteroskedasticity (GARCH) model. A method for modeling time series that exhibit non-constant volatility over time. The GARCH model assumes that at time $t$, $y_t$ is given by: ```math theme={null} y_t = v_t \sigma_t ``` with ```math theme={null} \sigma_t^2 = w + \sum_{i=1}^p a_i y_{t-i}^2 + \sum_{j=1}^q b_j \sigma_{t-j}^2. ``` Here $v_t$ is a sequence of iid random variables with zero mean and unit variance. The coefficients $w$, $a_i$, $i=1,...,p$, and $b_j$, $j=1,...,q$ must satisfy the following conditions: 1. $w > 0$ and $a_i, b_j \\geq 0$ for all $i$ and $j$. 2. $\\sum\_{k=1}^{max(p,q)} a_k + b_k < 1$. Here it is assumed that $a_i=0$ for $i>p$ and $b_j=0$ for $j>q$. The ARCH model is a particular case of the GARCH model when $q=0$.
References * [Engle, R. F. (1982). Autoregressive conditional heteroscedasticity with estimates of the variance of United Kingdom inflation. Econometrica: Journal of the econometric society, 987-1007.](http://www.econ.uiuc.edu/~econ508/Papers/engle82.pdf) * [Bollerslev, T. (1986). Generalized autoregressive conditional heteroskedasticity. Journal of econometrics, 31(3), 307-327.](https://citeseerx.ist.psu.edu/document?repid=rep1\&type=pdf\&doi=7da8bfa5295375c1141d797e80065a599153c19d) * [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis)
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `p` | [int](#int) | Number of lagged versions of the series. | 1 | | `q` | [int](#int) | Number of lagged versions of the volatility. | 1 | | `alias` | [str](#str) | Custom name of the model. | 'GARCH' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ##### `GARCH.fit` ```python theme={null} fit(y, X=None) ``` Fit GARCH model. Fit GARCH model to a time series (numpy array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | ---------------------------------- | --------------------------------- | ---------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | **Returns:** | Name | Type | Description | | ------ | ---- | ------------ | | `self` | | GARCH model. | ##### `GARCH.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted GARCH model. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `GARCH.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted GARCH model predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `GARCH.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient GARCH model. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | -------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not returns insample predictions. | False | **Returns:** | Name | Type | Description | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `forecasts` | [dict](#dict) | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### ARCH #### `ARCH` ```python theme={null} ARCH(p=1, alias='ARCH', prediction_intervals=None) ``` Bases: [GARCH](#statsforecast.models.GARCH) Autoregressive Conditional Heteroskedasticity (ARCH) model. A particular case of the GARCH(p,q) model where $q=0$. It assumes that at time $t$, $y_t$ is given by: ```math theme={null} y_t = \epsilon_t \sigma_t ``` with ```math theme={null} \sigma_t^2 = w0 + \sum_{i=1}^p a_i y_{t-i}^2 ``` Here $\\epsilon_t$ is a sequence of iid random variables with zero mean and unit variance. The coefficients $w$ and $a_i$, $i=1,...,p$ must be nonnegative and $\\sum\_{k=1}^p a_k < 1$.
References * [Engle, R. F. (1982). Autoregressive conditional heteroscedasticity with estimates of the variance of United Kingdom inflation. Econometrica: Journal of the econometric society, 987-1007.](http://www.econ.uiuc.edu/~econ508/Papers/engle82.pdf) * [James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.](https://press.princeton.edu/books/hardcover/9780691042893/time-series-analysis)
**Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------- | | `p` | [int](#int) | Number of lagged versions of the series. | 1 | | `alias` | [str](#str) | Custom name of the model. | 'ARCH' | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. By default, the model will compute the native prediction intervals. | None | ## Machine Learning ### SklearnModel #### `SklearnModel` ```python theme={null} SklearnModel(model, prediction_intervals=None, alias=None) ``` Bases: [\_TS](#statsforecast.models._TS) scikit-learn model wrapper **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------- | | `model` | [BaseEstimator](#sklearn.base.BaseEstimator) | scikit-learn estimator | *required* | | `prediction_intervals` | [Optional](#typing.Optional)\[[ConformalIntervals](#statsforecast.utils.ConformalIntervals)] | Information to compute conformal prediction intervals. This is required for generating future prediction intervals. | None | | `alias` | [str](#str) | Custom name of the model. If `None` will use the model's class. | None | ##### `SklearnModel.fit` ```python theme={null} fit(y, X) ``` Fit the model. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | --------------------------------- | ---------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Exogenous of shape (t, n\_x). | *required* | **Returns:** | Name | Type | Description | | -------------- | --------------------------------------------------------------- | --------------------------- | | `SklearnModel` | [SklearnModel](#statsforecast.models.SklearnModel) | Fitted SklearnModel object. | ##### `SklearnModel.predict` ```python theme={null} predict(h, X, level=None) ``` Predict with fitted SklearnModel. **Parameters:** | Name | Type | Description | Default | | ------- | ----------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Exogenous of shape (h, n\_x). | *required* | | `level` | [List](#typing.List)\[[int](#int)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `dict` | [Dict](#typing.Dict)\[[str](#str), [Any](#typing.Any)] | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `SklearnModel.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted SklearnModel insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | ----------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[int](#int)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `dict` | [Dict](#typing.Dict)\[[str](#str), [Any](#typing.Any)] | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `SklearnModel.forecast` ```python theme={null} forecast(y, h, X, X_future, level=None, fitted=False) ``` Memory Efficient SklearnModel predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | ----------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Insample exogenous of shape (t, n\_x). | *required* | | `X_future` | [array](#array) - [like](#like) | Exogenous of shape (h, n\_x). | *required* | | `level` | [List](#typing.List)\[[int](#int)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `dict` | [Dict](#typing.Dict)\[[str](#str), [Any](#typing.Any)] | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ## Fallback Models These models are used as fallbacks when other models fail during forecasting. ### ConstantModel #### `ConstantModel` ```python theme={null} ConstantModel(constant, alias='ConstantModel') ``` Bases: [\_TS](#statsforecast.models._TS) Constant Model. Returns Constant values. **Parameters:** | Name | Type | Description | Default | | ---------- | ---------------------------- | ----------------------------------- | ---------------------------- | | `constant` | [float](#float) | Custom value to return as forecast. | *required* | | `alias` | [str](#str) | Custom name of the model. | 'ConstantModel' | ##### `ConstantModel.fit` ```python theme={null} fit(y, X=None) ``` Fit the Constant model. Fit an Constant Model to a time series (numpy.array) `y`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------------------------------------- | -------------------------------------- | ----------------- | | `y` | [array](#numpy.array) | Clean time series of shape (t, ). | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (t, n\_x). | None | **Returns:** | Name | Type | Description | | --------------- | ---- | ---------------------- | | `ConstantModel` | | Constant fitted model. | ##### `ConstantModel.predict` ```python theme={null} predict(h, X=None, level=None) ``` Predict with fitted ConstantModel. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ##### `ConstantModel.predict_in_sample` ```python theme={null} predict_in_sample(level=None) ``` Access fitted Constant Model insample predictions. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | --------------------------------------------------- | ----------------- | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | **Returns:** | Name | Type | Description | | ------ | ---- | --------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `fitted` for point predictions and `level_*` for probabilistic predictions. | ##### `ConstantModel.forecast` ```python theme={null} forecast(y, h, X=None, X_future=None, level=None, fitted=False) ``` Memory Efficient Constant Model predictions. This method avoids memory burden due from object storage. It is analogous to `fit_predict` without storing information. It assumes you know the forecast horizon in advance. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------------------- | --------------------------------------------------- | ------------------ | | `y` | [array](#numpy.array) | Clean time series of shape (n,). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | | `X` | [array](#array) - [like](#like) | Optional insample exogenous of shape (t, n\_x). | None | | `X_future` | [array](#array) - [like](#like) | Optional exogenous of shape (h, n\_x). | None | | `level` | [List](#typing.List)\[[float](#float)] | Confidence levels (0-100) for prediction intervals. | None | | `fitted` | [bool](#bool) | Whether or not to return insample predictions. | False | **Returns:** | Name | Type | Description | | ------ | ---- | ------------------------------------------------------------------------------------------------- | | `dict` | | Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. | ### ZeroModel #### `ZeroModel` ```python theme={null} ZeroModel(alias='ZeroModel') ``` Bases: [ConstantModel](#statsforecast.models.ConstantModel) Returns Zero forecasts. Returns Zero values. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------ | ------------------------- | ------------------------ | | `alias` | [str](#str) | Custom name of the model. | 'ZeroModel' | ### NaNModel #### `NaNModel` ```python theme={null} NaNModel(alias='NaNModel') ``` Bases: [ConstantModel](#statsforecast.models.ConstantModel) NaN Model. Returns NaN values. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------ | ------------------------- | ----------------------- | | `alias` | [str](#str) | Custom name of the model. | 'NaNModel' | ## Usage Examples ### Basic Model Usage ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoARIMA, Naive from statsforecast.utils import generate_series # Generate example data df = generate_series(n_series=10) # Create StatsForecast instance with models sf = StatsForecast( models=[ AutoARIMA(season_length=7), Naive() ], freq='D' ) # Forecast forecasts = sf.forecast(df=df, h=7) ``` ### Using Multiple Models ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import ( AutoARIMA, AutoETS, SeasonalNaive, Theta, HistoricAverage ) # Combine multiple models for comparison models = [ AutoARIMA(season_length=12), AutoETS(season_length=12), SeasonalNaive(season_length=12), Theta(season_length=12), HistoricAverage() ] sf = StatsForecast(models=models, freq='M', n_jobs=-1) forecasts = sf.forecast(df=df, h=12, level=[80, 95]) ``` ### Model with Prediction Intervals ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoARIMA from statsforecast.utils import ConformalIntervals # Create model with conformal prediction intervals model = AutoARIMA( season_length=12, prediction_intervals=ConformalIntervals(n_windows=2, h=12), alias='ConformalAutoARIMA' ) sf = StatsForecast(models=[model], freq='M') forecasts = sf.forecast(df=df, h=12, level=[80, 95]) ``` ### Sparse/Intermittent Data ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import ( CrostonOptimized, ADIDA, IMAPA, TSB ) # Models specialized for sparse/intermittent data sparse_models = [ CrostonOptimized(), ADIDA(), IMAPA(), TSB(alpha_d=0.2, alpha_p=0.2) ] sf = StatsForecast(models=sparse_models, freq='D') forecasts = sf.forecast(df=sparse_df, h=30) ``` ### Multiple Seasonalities ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import MSTL, AutoTBATS # For data with multiple seasonal patterns models = [ MSTL(season_length=[24, 168]), # Hourly with daily and weekly seasonality AutoTBATS(season_length=[24, 168]) ] sf = StatsForecast(models=models, freq='H') forecasts = sf.forecast(df=hourly_df, h=168) ``` ### ARCH/GARCH for Volatility ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import GARCH, ARCH # Models for financial data with volatility volatility_models = [ GARCH(p=1, q=1), ARCH(p=1) ] sf = StatsForecast(models=volatility_models, freq='D') forecasts = sf.forecast(df=financial_df, h=30) ``` ### Using Scikit-learn Models ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import SklearnModel from sklearn.ensemble import RandomForestRegressor from sklearn.linear_model import Ridge # Wrap scikit-learn models models = [ SklearnModel(RandomForestRegressor(n_estimators=100), alias='RF'), SklearnModel(Ridge(alpha=1.0), alias='Ridge') ] sf = StatsForecast(models=models, freq='D') forecasts = sf.forecast(df=df, h=14) ``` ## Model Selection Tips * **For automatic selection**: Start with `AutoARIMA` or `AutoETS` * **For baseline comparison**: Use `Naive`, `SeasonalNaive`, or `HistoricAverage` * **For seasonal data**: Use models with `season_length` parameter * **For sparse data**: Use Croston family or ADIDA * **For multiple seasonalities**: Use MSTL or TBATS * **For volatile data**: Use GARCH or ARCH * **For ensemble approaches**: Combine multiple models and compare performance ## References For detailed information on the statistical models and algorithms, please refer to the [source code](https://github.com/Nixtla/statsforecast/blob/main/python/statsforecast/models.py) and the original academic papers referenced in the docstrings. # StatsForecast's Models Source: https://nixtlaverse.nixtla.io/statsforecast/src/core/models_intro.html ## Automatic Forecasting Automatic forecasting tools search for the best parameters and select the best possible model for a series of time series. These tools are useful for large collections of univariate time series. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | | :------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | | [`AutoARIMA`](./models.html#autoarima) | ✅ | ✅ | ✅ | ✅ | | [`AutoETS`](./models.html#autoets) | ✅ | ✅ | ✅ | ✅ | | [`AutoCES`](./models.html#autoces) | ✅ | ✅ | ✅ | ✅ | | [`AutoTheta`](./models.html#autotheta) | ✅ | ✅ | ✅ | ✅ | ## ARIMA Family These models exploit the existing autocorrelations in the time series. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | | :----------------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | | [`ARIMA`](./models.html#arima) | ✅ | ✅ | ✅ | ✅ | | [`AutoRegressive`](./models.html#autoregressive) | ✅ | ✅ | ✅ | ✅ | ## Theta Family Fit two theta lines to a deseasonalized time series, using different techniques to obtain and combine the two theta lines to produce the final forecasts. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | | :------------------------------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | | [`Theta`](./models.html#theta) | ✅ | ✅ | ✅ | ✅ | | [`OptimizedTheta`](./models.html#optimizedtheta) | ✅ | ✅ | ✅ | ✅ | | [`DynamicTheta`](./models.html#dynamictheta) | ✅ | ✅ | ✅ | ✅ | | [`DynamicOptimizedTheta`](./models.html#dynamicoptimizedtheta) | ✅ | ✅ | ✅ | ✅ | ## Multiple Seasonalities Suited for signals with more than one clear seasonality. Useful for low-frequency data like electricity and logs. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | | :--------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | | [`MSTL`](./models.html#mstl) | ✅ | ✅ | ✅ | ✅ | ## GARCH and ARCH Models Suited for modeling time series that exhibit non-constant volatility over time. The ARCH model is a particular case of GARCH. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | | :----------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | | [`GARCH`](./models.html#garch) | ✅ | ✅ | ✅ | ✅ | | [`ARCH`](./models.html#arch) | ✅ | ✅ | ✅ | ✅ | ## Baseline Models Classical models for establishing baseline. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | | :------------------------------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | | [`HistoricAverage`](./models.html#historicaverage) | ✅ | ✅ | ✅ | ✅ | | [`Naive`](./models.html#naive) | ✅ | ✅ | ✅ | ✅ | | [`RandomWalkWithDrift`](./models.html#randomwalkwithdrift) | ✅ | ✅ | ✅ | ✅ | | [`SeasonalNaive`](./models.html#seasonalnaive) | ✅ | ✅ | ✅ | ✅ | | [`WindowAverage`](./models.html#windowaverage) | ✅ | | | | | [`SeasonalWindowAverage`](./models.html#seasonalwindowaverage) | ✅ | | | | ## Exponential Smoothing Uses a weighted average of all past observations where the weights decrease exponentially into the past. Suitable for data with clear trend and/or seasonality. Use the `SimpleExponential` family for data with no clear trend or seasonality. | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | | :----------------------------------------------------------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | | [`SimpleExponentialSmoothing`](./models.html#simpleexponentialsmoothing) | ✅ | | | | | [`SimpleExponentialSmoothingOptimized`](./models.html#simpleexponentialsmoothingoptimized) | ✅ | | | | | [`Holt`](./models.html#holt) | ✅ | ✅ | ✅ | ✅ | | [`HoltWinters`](./models.html#holtwinters) | ✅ | ✅ | ✅ | ✅ | ## Sparse or Intermittent Suited for series with very few non-zero observations | Model | Point Forecast | Probabilistic Forecast | Insample fitted values | Probabilistic fitted values | | :--------------------------------------------------- | :------------: | :--------------------: | :--------------------: | :-------------------------: | | [`ADIDA`](./models.html#adida) | ✅ | | | | | [`CrostonClassic`](./models.html#crostonclassic) | ✅ | | | | | [`CrostonOptimized`](./models.html#crostonoptimized) | ✅ | | | | | [`CrostonSBA`](./models.html#crostonsba) | ✅ | | | | | [`IMAPA`](./models.html#imapa) | ✅ | | | | | [`TSB`](./models.html#tsb) | ✅ | | | | # Feature engineering | StatsForecast Source: https://nixtlaverse.nixtla.io/statsforecast/src/feature_engineering.html Generate features for downstream models ### `mstl_decomposition` ```python theme={null} mstl_decomposition(df, model, freq, h) ``` Decompose the series into trend and seasonal using the MSTL model. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------- | ------------------------------------------------- | ---------- | | `df` | pandas or polars DataFrame | DataFrame with columns \[`unique_id`, `ds`, `y`]. | *required* | | `model` | statsforecast MSTL | Model to use for the decomposition. | *required* | | `freq` | [str](#str) | Frequency of the data (pandas alias). | *required* | | `h` | [int](#int) | Forecast horizon. | *required* | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[DataFrame](#utilsforecast.compat.DataFrame), [DataFrame](#utilsforecast.compat.DataFrame)] | Tuple\[DataFrame, DataFrame]: A tuple containing: - train\_df (pandas or polars DataFrame): Original dataframe with the 'trend' and 'seasonal' columns added. - X\_df (pandas or polars DataFrame): Future dataframe to be provided to the predict method through `X_df`. | ```python theme={null} import pandas as pd from fastcore.test import test_fail from utilsforecast.losses import smape from statsforecast.models import Naive from statsforecast.utils import generate_series series = generate_series(10, freq='D') series['unique_id'] = series['unique_id'].astype('int64') ``` ```python theme={null} horizon = 14 model = MSTL(season_length=7) series = series.sample(frac=1.0) train_df, X_df = mstl_decomposition(series, model, 'D', horizon) ``` ```python theme={null} series_pl = generate_series(10, freq='D', engine='polars') series_pl = series_pl.with_columns(unique_id=pl.col('unique_id').cast(pl.Int64)) train_df_pl, X_df_pl = mstl_decomposition(series_pl, model, '1d', horizon) ``` ```python theme={null} pd.testing.assert_series_equal( train_df.groupby('unique_id')['ds'].max() + pd.offsets.Day(), X_df.groupby('unique_id')['ds'].min() ) assert X_df.shape[0] == train_df['unique_id'].nunique() * horizon pd.testing.assert_frame_equal(train_df, train_df_pl.to_pandas()) pd.testing.assert_frame_equal(X_df, X_df_pl.to_pandas()) with_estimate = train_df_pl.with_columns(estimate=pl.col('trend') + pl.col('seasonal')) assert smape(with_estimate, models=['estimate'])['estimate'].mean() < 0.1 ``` ```python theme={null} model = MSTL(season_length=[7, 28]) train_df, X_df = mstl_decomposition(series, model, 'D', horizon) assert train_df.columns.intersection(X_df.columns).tolist() == ['unique_id', 'ds', 'trend', 'seasonal7', 'seasonal28'] ``` # Generation and Composition Source: https://nixtlaverse.nixtla.io/synforecast/composition.html Convenience APIs, generator pools, and multivariate composition ### `generate_series` ```python theme={null} generate_series(n_series, freq='D', min_length=50, max_length=500, generators=None, engine='pandas', seed=0, with_generator_col=False) ``` Generate a synthetic panel of time series. Series are drawn from a balanced pool of generators covering diverse temporal behaviors (or from `generators` when provided) and returned in long format, mirroring `utilsforecast.data.generate_series`. Series are spread evenly across the generator list from the front, so when `n_series` is smaller than the pool only the first `n_series` generators contribute. The default pool is ordered round-robin across its behavioral niches, so a small panel still spans distinct behaviors: the first 15 generators cover all 15 niches. **Parameters:** | Name | Type | Description | Default | | -------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | `n_series` | [int](#int) | Number of series to generate. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data, as a pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer for an integer time index. Defaults to 'D'. | 'D' | | `min_length` | [int](#int) | Minimum length of each series. Defaults to 50. | 50 | | `max_length` | [int](#int) | Maximum length of each series. Defaults to 500. | 500 | | `generators` | [list](#list)\[[BaseGenerator](#synforecast.base.BaseGenerator)] | Generators to draw from. Defaults to `synforecast.balanced_pool`. Ignores min\_length / max\_length / freq / engine / seed when provided. | None | | `engine` | [str](#str) | Output dataframe library. Defaults to 'pandas'. | 'pandas' | | `seed` | [int](#int) | Random seed. Defaults to 0. | 0 | | `with_generator_col` | [bool](#bool) | When True, add a `generator` column with the alias of the generator that produced each series. Defaults to False. | False | **Returns:** | Type | Description | | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | [IntoDataFrame](#narwhals.stable.v2.typing.IntoDataFrame) | DataFrame in long format with columns \[`unique_id`, `ds`, `y`], plus `generator` when `with_generator_col` is True. | ### `balanced_pool` ```python theme={null} balanced_pool(min_length=200, max_length=200, freq='D', seed=42, **base_kwargs) ``` Create a balanced pool of generators covering diverse temporal behaviors. Returns 42 pre-configured generator instances across 15 behavioral niches, with allocation proportional to each generator's behavioral range. This avoids the implicit bias toward financial processes that occurs when using all generators equally. The list is ordered round-robin across the niches (one variant of every niche, then second variants, and so on), so any prefix spans as many distinct behaviors as possible: the first 15 entries cover all 15 niches. Consumers that use only the first k generators — such as `generate_series` with `n_series < 42` — therefore still get a behaviorally diverse panel.
Behavioral niches covered * ARMA + seasonality (SARIMA, 5 variants) * Exponential smoothing (ETS, 4 variants) * Long-range memory (Fractional Brownian Motion, 3 Hurst regimes) * Structural breaks (Regime Switching, 2 variants) * Volatility clustering (GARCH, 2 persistence levels) * Irregular cycles (Cyclic, 2 regularity levels) * Sparse/intermittent (Intermittent Demand, 3 patterns) * Multi-seasonal (Energy Load, 2 load types) * Sensor artifacts (IoT Sensor, 3 health states) * Physiological (Vital Signs, 3 patient types) * Smooth/rough functions (Gaussian Process, 4 kernels) * Deterministic chaos (Chaotic System, 3 systems) * Count time series (INAR, 2 innovation types) * Bounded/proportion data (Bounded Process, 2 models) * Heavy-tailed processes (Levy Process, 2 stability levels)
**Parameters:** | Name | Type | Description | Default | | --------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------------- | | `min_length` | [int](#int) | Minimum time series length for all generators. | 200 | | `max_length` | [int](#int) | Maximum time series length for all generators. | 200 | | `freq` | [str](#str) \| [int](#int) | Frequency for all generators, as a pandas offset alias or integer. | 'D' | | `seed` | [int](#int) \| None | Base random seed. Each generator gets seed + i for reproducibility. Set to None for random seeds. | 42 | | `**base_kwargs` | [Any](#typing.Any) | Additional keyword arguments passed to all generators (e.g., engine, id\_col, time\_col, target\_col). | \{} | **Returns:** | Type | Description | | ----------------------------------------------------------------------------- | ------------------------------------------------------------- | | [list](#list)\[[BaseGenerator](#synforecast.base.BaseGenerator)] | List of 42 BaseGenerator instances ready for use with SynSet. | **Examples:** ```pycon theme={null} >>> from synforecast import SynSet, balanced_pool >>> dataset = SynSet(balanced_pool(min_length=100, max_length=100, freq="D")) >>> df = dataset.generate(n_series_per_generator=1) ``` ### `pretraining_pool` ```python theme={null} pretraining_pool(min_length=256, max_length=1024, freq='D', seed=42, include_balanced=True, n_meta_variants=3, **base_kwargs) ``` Create a breadth-maximizing pool for foundation-model pretraining. This is the pretraining-oriented counterpart to :func:`balanced_pool`. It adds the diversity-targeted *meta-generators* that `balanced_pool` deliberately excludes — `TSIGenerator` (randomized trend/seasonal/ irregular composition), `TCMGenerator` (random temporal causal graphs), and `KernelSynthGenerator` (samples from randomly composed GP kernels). Each resamples a fresh configuration per series, so a handful of instances spans a very wide distribution. By default it also includes the full `balanced_pool` so the corpus carries interpretable single-mechanism behaviors alongside the meta-generators. Unlike `balanced_pool`, the default length range is wide (256-1024 steps), matching the longer contexts typical of pretraining. **Parameters:** | Name | Type | Description | Default | | ------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `min_length` | [int](#int) | Minimum series length for all generators. | 256 | | `max_length` | [int](#int) | Maximum series length for all generators. | 1024 | | `freq` | [str](#str) \| [int](#int) | Frequency for all generators, as a pandas offset alias or integer. | 'D' | | `seed` | [int](#int) \| None | Base random seed. Each generator gets a distinct offset. Set to None for random seeds. | 42 | | `include_balanced` | [bool](#bool) | When True (default), prepend the full :func:`balanced_pool`; when False, return only the meta-generators (a purely procedural pretraining corpus). | True | | `n_meta_variants` | [int](#int) | Number of independently-seeded instances of each meta-generator (default 3). More instances give the meta-generators a larger share when series are spread evenly across the pool, as in :func:`generate_series`. | 3 | | `**base_kwargs` | [Any](#typing.Any) | Additional keyword arguments passed to all generators (e.g., engine, id\_col, time\_col, target\_col). | \{} | **Returns:** | Type | Description | | ----------------------------------------------------------------------------- | ---------------------------------------------------------- | | [list](#list)\[[BaseGenerator](#synforecast.base.BaseGenerator)] | List of BaseGenerator instances ready for use with SynSet. | **Examples:** ```pycon theme={null} >>> from synforecast import SynSet, pretraining_pool >>> pool = pretraining_pool(min_length=512, max_length=512, freq="h") >>> df = SynSet(pool).generate(n_series_per_generator=1) ``` ```pycon theme={null} >>> # Purely procedural corpus (meta-generators only) >>> meta = pretraining_pool(include_balanced=False) ``` ### `Multivariatizer` Bases: [BaseModel](#pydantic.BaseModel) Wrap a univariate :class:`BaseGenerator` to produce correlated channels. `generate(n_series)` draws `n_series` independent series of one shared length from the wrapped generator, standardizes them, applies the configured couplings, then restores each channel's original level and scale. The output is the same long-format frame the wrapped generator produces (its `id_col`/`time_col`/`target_col` and `engine`). Couplings (both may compose; `mixing` is applied first): * `"mixing"` (cotemporaneous): channels become instantaneous linear combinations `Z @ L.T` of the standardized bases, where `L` is the Cholesky factor of a random well-conditioned correlation target `C = (1 - s) I + s Q`. `Q` is the correlation matrix of a random Gaussian Gram matrix and `s` (the mixing strength, drawn from `mixing_strength_range`) directly sets the magnitude of the induced cross-correlations; `s < 1` keeps `C` positive definite, so `L` is well conditioned. * `"leadlag"` (sequential): each non-root channel becomes, with probability 0.5 (at least one always does), a lagged, sign-flipped, noise-perturbed copy of an earlier channel: `z_j = sign * roll(z_src, lag) + sigma * eps` with `lag` from `lag_range` (clamped below the series length, circular wrap so all channels share the same timestamps) and `sigma` from `noise_scale_range`. Per-channel scaling comes from the level/scale restore. Guards: every drawn base series must be finite with `|x| < 1e8` and `std > 1e-8`; violating draws are redrawn up to 5 times, then replaced by unit Gaussian noise. Seeding: the multivariatizer's own `seed` fully determines the output. The wrapped generator is copied and reseeded from the multivariatizer's rng on every `generate` call, so the base generator's own seed and rng state never influence the result and the original object is not mutated. The last sampled coupling recipe (mixing strength/matrix, lead-lag pairs with their lags) is exposed as `last_recipe` for introspection. **Parameters:** | Name | Type | Description | Default | | ----------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------- | | `base` | [BaseGenerator](#synforecast.base.BaseGenerator) | Wrapped univariate generator; supplies the length range, frequency, column names, and dataframe engine. | *required* | | `couplings` | [list](#list)\[[str](#str)] | Couplings to apply, subset of \['mixing', 'leadlag'] (default: both). | *required* | | `mixing_strength_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Range for the mixing strength s in \[0, 1); s scales the induced cotemporaneous cross-correlations (default: (0.2, 0.9)). | *required* | | `lag_range` | [tuple](#tuple)\[[int](#int), [int](#int)] | Inclusive range for lead-lag offsets in time steps (default: (1, 24)). | *required* | | `noise_scale_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Range for the lead-lag perturbation noise std, relative to the unit-variance standardized channels (default: (0.02, 0.2)). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* |
Example > > > from synforecast.generators import TSIGenerator > > > base = TSIGenerator(min\_length=256, max\_length=512, freq="h") > > > mv = Multivariatizer(base=base, seed=42) > > > df = mv.generate(n\_series=4)
#### `Multivariatizer.generate` ```python theme={null} generate(n_series, start_id=0) ``` Generate n\_series cross-dependent channels from the wrapped base. All channels share one length drawn from the base generator's \[min\_length, max\_length]; each channel is one `unique_id` in the long-format output. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | ---------------------------------------------- | -------------- | | `n_series` | [int](#int) | Number of coupled channels to generate. | *required* | | `start_id` | [int](#int) | Starting ID for series numbering (default: 0). | 0 | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | DataFrame in long format with the wrapped generator's \[id\_col, time\_col, target\_col] columns and dataframe engine. | #### `Multivariatizer.last_recipe` ```python theme={null} last_recipe: dict | None ``` The coupling recipe sampled by the most recent generate() call. # BaseGenerator Source: https://nixtlaverse.nixtla.io/synforecast/core.html Base class for all time series generators ### `BaseGenerator` Bases: [BaseModel](#pydantic.BaseModel), [ABC](#abc.ABC) Base class for all time series generators. **Parameters:** | Name | Type | Description | Default | | ------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `min_length` | [int](#int) | Minimum length of each series | *required* | | `max_length` | [int](#int) | Maximum length of each series | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data. Either a pandas offset alias (e.g. 'D', 'h', '5min', 'MS', 'W-MON') or an integer for an integer time index | *required* | | `engine` | [str](#str) | Output dataframe library (default: 'pandas'). Options are 'pandas', 'polars', 'cudf', 'modin', 'pyarrow' | *required* | | `alias` | [str](#str) \| None | Name of the generator (default: class name) | *required* | | `id_col` | [str](#str) | Name of the ID column (default: 'unique\_id') | *required* | | `time_col` | [str](#str) | Name of the timestamp column (default: 'ds') | *required* | | `target_col` | [str](#str) | Name of the value column (default: 'y') | *required* | | `start_datetime` | [str](#str) | First timestamp of every series, in any format accepted by pandas.Timestamp (default: '2000-01-01'). Ignored when freq is an integer | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None) | *required* | | `Exogenous parameters` | | | *required* | | `exogenous` | [ExogenousConfig](#synforecast.exogenous.ExogenousConfig) \| None | Configuration for exogenous variable generation. None = no exogenous columns (default: None) | *required* | | `Missingness parameters` | | | *required* | | `missing_data` | [bool](#bool) | Enable missing data patterns (default: False) | *required* | | `missing_pattern` | [str](#str) | Pattern: 'random', 'block', 'seasonal' (default: 'random') | *required* | | `missing_rate` | [float](#float) | Proportion of missing values 0-1 (default: 0.1) | *required* | | `missing_block_size` | [int](#int) | Size of missing blocks for 'block' pattern (default: 3) | *required* | | `missing_seasonal_period` | [int](#int) | Period for 'seasonal' pattern (default: 7) | *required* | | `Anomaly parameters` | | | *required* | | `anomalies` | [bool](#bool) | Enable anomaly injection (default: False) | *required* | | `anomaly_fraction` | [float](#float) | Fraction of points that are anomalies (default: 0.05) | *required* | | `anomaly_types` | [list](#list)\[[str](#str)] | Types: 'spike', 'dip', 'level\_shift' (default: \['spike', 'dip']) | *required* | | `spike_magnitude` | [float](#float) | Magnitude of spikes (default: 10.0) | *required* | | `dip_magnitude` | [float](#float) | Magnitude of dips (default: -10.0) | *required* | | `level_shift_magnitude` | [float](#float) | Magnitude of level shifts (default: 20.0) | *required* | | `level_shift_duration` | [int](#int) | Duration of level shifts in time steps (default: 10) | *required* | | `Changepoint parameters` | | | *required* | | `changepoints` | [bool](#bool) | Enable changepoint injection (default: False) | *required* | | `num_changepoints` | [int](#int) | Number of changepoints (default: 2) | *required* | | `changepoint_type` | [str](#str) | Type: 'level', 'trend', 'variance', 'mixed' (default: 'level') | *required* | | `changepoint_level_changes` | [list](#list)\[[float](#float)] \| None | Size of level changes (default: random) | *required* | | `changepoint_trend_changes` | [list](#list)\[[float](#float)] \| None | Size of trend changes (default: random) | *required* | | `changepoint_variance_changes` | [list](#list)\[[float](#float)] \| None | Size of variance changes (default: random) | *required* | | `changepoint_locations` | [list](#list)\[[float](#float)] \| None | Relative positions 0-1 (default: random) | *required* | #### `BaseGenerator.generate` ```python theme={null} generate(n_series, start_id=0, n_jobs=-1) ``` Generate synthetic time series data. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | | `n_series` | [int](#int) | Number of time series to generate | *required* | | `start_id` | [int](#int) | Starting ID for the series numbering (default: 0) Series will be numbered from start\_id to start\_id + n\_series - 1 | 0 | | `n_jobs` | [int](#int) | Number of parallel workers. -1 (default) uses `RAYON_NUM_THREADS` if set, otherwise all logical cores. Results are seed-deterministic and do not depend on n\_jobs. | -1 | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | DataFrame in long format with columns \[id\_col, time\_col, target\_col] (default \['unique\_id', 'ds', 'y']), plus any exogenous or flag columns. | #### `BaseGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | # Dataset Source: https://nixtlaverse.nixtla.io/synforecast/dataset.html Dataset composition and augmentation ### `SynSet` ```python theme={null} SynSet(generators) ``` Generate synthetic time series datasets from multiple generators. Combine multiple generators into one long-format panel; each generator contributes its own type of time series pattern. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------- | | `generators` | [list](#list)\[[BaseGenerator](#synforecast.base.BaseGenerator)] | List of instantiated generator objects to use for creating time series. | *required* | **Examples:** ```pycon theme={null} >>> from synforecast import SynSet >>> from synforecast.generators import RandomWalkGenerator, SeasonalGenerator >>> >>> # Create generators >>> rw_gen = RandomWalkGenerator( ... min_length=100, ... max_length=150, ... freq="h", ... seed=42, ... ) >>> seasonal_gen = SeasonalGenerator( ... min_length=100, ... max_length=150, ... freq="h", ... seed=43, ... ) >>> >>> # Create dataset >>> dataset = SynSet([rw_gen, seasonal_gen]) >>> df = dataset.generate(n_series_per_generator=5) ``` Initialize the SynSet with a list of generators. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------------------------------------------------------------- | --------------------------------------- | ---------- | | `generators` | [list](#list)\[[BaseGenerator](#synforecast.base.BaseGenerator)] | List of instantiated generator objects. | *required* | **Raises:** | Type | Description | | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | [ValueError](#ValueError) | If generators list is empty or contains non-BaseGenerator objects. If generators have inconsistent column names (id\_col, time\_col, target\_col). | #### `SynSet.generate` ```python theme={null} generate(n_series_per_generator, n_jobs=-1) ``` Generate synthetic time series data from all generators. **Parameters:** | Name | Type | Description | Default | | ------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | | `n_series_per_generator` | [int](#int) | Number of time series to generate from each generator. | *required* | | `n_jobs` | [int](#int) | Number of parallel workers. -1 (default) uses `RAYON_NUM_THREADS` if set, otherwise all logical cores. Results are seed-deterministic and do not depend on n\_jobs. | -1 | **Returns:** | Type | Description | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | [IntoFrameT](#narwhals.stable.v2.typing.IntoFrameT) | DataFrame containing all generated time series from all generators, in long format with columns \[id\_col, time\_col, target\_col]. |
Notes Series IDs are unique across all generators. With 2 generators and 3 series per generator, IDs run from 0 to 5.
### `SynAugment` ```python theme={null} SynAugment(id_col='unique_id', time_col='ds', target_col='y', seed=None, engine=None, on_error='raise') ``` Augment time series datasets with synthetic series. Analyzes input time series, auto-selects appropriate generators based on statistical properties, fits parameters, and generates statistically similar synthetic series. The augmentation process: 1. For each unique series in the input DataFrame, analyze its statistical properties 2. Auto-select the most appropriate generator (or use user override) 3. Fit generator parameters to match the series' statistical fingerprint 4. Generate n\_augment synthetic series that preserve these properties 5. Return combined DataFrame with original and synthetic series Synthetic series IDs follow the pattern `"{original_id}_aug_{i}"` **Parameters:** | Name | Type | Description | Default | | ------------ | -------------------------------- | ------------------------------------------------------------------------------- | ------------------------- | | `id_col` | [str](#str) | Name of the ID column (default: 'unique\_id') | 'unique\_id' | | `time_col` | [str](#str) | Name of the timestamp column (default: 'ds') | 'ds' | | `target_col` | [str](#str) | Name of the value column (default: 'y') | 'y' | | `seed` | [int](#int) \| None | Random seed for reproducibility | None | | `engine` | [str](#str) \| None | Output dataframe library. None (default) matches the input DataFrame's library. | None |
Example > > > from synforecast import SynAugment > > > import polars as pl > > > > > > # Create sample data > > > > > > df = pl.DataFrame( > > > ... \{ > > > ... "unique\_id": \["series\_0"] \* 100, > > > ... "ds": pl.date\_range( > > > ... pl.date(2020, 1, 1), pl.date(2020, 4, 9), eager=True > > > ... ), > > > ... "y": \[i + np.random.randn() for i in range(100)], > > > ... } > > > ... ) > > > > > > # Augment the dataset > > > > > > augmenter = SynAugment(seed=42) > > > augmented\_df = augmenter.augment(df, n\_augment=3) > > > augmented\_df\["unique\_id"].n\_unique() > > > 4
Initialize the SynAugment instance. **Parameters:** | Name | Type | Description | Default | | ------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `id_col` | [str](#str) | Name of the ID column | 'unique\_id' | | `time_col` | [str](#str) | Name of the timestamp column | 'ds' | | `target_col` | [str](#str) | Name of the value column | 'y' | | `seed` | [int](#int) \| None | Random seed for reproducibility | None | | `engine` | [str](#str) \| None | Output dataframe library (e.g. 'pandas', 'polars'). None (default) matches the input DataFrame's library. | None | | `on_error` | [Literal](#typing.Literal)\['raise', 'ar1'] | What to do when a fitted generator fails for a series. 'raise' (default) propagates the error; 'ar1' substitutes an AR(1) series matching the source's mean, std, and lag-1 autocorrelation, and reports all substitutions in a single warning at the end of `augment`. | 'raise' | #### `SynAugment.analyze` ```python theme={null} analyze(df) ``` Analyze all series in DataFrame and return properties. For each unique series, detects statistical properties and recommends the most appropriate generator. **Parameters:** | Name | Type | Description | Default | | ---- | ---------------------------------------------------------------- | --------------------------------------------------------------------------- | ---------- | | `df` | [IntoFrameT](#narwhals.stable.v2.typing.IntoFrameT) | DataFrame with time series data (must have id\_col, time\_col, target\_col) | *required* | **Returns:** | Type | Description | | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [dict](#dict)\[[str](#str), [dict](#dict)] | Mapping of each `unique_id` to its analysis results. Every entry holds `recommended_generator` (the chosen generator name), `properties` (all detected statistical properties), and `fitted_params` (the estimated generator parameters). | **Raises:** | Type | Description | | -------------------------------------- | ---------------------------------------- | | [ValueError](#ValueError) | If DataFrame is missing required columns | #### `SynAugment.augment` ```python theme={null} augment(df, n_augment=1, generator_override=None, preserve_timestamps=True) ``` Augment dataset with synthetic series. For each series in the input DataFrame, generates n\_augment synthetic series that are statistically similar to the original. **Parameters:** | Name | Type | Description | Default | | --------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `df` | [IntoFrameT](#narwhals.stable.v2.typing.IntoFrameT) | Input DataFrame with time series (must have id\_col, time\_col, target\_col) | *required* | | `n_augment` | [int](#int) | Number of synthetic series to generate per original series | 1 | | `generator_override` | [dict](#dict)\[[str](#str), [str](#str)] \| None | Optional dict mapping unique\_id to generator name. Overrides automatic generator selection for specified series. Example: \{"series\_0": "SARIMAGenerator"} | None | | `preserve_timestamps` | [bool](#bool) | If True, synthetic series use the same timestamps as the original series | True | **Returns:** | Type | Description | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | [IntoFrameT](#narwhals.stable.v2.typing.IntoFrameT) | Combined DataFrame with original and synthetic series. Synthetic series IDs follow the pattern `"{original_id}_aug_{i}"` | **Raises:** | Type | Description | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | [ValueError](#ValueError) | If DataFrame is missing required columns, n\_augment \< 1, or generator\_override names an unsupported generator | | [RuntimeError](#RuntimeError) | If a fitted generator fails for a series and the instance was created with on\_error='raise' (the default) |
Example > > > augmenter = SynAugment(seed=42) > > > > > > # Basic augmentation > > > > > > augmented\_df = augmenter.augment(df, n\_augment=3) # doctest: +SKIP > > > > > > # With generator override > > > > > > augmented\_df = augmenter.augment( # doctest: +SKIP > > > ... df, n\_augment=2, generator\_override=\{"series\_0": "SARIMAGenerator"} > > > ... )
#### `SynAugment.augment_single_series` ```python theme={null} augment_single_series(series_id, values, timestamps, n_augment=1, generator_name=None) ``` Augment a single series (lower-level API). **Parameters:** | Name | Type | Description | Default | | ---------------- | -------------------------------------- | --------------------------------------------- | ----------------- | | `series_id` | [str](#str) | ID of the original series | *required* | | `values` | [ndarray](#numpy.ndarray) | Array of time series values | *required* | | `timestamps` | [ndarray](#numpy.ndarray) | Array of timestamps | *required* | | `n_augment` | [int](#int) | Number of synthetic series to generate | 1 | | `generator_name` | [str](#str) \| None | Optional generator name; if None, auto-detect | None | **Returns:** | Type | Description | | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | [list](#list)\[[tuple](#tuple)\[[str](#str), [ndarray](#numpy.ndarray), [ndarray](#numpy.ndarray)]] | List of tuples: (new\_id, synthetic\_values, timestamps) | # Anomaly injection Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/anomalies.html Real series are punctuated by spikes, dips, and sustained level shifts — sensor glitches, promotions, outages. Any SynForecast generator can inject these on demand, so you can measure how a model or an anomaly detector behaves when they appear, with the exact locations known in advance. > **Ground-truth labels** > > Set `exogenous=ExogenousConfig(anomaly_flags=True)` and the output > gains an `anomaly_flag` column marking every injected point. That > turns a synthetic series into a labelled benchmark for detection > methods. The plots below use it to highlight the injected anomalies in > red. ```python theme={null} import matplotlib.pyplot as plt import polars as pl from synforecast.exogenous import ExogenousConfig from synforecast.generators import RandomWalkGenerator, SeasonalGenerator, VARGenerator FLAGS = ExogenousConfig(anomaly_flags=True) def plot_anomalies(df, title): """Plot each series and mark injected anomalies (anomaly_flag == 1) in red.""" fig, ax = plt.subplots(figsize=(11, 4)) multi = df["unique_id"].n_unique() > 1 for uid in df["unique_id"].unique(maintain_order=True).to_list(): s = df.filter(pl.col("unique_id") == uid).sort("ds") ax.plot(s["ds"], s["y"], linewidth=1, alpha=0.8, label=str(uid)) hits = s.filter(pl.col("anomaly_flag") == 1) ax.scatter(hits["ds"], hits["y"], color="crimson", s=25, zorder=3, label="injected" if not multi else None) ax.set(title=title, xlabel="ds", ylabel="y") ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## Point anomalies: spikes and dips Spikes and dips are single-point outliers. `anomaly_fraction` sets how many points are affected; `spike_magnitude` and `dip_magnitude` set their size (in the series’ own units). ```python theme={null} point_gen = RandomWalkGenerator( engine="polars", min_length=200, max_length=200, freq="D", drift=0.1, volatility=2.0, anomalies=True, anomaly_fraction=0.05, anomaly_types=["spike", "dip"], spike_magnitude=20.0, dip_magnitude=-20.0, exogenous=FLAGS, seed=42, ) point_df = point_gen.generate(n_series=1) plot_anomalies(point_df, "Random walk with 5% spikes and dips") ``` Each red marker is an injected outlier that departs sharply from the local trend, then the series resumes as if nothing happened — the signature of a point anomaly. ## Level shifts A level shift is a *sustained* jump: the series steps to a new level and stays there for `level_shift_duration` steps. These are harder for models than isolated spikes because they look like a regime change. ```python theme={null} shift_gen = RandomWalkGenerator( engine="polars", min_length=200, max_length=200, freq="D", drift=0.05, volatility=1.5, anomalies=True, anomaly_fraction=0.03, anomaly_types=["level_shift"], level_shift_magnitude=25.0, level_shift_duration=15, exogenous=FLAGS, seed=42, ) plot_anomalies(shift_gen.generate(n_series=1), "Random walk with 15-step level shifts") ``` The flagged region marks where each shift begins; the elevated plateau that follows is the sustained deviation. ## On any generator, including multivariate Anomaly injection is part of the shared generator pipeline, so it works the same on a seasonal series or a multivariate `VAR`, where each channel is perturbed independently. ```python theme={null} seasonal_df = SeasonalGenerator( engine="polars", min_length=200, max_length=200, freq="D", seasonality_period=7, seasonality_amplitude=10.0, base_level=100.0, anomalies=True, anomaly_fraction=0.05, anomaly_types=["dip"], dip_magnitude=-30.0, exogenous=FLAGS, seed=42, ).generate(n_series=1) plot_anomalies(seasonal_df, "Weekly-seasonal series with injected dips") ``` ```python theme={null} var_df = VARGenerator( engine="polars", min_length=150, max_length=150, freq="D", lag_order=1, anomalies=True, anomaly_fraction=0.05, anomaly_types=["spike", "dip"], spike_magnitude=15.0, dip_magnitude=-15.0, exogenous=FLAGS, seed=42, ).generate(n_series=3) plot_anomalies(var_df, "Three correlated VAR series, anomalies injected per channel") ``` ## Combine with missing data Anomalies and missing values compose, so you can build a realistically messy series in one call — outliers to detect and gaps to impute. ```python theme={null} messy_df = RandomWalkGenerator( engine="polars", min_length=200, max_length=200, freq="D", drift=0.1, volatility=2.0, anomalies=True, anomaly_fraction=0.05, anomaly_types=["spike", "dip"], spike_magnitude=25.0, dip_magnitude=-25.0, missing_data=True, missing_pattern="random", missing_rate=0.1, exogenous=FLAGS, seed=42, ).generate(n_series=1) print(f"missing values: {messy_df['y'].null_count()} of {len(messy_df)}") plot_anomalies(messy_df, "Anomalies (red) plus 10% missing values (gaps)") ``` ```text theme={null} missing values: 0 of 200 ``` > **Related capabilities** > > * [Changepoints](changepoints) — structural breaks in level, trend, > or variance (a modelled regime change rather than an anomaly). > * [Missingness](missingness) — random, block, and seasonal gap > patterns. > * The magnitudes and durations here are the common knobs; every > anomaly parameter is listed in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # SynAugment: data augmentation Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/augmentation.html `SynAugment` expands a panel of real series with synthetic look-alikes. For each input series it detects the pattern (seasonality, trend, stationarity), picks a matching generator, fits its parameters, and draws new series that share the original’s statistical fingerprint — a cheap way to give a global model more to learn from. > **Two augmentation strategies** > > * **`augment`** (this guide) — fit a generator per series and draw > statistically similar copies. > * **[`mixup`](#tsmixup-convex-combinations-of-real-series)** — blend > several real series with convex weights (TSMixup), covered at the > end. > > Fit augmentation on the **training split only** — fitting on > validation or test observations leaks information into training. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import polars as pl from synforecast import SynAugment from synforecast.generators import RandomWalkGenerator, SeasonalGenerator ``` ## Basic augmentation Create a simple random walk series and augment it with 3 synthetic copies. ```python theme={null} n = 100 np.random.seed(42) df = pl.DataFrame( { "unique_id": ["series_0"] * n, "ds": pl.datetime_range( pl.datetime(2020, 1, 1), pl.datetime(2020, 1, 1) + pl.duration(hours=n - 1), interval="1h", eager=True, ), "y": np.cumsum(np.random.randn(n) * 0.5 + 0.01), } ) print(f"Original dataset: {len(df)} rows, {df['unique_id'].n_unique()} series") augmenter = SynAugment(seed=42) augmented_df = augmenter.augment(df, n_augment=3) print( f"Augmented dataset: {len(augmented_df)} rows, {augmented_df['unique_id'].n_unique()} series" ) print(f"Series IDs: {sorted(augmented_df['unique_id'].unique().to_list())}") ``` ```text theme={null} Original dataset: 100 rows, 1 series Augmented dataset: 400 rows, 4 series Series IDs: ['series_0', 'series_0_aug_0', 'series_0_aug_1', 'series_0_aug_2'] ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in augmented_df["unique_id"].unique().to_list(): series = augmented_df.filter(pl.col("unique_id") == uid) is_original = "aug" not in uid ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.9 if is_original else 0.5, linewidth=2 if is_original else 1) ax.set_title("Original vs augmented series") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## Analyzing series before augmentation SynAugment analyzes each series to detect its properties and recommend the best generator. ```python theme={null} n = 150 t = np.arange(n) seasonal_values = 50 + 10 * np.sin(2 * np.pi * t / 24) + np.random.randn(n) * 2 random_walk_values = np.cumsum(np.random.randn(n)) intermittent_values = np.zeros(n) demand_times = np.random.choice(n, size=30, replace=False) intermittent_values[demand_times] = np.random.randint(1, 20, 30) multi_df = pl.DataFrame( { "unique_id": ["seasonal"] * n + ["random_walk"] * n + ["intermittent"] * n, "ds": list( pl.datetime_range( pl.datetime(2020, 1, 1), pl.datetime(2020, 1, 1) + pl.duration(hours=n - 1), interval="1h", eager=True, ) ) * 3, "y": list(seasonal_values) + list(random_walk_values) + list(intermittent_values), } ) augmenter = SynAugment(seed=42) analysis = augmenter.analyze(multi_df) print("Analysis results for each series:") for series_id, info in analysis.items(): print(f"\n {series_id}:") print(f" Recommended generator: {info['recommended_generator']}") props = info["properties"] print(f" Has seasonality: {props['seasonality']['has_seasonality']}") print(f" Has trend: {props['trend']['has_trend']}") print(f" Is stationary: {props['stationarity']['is_stationary']}") print(f" Is intermittent: {props['intermittency']['is_intermittent']}") ``` ```text theme={null} Analysis results for each series: seasonal: Recommended generator: SeasonalGenerator Has seasonality: True Has trend: False Is stationary: True Is intermittent: False intermittent: Recommended generator: IntermittentDemandGenerator Has seasonality: False Has trend: False Is stationary: True Is intermittent: True random_walk: Recommended generator: RandomWalkGenerator Has seasonality: False Has trend: True Is stationary: False Is intermittent: False ``` ```python theme={null} fig, axes = plt.subplots(1, 3, figsize=(15, 4)) series_names = ["seasonal", "random_walk", "intermittent"] for i, name in enumerate(series_names): series = multi_df.filter(pl.col("unique_id") == name) axes[i].plot(series["ds"].to_list(), series["y"].to_list(), alpha=0.8) axes[i].set_title(f"{name} (rec: {analysis[name]['recommended_generator']})") axes[i].set_xlabel("Timestamp") axes[i].set_ylabel("Value") plt.tight_layout() plt.show() ``` ## Using generator overrides Override the auto-detected generator for specific series when you want to use a particular model. ```python theme={null} augmented_override = augmenter.augment( multi_df, n_augment=1, generator_override={ "seasonal": "SARIMAGenerator", "random_walk": "FractionalBrownianMotionGenerator", }, ) print(f"Augmented with overrides: {augmented_override['unique_id'].n_unique()} series") unique_ids = sorted(augmented_override["unique_id"].unique().to_list()) print(f"Series IDs: {unique_ids}") ``` ```text theme={null} Augmented with overrides: 6 series Series IDs: ['intermittent', 'intermittent_aug_0', 'random_walk', 'random_walk_aug_0', 'seasonal', 'seasonal_aug_0'] ``` ## Comparing original vs synthetic statistics Verify that the augmented series preserve the statistical properties of the original. ```python theme={null} rw_df = df.clone() augmented = augmenter.augment(rw_df, n_augment=5) original = augmented.filter(pl.col("unique_id") == "series_0") print(f"Original series (series_0):") print(f" Mean: {original['y'].mean():.4f}") print(f" Std: {original['y'].std():.4f}") print(f" Min: {original['y'].min():.4f}") print(f" Max: {original['y'].max():.4f}") print(f"\nSynthetic series:") for i in range(5): aug = augmented.filter(pl.col("unique_id") == f"series_0_aug_{i}") print(f" series_0_aug_{i}: mean={aug['y'].mean():.4f}, std={aug['y'].std():.4f}") ``` ```text theme={null} Original series (series_0): Mean: -2.6976 Std: 2.0971 Min: -5.4833 Max: 2.3403 Synthetic series: series_0_aug_0: mean=-2.6977, std=2.0971 series_0_aug_1: mean=-2.6973, std=2.0972 series_0_aug_2: mean=-2.6975, std=2.0969 series_0_aug_3: mean=-2.6970, std=2.0969 series_0_aug_4: mean=-2.6976, std=2.0970 ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in augmented["unique_id"].unique().to_list(): series = augmented.filter(pl.col("unique_id") == uid) is_original = "aug" not in uid ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.9 if is_original else 0.4, linewidth=2 if is_original else 0.8) ax.set_title("Original vs 5 augmented series — statistical comparison") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.legend(fontsize=7, ncol=2) plt.tight_layout() plt.show() ``` ## Augmenting a generated dataset Generate a dataset using SynForecast generators, then augment the combined dataset. ```python theme={null} rw_gen = RandomWalkGenerator(engine="polars", **{ "min_length": 100, "max_length": 100, "freq": "h", "drift": 0.05, "volatility": 1.0, "seed": 42, } ) seasonal_gen = SeasonalGenerator(engine="polars", **{ "min_length": 100, "max_length": 100, "freq": "h", "seasonality_period": 24, "seasonality_amplitude": 5.0, "trend": 0.01, "seed": 43, } ) rw_df = rw_gen.generate(n_series=2) seasonal_df = seasonal_gen.generate(n_series=2, start_id=2) combined_df = pl.concat([rw_df, seasonal_df]) print(f"Generated dataset: {combined_df['unique_id'].n_unique()} series") print(f"Series IDs: {sorted(combined_df['unique_id'].unique().to_list())}") augmenter = SynAugment(seed=42) augmented_combined = augmenter.augment(combined_df, n_augment=2) print(f"\nAfter augmentation: {augmented_combined['unique_id'].n_unique()} series") print(f"Series IDs: {sorted(augmented_combined['unique_id'].unique().to_list())}") ``` ```text theme={null} Generated dataset: 4 series Series IDs: ['0', '1', '2', '3'] After augmentation: 12 series Series IDs: ['0', '0_aug_0', '0_aug_1', '1', '1_aug_0', '1_aug_1', '2', '2_aug_0', '2_aug_1', '3', '3_aug_0', '3_aug_1'] ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 5)) for uid in augmented_combined["unique_id"].unique().to_list(): series = augmented_combined.filter(pl.col("unique_id") == uid) is_original = "aug" not in uid ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.9 if is_original else 0.4, linewidth=2 if is_original else 0.8) ax.set_title("Generated dataset: original and augmented series") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.legend(fontsize=6, ncol=3) plt.tight_layout() plt.show() ``` ## TSMixup: convex combinations of real series `SynAugment.mixup` implements TSMixup, the augmentation used to pretrain the Chronos models (Ansari et al. 2024). Each synthetic series is a convex combination of a random window from `1..max_mix` source series, weighted by a `Dirichlet(alpha)` draw. Unlike `augment`, which fits one generator per series, a mixup series blends the dynamics of several — a cheap way to broaden a small panel without fitting any model. The blend lives in scaled space: sources are normalized before mixing (`scaling="mean"` divides by the mean absolute value, the Chronos default), so the output shares the scaling rather than any single source’s level. Use `scaling="none"` when the series already share a scale. ```python theme={null} mixed = SynAugment(seed=0).mixup(combined_df, n_series=6, max_mix=3, scaling="mean") mixup_ids = [ u for u in mixed["unique_id"].unique().to_list() if str(u).startswith("mixup_") ] print( f"{combined_df['unique_id'].n_unique()} source series " f"-> {len(mixup_ids)} TSMixup series" ) mixed.filter(pl.col("unique_id") == mixup_ids[0]).head() ``` ```text theme={null} 4 source series -> 6 TSMixup series ``` | unique\_id | ds | y | | ---------- | ------------------- | -------- | | cat | datetime\[ns] | f64 | | "mixup\_0" | 2000-01-01 00:00:00 | 0.388051 | | "mixup\_0" | 2000-01-01 01:00:00 | 0.405526 | | "mixup\_0" | 2000-01-01 02:00:00 | 0.565628 | | "mixup\_0" | 2000-01-01 03:00:00 | 0.651099 | | "mixup\_0" | 2000-01-01 04:00:00 | 0.666551 | ```python theme={null} fig, axes = plt.subplots(1, 2, figsize=(13, 4), sharex=True) for uid in combined_df["unique_id"].unique(maintain_order=True).to_list(): s = combined_df.filter(pl.col("unique_id") == uid) axes[0].plot(s["ds"], s["y"], linewidth=1, label=str(uid)) axes[0].set_title("Source series (raw scale)") axes[0].legend(fontsize=8) for uid in mixup_ids[:4]: s = mixed.filter(pl.col("unique_id") == uid) axes[1].plot(s["ds"], s["y"], linewidth=1, label=str(uid)) axes[1].set_title("TSMixup series (mean-scaled blends)") axes[1].legend(fontsize=8) for ax in axes: ax.set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## Low-Level API - augment\_single\_series For fine-grained control, use the low-level API to augment individual series directly. ```python theme={null} single_series = df.filter(pl.col("unique_id") == "series_0") values = single_series["y"].to_numpy() timestamps = single_series["ds"].to_numpy() augmenter = SynAugment(seed=42) augmented_tuples = augmenter.augment_single_series( series_id="my_series", values=values, timestamps=timestamps, n_augment=2, generator_name=None, ) print(f"Generated {len(augmented_tuples)} augmented series:") for aug_id, aug_values, aug_ts in augmented_tuples: print(f" {aug_id}: length={len(aug_values)}, mean={np.mean(aug_values):.4f}") ``` ```text theme={null} Generated 2 augmented series: my_series_aug_0: length=100, mean=-2.6976 my_series_aug_1: length=100, mean=-2.6975 ``` # Data augmentation with real datasets Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/augmentation_real_data.html This example augments real M4 time series with `SynAugment` and shows what the augmentation actually guarantees. `SynAugment` fits a generator to each input series, draws new series from it, and then rescales every synthetic draw to the source series’ **mean**, **standard deviation**, and **lag-1 autocorrelation**. Those three statistics therefore match *by construction* — reporting them back as “close to the original” would only measure the rescaling step, not the quality of the augmentation. So instead of a fidelity table dominated by matched-by-design statistics, this page reports two things the rescaling does not determine: 1. **Whether the synthetic series are new draws rather than near-copies.** We check the correlation between each synthetic series and its source. 2. **How the unpinned properties behave** — the distribution shape (min, max, skewness), which is free to vary and shows how much the synthetic draws explore around the target moments. **Requires**: `datasetsforecast` (`pip install datasetsforecast`) ```python theme={null} import tempfile import matplotlib.pyplot as plt import numpy as np import polars as pl from datasetsforecast.m4 import M4 from synforecast import SynAugment ``` ```python theme={null} def compute_series_stats(values: np.ndarray) -> dict: """Compute statistical properties of a time series.""" return { "mean": float(np.mean(values)), "std": float(np.std(values)), "min": float(np.min(values)), "max": float(np.max(values)), "range": float(np.max(values) - np.min(values)), "cv": float(np.std(values) / np.mean(values)) if np.mean(values) != 0 else 0, "skewness": float(np.mean(((values - np.mean(values)) / np.std(values)) ** 3)) if np.std(values) > 0 else 0, "autocorr_lag1": float(np.corrcoef(values[:-1], values[1:])[0, 1]) if len(values) > 1 else 0, } # Statistics SynAugment matches to the source series by construction (via the # rescaling step in SynAugment._match_autocorrelation). A small difference here # is expected and is not evidence of augmentation quality. PINNED_STATS = {"mean", "std", "cv", "autocorr_lag1"} def print_comparison_table(original_stats: dict, synthetic_stats_list: list) -> None: """Print a comparison table of original vs synthetic statistics. The ``source`` column marks whether a statistic is pinned by construction or free to vary. Read the *free* rows to judge how much the synthetic series depart from the source in ways the rescaling does not control. """ avg_synthetic = {} for key in original_stats: avg_synthetic[key] = np.mean([s[key] for s in synthetic_stats_list]) header = ( f"{'Statistic':<15} {'Original':>12} {'Synthetic (avg)':>15} " f"{'Diff %':>10} {'Source':<8}" ) print(header) print("-" * len(header)) for key in original_stats: orig = original_stats[key] synth = avg_synthetic[key] if abs(orig) > 1e-6: diff_pct = abs(synth - orig) / abs(orig) * 100 else: diff_pct = 0 if abs(synth) < 1e-6 else 100 source = "pinned" if key in PINNED_STATS else "free" print( f"{key:<15} {orig:>12.4f} {synth:>15.4f} {diff_pct:>9.1f}% {source:<8}" ) def correlation_to_source(original: np.ndarray, synthetic: np.ndarray) -> float: """Pearson correlation between a source series and one synthetic draw. A value near 1.0 would mean the synthetic series is essentially a copy; values near 0 mean it is an independent draw that only shares the pinned summary statistics. """ n = min(len(original), len(synthetic)) o, s = original[:n], synthetic[:n] mask = ~(np.isnan(o) | np.isnan(s)) if mask.sum() < 2: return float("nan") return float(np.corrcoef(o[mask], s[mask])[0, 1]) ``` ## M4 hourly data Load hourly time series from the M4 competition and analyze their patterns before augmenting. ```python theme={null} tmpdir = tempfile.mkdtemp() df_hourly, *_ = M4.load(directory=tmpdir, group="Hourly") df_hourly = pl.from_pandas(df_hourly) sample_ids = df_hourly["unique_id"].unique().head(5).to_list() df_sample = df_hourly.filter(pl.col("unique_id").is_in(sample_ids)) print(f"Loaded {df_hourly['unique_id'].n_unique()} hourly series from M4") print(f"Using {len(sample_ids)} series for demonstration: {sample_ids}") print(f"Total rows in sample: {len(df_sample)}") print("\nSample data (first series, first 5 rows):") df_sample.filter(pl.col("unique_id") == sample_ids[0]).head(5) ``` ```text theme={null} 0%| | 0.00/555k [00:00 ### Analyzing series patterns SynAugment detects seasonality, trend, and stationarity to choose the best generator for each series. ```python theme={null} augmenter = SynAugment(seed=42) analysis = augmenter.analyze(df_sample) for series_id in sample_ids: info = analysis[series_id] print(f"\n {series_id}:") print(f" Recommended generator: {info['recommended_generator']}") props = info["properties"] print(f" Has seasonality: {props['seasonality']['has_seasonality']}") if props["seasonality"]["has_seasonality"]: print(f" Seasonality period: {props['seasonality']['period']}") print(f" Has trend: {props['trend']['has_trend']}") print(f" Is stationary: {props['stationarity']['is_stationary']}") ``` ```text theme={null} H144: Recommended generator: SeasonalGenerator Has seasonality: True Seasonality period: 24 Has trend: False Is stationary: True H146: Recommended generator: SeasonalGenerator Has seasonality: True Seasonality period: 24 Has trend: False Is stationary: True H235: Recommended generator: SeasonalGenerator Has seasonality: True Seasonality period: 24 Has trend: True Is stationary: True H234: Recommended generator: SeasonalGenerator Has seasonality: True Seasonality period: 24 Has trend: True Is stationary: True H357: Recommended generator: SeasonalGenerator Has seasonality: True Seasonality period: 24 Has trend: False Is stationary: True ``` ### Augmenting the data Generate 3 synthetic series per original series. ```python theme={null} augmented_df = augmenter.augment(df_sample, n_augment=3) print(f"Original series: {df_sample['unique_id'].n_unique()}") print(f"Total series after augmentation: {augmented_df['unique_id'].n_unique()}") ``` ```text theme={null} Original series: 5 Total series after augmentation: 20 ``` ```python theme={null} fig, axes = plt.subplots(len(sample_ids), 1, figsize=(14, 3 * len(sample_ids))) for i, uid in enumerate(sample_ids): ax = axes[i] if len(sample_ids) > 1 else axes # Plot original orig = augmented_df.filter(pl.col("unique_id") == uid) ax.plot(orig["ds"].to_list(), orig["y"].to_list(), label=f"{uid} (original)", alpha=0.9, linewidth=2) # Plot augmented for j in range(3): aug_id = f"{uid}_aug_{j}" aug = augmented_df.filter(pl.col("unique_id") == aug_id) if len(aug) > 0: ax.plot(aug["ds"].to_list(), aug["y"].to_list(), label=aug_id, alpha=0.4, linewidth=0.8) ax.set_title(f"{uid}: Original vs Augmented") ax.set_ylabel("Value") ax.legend(fontsize=7, ncol=2) axes[-1].set_xlabel("Timestamp") if len(sample_ids) > 1 else axes.set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## What augmentation pins, and what it leaves free For one series, we compare the original statistics against the average across its synthetic draws. The `mean`, `std`, `cv`, and `autocorr_lag1` rows are pinned by construction — expect them near 0% and read nothing into it. The `min`, `max`, `range`, and `skewness` rows are free to vary and show how the synthetic draws differ in shape. We also report the correlation between each synthetic draw and the source series. Low correlations are the point here: they show the draws are genuinely new series that happen to share the pinned summary statistics, not rescaled copies of the input. ```python theme={null} test_id = sample_ids[0] print(f"Detailed comparison for series: {test_id}\n") original_values = ( augmented_df.filter(pl.col("unique_id") == test_id).sort("ds")["y"].to_numpy() ) original_stats = compute_series_stats(original_values) synthetic_stats_list = [] correlations = [] for i in range(3): aug_id = f"{test_id}_aug_{i}" aug_values = ( augmented_df.filter(pl.col("unique_id") == aug_id) .sort("ds")["y"] .to_numpy() ) synthetic_stats_list.append(compute_series_stats(aug_values)) correlations.append(correlation_to_source(original_values, aug_values)) print_comparison_table(original_stats, synthetic_stats_list) print( "\nCorrelation of each synthetic draw to the source series: " + ", ".join(f"{c:.2f}" for c in correlations) ) print( "Low correlations confirm the synthetic series are independent draws that " "share the pinned statistics, not copies of the input." ) ``` ```text theme={null} Detailed comparison for series: H144 Statistic Original Synthetic (avg) Diff % Source ------------------------------------------------------------------ mean 551.3262 551.3307 0.0% pinned std 382.3467 382.3399 0.0% pinned min 12.0000 -946.5067 7987.6% free max 1673.0000 1579.6324 5.6% free range 1661.0000 2526.1390 52.1% free cv 0.6935 0.6935 0.0% pinned skewness 0.2819 -0.2291 181.3% free autocorr_lag1 0.9034 0.9334 3.3% pinned Correlation of each synthetic draw to the source series: -0.09, -0.11, 0.00 Low correlations confirm the synthetic series are independent draws that share the pinned statistics, not copies of the input. ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) # Plot original series orig = augmented_df.filter(pl.col("unique_id") == test_id) ax.plot(orig["ds"].to_list(), orig["y"].to_list(), label=f"{test_id} (original)", alpha=0.9, linewidth=2, color="black") # Plot synthetic copies for i in range(3): aug_id = f"{test_id}_aug_{i}" aug = augmented_df.filter(pl.col("unique_id") == aug_id) if len(aug) > 0: ax.plot(aug["ds"].to_list(), aug["y"].to_list(), label=aug_id, alpha=0.5, linewidth=1) ax.set_title(f"Statistical Comparison: {test_id} Original vs Synthetic") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## M4 daily data Augment daily frequency data from the M4 competition. ```python theme={null} df_daily, *_ = M4.load(directory=tmpdir, group="Daily") df_daily = pl.from_pandas(df_daily) daily_ids = df_daily["unique_id"].unique().head(4).to_list() df_daily_sample = df_daily.filter(pl.col("unique_id").is_in(daily_ids)) print(f"Loaded {df_daily['unique_id'].n_unique()} daily series from M4") print(f"Using {len(daily_ids)} series: {daily_ids}") augmenter_daily = SynAugment(seed=123) analysis_daily = augmenter_daily.analyze(df_daily_sample) print("\nAnalysis results:") for series_id in daily_ids: info = analysis_daily[series_id] print(f" {series_id}: {info['recommended_generator']}") augmented_daily = augmenter_daily.augment(df_daily_sample, n_augment=2) print( f"\nAugmented from {len(daily_ids)} to {augmented_daily['unique_id'].n_unique()} series" ) ``` ```text theme={null} 0%| | 0.00/32.5M [00:00 0: ax.plot(aug["ds"].to_list(), aug["y"].to_list(), label=aug_id, alpha=0.4, linewidth=0.8) ax.set_title(f"{uid}: {analysis_daily[uid]['recommended_generator']}") ax.set_ylabel("Value") ax.legend(fontsize=7) plt.suptitle("M4 daily data: original vs augmented", fontsize=14) plt.tight_layout() plt.show() ``` ## Cross-series statistics Because every synthetic series is pinned to its source’s mean and std, the panel-level averages of those statistics line up almost exactly. That confirms the pinning holds across the whole panel — it is not emergent fidelity. In the histograms the synthetic means and stds stack on the same locations as the originals (three synthetic draws per source), while the lag-1 autocorrelation, which is only *approximately* targeted, shows a little more spread. ```python theme={null} original_means = [] original_stds = [] original_autocorrs = [] for series_id in sample_ids: values = ( df_sample.filter(pl.col("unique_id") == series_id) .sort("ds")["y"] .to_numpy() ) original_means.append(np.mean(values)) original_stds.append(np.std(values)) if len(values) > 1: original_autocorrs.append(np.corrcoef(values[:-1], values[1:])[0, 1]) synthetic_means = [] synthetic_stds = [] synthetic_autocorrs = [] synthetic_ids = [ uid for uid in augmented_df["unique_id"].unique().to_list() if "_aug_" in str(uid) ] for series_id in synthetic_ids: values = ( augmented_df.filter(pl.col("unique_id") == series_id) .sort("ds")["y"] .to_numpy() ) synthetic_means.append(np.mean(values)) synthetic_stds.append(np.std(values)) if len(values) > 1: synthetic_autocorrs.append(np.corrcoef(values[:-1], values[1:])[0, 1]) print(f"Original series ({len(sample_ids)} series):") print( f" Mean of means: {np.mean(original_means):.4f} (std: {np.std(original_means):.4f})" ) print( f" Mean of stds: {np.mean(original_stds):.4f} (std: {np.std(original_stds):.4f})" ) print( f" Mean autocorr: {np.mean(original_autocorrs):.4f} (std: {np.std(original_autocorrs):.4f})" ) print(f"\nSynthetic series ({len(synthetic_ids)} series):") print( f" Mean of means: {np.mean(synthetic_means):.4f} (std: {np.std(synthetic_means):.4f})" ) print( f" Mean of stds: {np.mean(synthetic_stds):.4f} (std: {np.std(synthetic_stds):.4f})" ) print( f" Mean autocorr: {np.mean(synthetic_autocorrs):.4f} (std: {np.std(synthetic_autocorrs):.4f})" ) ``` ```text theme={null} Original series (5 series): Mean of means: 175.2437 (std: 210.1079) Mean of stds: 130.3940 (std: 157.5724) Mean autocorr: 0.8746 (std: 0.0858) Synthetic series (15 series): Mean of means: 175.2455 (std: 210.1099) Mean of stds: 130.3943 (std: 157.5715) Mean autocorr: 0.9418 (std: 0.0417) ``` ```python theme={null} fig, axes = plt.subplots(1, 3, figsize=(15, 4)) axes[0].hist(original_means, bins=10, alpha=0.7, label="Original", color="steelblue") axes[0].hist(synthetic_means, bins=10, alpha=0.5, label="Synthetic", color="coral") axes[0].set_title("Distribution of means") axes[0].set_xlabel("Mean") axes[0].legend() axes[1].hist(original_stds, bins=10, alpha=0.7, label="Original", color="steelblue") axes[1].hist(synthetic_stds, bins=10, alpha=0.5, label="Synthetic", color="coral") axes[1].set_title("Distribution of std devs") axes[1].set_xlabel("Std Dev") axes[1].legend() axes[2].hist(original_autocorrs, bins=10, alpha=0.7, label="Original", color="steelblue") axes[2].hist(synthetic_autocorrs, bins=10, alpha=0.5, label="Synthetic", color="coral") axes[2].set_title("Distribution of Lag-1 autocorrelation") axes[2].set_xlabel("Autocorrelation") axes[2].legend() plt.suptitle("Cross-Series statistics: original vs synthetic", fontsize=14) plt.tight_layout() plt.show() ``` ## Augmenting for ML training A common use case: expand a small dataset to create a larger training set for ML models. ```python theme={null} small_dataset = df_hourly.filter( pl.col("unique_id").is_in(df_hourly["unique_id"].unique().head(10).to_list()) ) print(f"Original training set: {small_dataset['unique_id'].n_unique()} series") print(f"Total observations: {len(small_dataset)}") ml_augmenter = SynAugment(seed=42) expanded_dataset = ml_augmenter.augment(small_dataset, n_augment=5) print(f"\nExpanded training set: {expanded_dataset['unique_id'].n_unique()} series") print(f"Total observations: {len(expanded_dataset)}") print(f"Expansion factor: {len(expanded_dataset) / len(small_dataset):.1f}x") lengths = expanded_dataset.group_by("unique_id").agg(pl.len().alias("length")) print(f"\nSeries length distribution:") print(f" Min: {lengths['length'].min()}, Max: {lengths['length'].max()}") print(f" Mean: {lengths['length'].mean():.1f}") ``` ```text theme={null} Original training set: 10 series Total observations: 9300 Expanded training set: 60 series Total observations: 55800 Expansion factor: 6.0x Series length distribution: Min: 748, Max: 1008 Mean: 930.0 ``` ## A non-duplication check Before using an augmented panel, a basic sanity check is whether its synthetic draws are near-copies of the source series. `SynAugment` pins each synthetic series to its source’s summary statistics but draws the series fresh. The check below measures non-duplication only; it is not a privacy test. To check, we z-normalize every series (dropping the pinned mean and std so we compare shape, not level) and measure how far each synthetic series is from its nearest real series. The distances between the real series themselves give the reference scale. ```python theme={null} import itertools def _znorm(a): return (a - np.mean(a)) / (np.std(a) + 1e-9) privacy_real = { u: df_sample.filter(pl.col('unique_id') == u).sort('ds')['y'].to_numpy() for u in sample_ids } _L = min(len(v) for v in privacy_real.values()) _R = {u: _znorm(v[:_L]) for u, v in privacy_real.items()} def _zdist(a, b): return float(np.sqrt(np.mean((a - b) ** 2))) real_real = np.array([_zdist(_R[a], _R[b]) for a, b in itertools.combinations(_R, 2)]) syn_ids_priv = [u for u in augmented_df['unique_id'].unique().to_list() if '_aug_' in u] syn_nn = np.array([ min(_zdist(_znorm(augmented_df.filter(pl.col('unique_id') == sid) .sort('ds')['y'].to_numpy()[:_L]), _R[u]) for u in _R) for sid in syn_ids_priv ]) print(f'closest pair of REAL series: z-distance {real_real.min():.2f}') print(f'every synthetic to its NEAREST real: z-distance >= {syn_nn.min():.2f}') print(f'synthetic closer to a real than the closest real-real pair: ' f'{int((syn_nn < real_real.min()).sum())} / {len(syn_nn)}') fig, ax = plt.subplots(figsize=(9, 4)) bins = np.linspace(0, max(real_real.max(), syn_nn.max()) * 1.05, 20) ax.hist(real_real, bins=bins, alpha=0.7, color='steelblue', label='real vs real (reference)') ax.hist(syn_nn, bins=bins, alpha=0.6, color='mediumpurple', label='synthetic vs nearest real') ax.axvline(0.0, color='crimson', ls='--', label='exact copy (distance 0)') ax.set(xlabel='z-normalized RMSE between series', ylabel='count', title='Synthetic-to-real distances in this sample') ax.legend() plt.tight_layout() plt.show() ``` ```text theme={null} closest pair of REAL series: z-distance 0.12 every synthetic to its NEAREST real: z-distance >= 1.06 synthetic closer to a real than the closest real-real pair: 0 / 15 ``` In this sample, every synthetic series sits farther from its nearest real series than the closest pair of real series sit from each other, and none lands near the zero-distance mark an exact copy would. This rules out exact and near duplicates under this one distance metric; it does not test resistance to reconstruction or inference attacks. > **What this is, and is not** > > This is only an empirical non-duplication check under z-normalized > RMSE. It does not establish resistance to reconstruction, membership > inference, attribute inference, or other privacy attacks. `SynAugment` > provides no differential-privacy guarantee or epsilon bound. For > sensitive or regulated data, use a dedicated disclosure-risk > assessment and appropriate privacy controls. ## Summary * `SynAugment` guarantees the mean, standard deviation, and lag-1 autocorrelation of each synthetic series by rescaling it to its source. Treat those matches as a property of the method, not as a validation result. * The synthetic series are new draws rather than copies: their trajectories decorrelate from the source, and their distribution shape (min, max, skewness) varies freely around the pinned moments. * Use augmentation to expand a small panel with series that share the summary statistics of the originals. Fit augmentation parameters on the training split only, and choose the augmentation ratio on validation data — never on the final holdout. * Before sharing, run non-duplication and domain-specific disclosure-risk checks. This example’s distance test is a sanity check, not evidence of privacy. # Balanced pool Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/balanced_pool.html `balanced_pool()` returns 42 pre-configured generator instances, built from 15 of SynForecast’s 31 generator classes with one class per behavioral niche. The niches span ARMA and exponential smoothing, long-range memory, volatility clustering, intermittent demand, deterministic chaos, counts, and bounded and heavy-tailed processes. It is the default corpus behind [`generate_series`](../getting-started/quickstart), and a bias-free starting point for benchmarking or pretraining: generators are allocated proportionally to each niche’s behavioral range, so no single domain dominates the pool. The list is ordered round-robin across the niches, so any prefix spans as many distinct behaviors as possible: a `generate_series` panel smaller than the pool still gets one niche per series. > **What’s in, and what’s deliberately out** > > The pool draws from *interpretable, single-mechanism* generators — > each slot is one named data-generating process. The meta-generators > (`TSIGenerator`, `TCMGenerator`, `KernelSynthGenerator`) are excluded > on purpose: they already randomize across many behaviors internally, > so folding them in would blur the one-niche-one-mechanism design. Use > the [`pretraining_pool()`](#pretraining-pool) preset for maximal > breadth in foundation-model pretraining; it bundles those > meta-generators, with `balanced_pool` included by default. ```python theme={null} import matplotlib.pyplot as plt import polars as pl from synforecast import SynSet, balanced_pool, pretraining_pool ``` ## Generate a balanced dataset Create 42 generators and generate one series per generator. ```python theme={null} generators = balanced_pool( min_length=200, max_length=200, freq="D", seed=42, engine="polars" ) dataset = SynSet(generators) df = dataset.generate(n_series_per_generator=1) print(f"Generators: {len(generators)}") print(f"Series: {df['unique_id'].n_unique()}") print(f"Total observations: {len(df)}") ``` ```text theme={null} Generators: 42 Series: 42 Total observations: 8400 ``` ## Generator names Each generator has a descriptive name indicating its type and configuration. ```python theme={null} for i, gen in enumerate(generators): print(f" {i}: {gen.alias}") ``` ```text theme={null} 0: SARIMAGenerator 1: ETSGenerator 2: FractionalBrownianMotionGenerator 3: RegimeSwitchingGenerator 4: GARCHGenerator 5: CyclicGenerator 6: IntermittentDemandGenerator 7: EnergyLoadGenerator 8: IoTSensorGenerator 9: VitalSignsGenerator 10: GaussianProcessGenerator 11: ChaoticSystemGenerator 12: INARGenerator 13: BoundedProcessGenerator 14: LevyProcessGenerator 15: SARIMAGenerator 16: ETSGenerator 17: FractionalBrownianMotionGenerator 18: RegimeSwitchingGenerator 19: GARCHGenerator 20: CyclicGenerator 21: IntermittentDemandGenerator 22: EnergyLoadGenerator 23: IoTSensorGenerator 24: VitalSignsGenerator 25: GaussianProcessGenerator 26: ChaoticSystemGenerator 27: INARGenerator 28: BoundedProcessGenerator 29: LevyProcessGenerator 30: SARIMAGenerator 31: ETSGenerator 32: FractionalBrownianMotionGenerator 33: IntermittentDemandGenerator 34: IoTSensorGenerator 35: VitalSignsGenerator 36: GaussianProcessGenerator 37: ChaoticSystemGenerator 38: SARIMAGenerator 39: ETSGenerator 40: GaussianProcessGenerator 41: SARIMAGenerator ``` ## Overview: all 42 series A compact grid showing every series in the balanced pool. ```python theme={null} fig, axes = plt.subplots(7, 6, figsize=(18, 16)) axes = axes.flatten() for i, gen in enumerate(generators): uid = str(i) series = df.filter(pl.col("unique_id") == uid) values = series["y"].to_list() axes[i].plot(values, linewidth=0.8) axes[i].set_title(gen.alias, fontsize=7) axes[i].tick_params(labelsize=5) fig.suptitle("Balanced pool: 42 generators across 15 behavioral niches", fontsize=14) plt.tight_layout() plt.show() ``` ## Niche deep-dives Each behavioral niche contributes a different number of generators. Below we group them by niche and plot the variants side by side. ```python theme={null} niche_labels = [ ("ARMA + Seasonality (SARIMA)", "SARIMAGenerator"), ("Exponential Smoothing (ETS)", "ETSGenerator"), ("Long-Range Memory (FBM)", "FractionalBrownianMotionGenerator"), ("Structural Breaks (Regime Switching)", "RegimeSwitchingGenerator"), ("Volatility Clustering (GARCH)", "GARCHGenerator"), ("Irregular Cycles (Cyclic)", "CyclicGenerator"), ("Sparse/Intermittent (Intermittent Demand)", "IntermittentDemandGenerator"), ("Multi-Seasonal (Energy Load)", "EnergyLoadGenerator"), ("Sensor Artifacts (IoT Sensor)", "IoTSensorGenerator"), ("Physiological (Vital Signs)", "VitalSignsGenerator"), ("Smooth/Rough Functions (Gaussian Process)", "GaussianProcessGenerator"), ("Deterministic Chaos (Chaotic System)", "ChaoticSystemGenerator"), ("Count Time Series (INAR)", "INARGenerator"), ("Bounded/Proportion Data (Bounded Process)", "BoundedProcessGenerator"), ("Heavy-Tailed Processes (Levy Process)", "LevyProcessGenerator"), ] # The pool is interleaved across niches, so look indices up by class # rather than assuming contiguous blocks. niches = [ (label, [i for i, g in enumerate(generators) if type(g).__name__ == cls]) for label, cls in niche_labels ] ``` ```python theme={null} for niche_name, indices in niches: n = len(indices) fig, axes = plt.subplots(1, n, figsize=(4 * n, 3), squeeze=False) fig.suptitle(niche_name, fontsize=12, fontweight="bold") for j, idx in enumerate(indices): uid = str(idx) series = df.filter(pl.col("unique_id") == uid) axes[0][j].plot(series["y"].to_list(), linewidth=0.9) axes[0][j].set_title(generators[idx].alias, fontsize=8) axes[0][j].tick_params(labelsize=7) plt.tight_layout() plt.show() ``` ## Summary statistics Compare key statistics across all 42 series to see how the balanced pool spans different value ranges and variabilities. ```python theme={null} stats = ( df.group_by("unique_id") .agg( [ pl.col("y").count().alias("count"), pl.col("y").min().alias("min"), pl.col("y").max().alias("max"), pl.col("y").mean().alias("mean"), pl.col("y").std().alias("std"), ] ) .sort("unique_id") ) stats ``` | unique\_id | count | min | max | mean | std | | ---------- | ----- | --------- | ---------- | ---------- | --------- | | cat | u32 | f64 | f64 | f64 | f64 | | "0" | 200 | -2.652759 | 3.609343 | -0.068571 | 0.99059 | | "1" | 200 | 96.138073 | 104.540914 | 99.457374 | 1.680738 | | "10" | 200 | -1.549691 | 1.22711 | -0.083459 | 0.761829 | | "11" | 200 | -15.4612 | 16.7784 | 0.983599 | 7.871835 | | "12" | 200 | 1.0 | 16.0 | 7.64 | 2.544163 | | … | … | … | … | … | … | | "5" | 200 | 80.533159 | 119.965854 | 101.125541 | 12.976649 | | "6" | 200 | 0.0 | 9.0 | 0.915 | 2.168244 | | "7" | 200 | 88.836529 | 178.20318 | 129.009721 | 25.104989 | | "8" | 200 | 19.799144 | 20.373597 | 20.096759 | 0.105014 | | "9" | 200 | 55.425593 | 77.339503 | 64.864145 | 4.815145 | ## Scaling up Generate multiple series per generator for a larger dataset. ```python theme={null} df_large = dataset.generate(n_series_per_generator=5) print(f"Series: {df_large['unique_id'].n_unique()}") print(f"Total observations: {len(df_large)}") ``` ```text theme={null} Series: 210 Total observations: 42000 ``` Plot five series from a single generator to see intra-generator variation. ```python theme={null} fig, ax = plt.subplots(figsize=(10, 4)) # SynSet assigns ids per generator in pool order, so series 0-4 come # from generators[0]: the SARIMAGenerator configured as a stationary AR(1) for sid in range(5): uid = str(sid) series = df_large.filter(pl.col("unique_id") == uid) ax.plot(series["y"].to_list(), alpha=0.7, label=uid) ax.set_title(f"Intra-Generator Variation: {generators[0].alias}") ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## Pretraining pool `pretraining_pool()` is the breadth-maximizing counterpart to `balanced_pool()`. It keeps the 42 single-mechanism instances above and adds independently-seeded copies of the three meta-generators `balanced_pool` deliberately leaves out — `TSIGenerator`, `TCMGenerator`, and `KernelSynthGenerator`. Each of those resamples a fresh trend/seasonal, causal-graph, or GP-kernel configuration per series, so a handful of instances already spans a very wide distribution — the goal when pretraining a foundation model rather than benchmarking one named process. The default length range is wider too (256-1024 steps), matching typical pretraining contexts. Two knobs control the mix: `n_meta_variants` sets how many independently-seeded copies of each meta-generator to add (default 3), and `include_balanced=False` drops the single-mechanism generators for a purely procedural corpus. ```python theme={null} pretrain = pretraining_pool( min_length=256, max_length=256, freq="D", seed=42, engine="polars" ) meta_only = pretraining_pool(include_balanced=False, engine="polars") from collections import Counter print(f"pretraining_pool: {len(pretrain)} generators") print(f" {len(pretrain) - len(meta_only)} single-mechanism (from balanced_pool)") print(f" {len(meta_only)} meta-generator instances") print(" meta-generators:", dict(Counter(type(g).__name__ for g in meta_only))) ``` ```text theme={null} pretraining_pool: 51 generators 42 single-mechanism (from balanced_pool) 9 meta-generator instances meta-generators: {'TSIGenerator': 3, 'TCMGenerator': 3, 'KernelSynthGenerator': 3} ``` ```python theme={null} # Each meta-generator instance draws its own structure, so even nine series # span trends, seasonality, causal dynamics, and kernel-sampled shapes. meta_df = SynSet(meta_only).generate(n_series_per_generator=1) fig, axes = plt.subplots(3, 3, figsize=(15, 7)) for ax, (uid, gen) in zip(axes.ravel(), enumerate(meta_only)): series = meta_df.filter(pl.col("unique_id") == str(uid)) ax.plot(series["y"].to_list(), linewidth=0.8) ax.set_title(type(gen).__name__, fontsize=9) ax.tick_params(labelsize=7) fig.suptitle("Meta-generators: each instance resamples its own structure") plt.tight_layout() plt.show() ``` Whether pretraining on a synthetic corpus like this actually helps depends on the target data and the amount of real history available. See [when synthetic data helps](when_synthetic_helps) for a paired, multi-seed benchmark that reports where it wins, where it is neutral, and where the edge reverses. # Changepoint injection Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/changepoints.html A changepoint is a *structural break*: the series switches to a new level, trend, or variance and stays there. Unlike an anomaly, which is a transient outlier, a changepoint is a genuine regime change — exactly what breaks models that assume the past looks like the future. Injecting them lets you test whether a forecaster adapts, and gives changepoint detectors a labelled benchmark. > **Types and placement** > > `changepoint_type` selects what breaks — `"level"`, `"trend"`, > `"variance"`, or `"mixed"`. Set `changepoint_locations` (fractions of > the series) and the matching `changepoint_*_changes` to place breaks > exactly, or give only `num_changepoints` to scatter them randomly. > `exogenous=ExogenousConfig(changepoint_flags=True)` adds a > `changepoint_flag` column marking each break — drawn as dashed > vertical lines below. ```python theme={null} import matplotlib.pyplot as plt import polars as pl from synforecast.exogenous import ExogenousConfig from synforecast.generators import RandomWalkGenerator, SeasonalGenerator, VARGenerator FLAGS = ExogenousConfig(changepoint_flags=True) def plot_changepoints(df, title): """Plot each series and mark changepoints (changepoint_flag == 1) as vlines.""" fig, ax = plt.subplots(figsize=(11, 4)) for uid in df["unique_id"].unique(maintain_order=True).to_list(): s = df.filter(pl.col("unique_id") == uid).sort("ds") ax.plot(s["ds"], s["y"], linewidth=1, alpha=0.85, label=str(uid)) for ts in s.filter(pl.col("changepoint_flag") == 1)["ds"].to_list(): ax.axvline(ts, color="crimson", linestyle="--", linewidth=1, alpha=0.7) ax.set(title=title, xlabel="ds", ylabel="y") if df["unique_id"].n_unique() > 1: ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## Level breaks The most common structural break: the series jumps to a new baseline. Here three breaks at 20%, 50%, and 80% of the series, with explicit jump sizes. ```python theme={null} level_df = RandomWalkGenerator( engine="polars", min_length=300, max_length=300, freq="D", drift=0.1, volatility=2.0, changepoints=True, num_changepoints=3, changepoint_type="level", changepoint_level_changes=[50.0, -30.0, 40.0], changepoint_locations=[0.2, 0.5, 0.8], exogenous=FLAGS, seed=42, ).generate(n_series=1) plot_changepoints(level_df, "Level breaks at 20%, 50%, 80%") ``` At each dashed line the series steps to a new level and continues from there — the jump sizes are exactly the `changepoint_level_changes` you passed. ## Trend breaks A trend break changes the *slope* rather than the level, so the series bends at each changepoint. This example adds them on top of a weekly-seasonal series. ```python theme={null} trend_df = SeasonalGenerator( engine="polars", min_length=300, max_length=300, freq="D", seasonality_period=7, seasonality_amplitude=10.0, base_level=100.0, changepoints=True, num_changepoints=2, changepoint_type="trend", changepoint_trend_changes=[0.3, -0.2], changepoint_locations=[0.3, 0.7], exogenous=FLAGS, seed=42, ).generate(n_series=1) plot_changepoints(trend_df, "Trend breaks on a seasonal series") ``` ## Variance breaks A variance break changes the *noise amplitude* — the level and trend are unchanged, but the series becomes calmer or more volatile. These matter for prediction intervals, which should widen after a jump in variance. ```python theme={null} variance_df = RandomWalkGenerator( engine="polars", min_length=300, max_length=300, freq="D", drift=0.05, volatility=2.0, changepoints=True, num_changepoints=2, changepoint_type="variance", changepoint_variance_changes=[2.0, 0.5], changepoint_locations=[0.33, 0.67], exogenous=FLAGS, seed=42, ).generate(n_series=1) plot_changepoints(variance_df, "Variance breaks (2x then 0.5x)") ``` ## Mixed types and automatic placement Use `changepoint_type="mixed"` to draw a different kind of break at each changepoint, and omit `changepoint_locations` to scatter them at random positions — closer to how breaks arrive in real data. ```python theme={null} mixed_df = RandomWalkGenerator( engine="polars", min_length=400, max_length=400, freq="D", drift=0.1, volatility=2.0, changepoints=True, num_changepoints=4, changepoint_type="mixed", exogenous=FLAGS, seed=42, ).generate(n_series=1) plot_changepoints(mixed_df, "Four mixed breaks at random locations") ``` ## Multivariate and composed Like all pipeline features, changepoints apply to multivariate generators, and compose with anomalies and missing data for a realistically hard series. ```python theme={null} var_df = VARGenerator( engine="polars", min_length=200, max_length=200, freq="D", lag_order=1, changepoints=True, num_changepoints=2, changepoint_type="level", changepoint_level_changes=[30.0, -20.0], changepoint_locations=[0.3, 0.7], exogenous=FLAGS, seed=42, ).generate(n_series=3) plot_changepoints(var_df, "Level breaks shared across correlated VAR series") ``` ```python theme={null} composed_df = RandomWalkGenerator( engine="polars", min_length=400, max_length=400, freq="D", drift=0.1, volatility=2.0, changepoints=True, num_changepoints=3, changepoint_type="mixed", changepoint_locations=[0.25, 0.5, 0.75], anomalies=True, anomaly_fraction=0.04, anomaly_types=["spike", "dip"], spike_magnitude=40.0, dip_magnitude=-40.0, missing_data=True, missing_pattern="random", missing_rate=0.05, exogenous=FLAGS, seed=42, ).generate(n_series=1) print(f"missing values: {composed_df['y'].null_count()} of {len(composed_df)}") plot_changepoints(composed_df, "Changepoints (dashed) + anomalies + missing data") ``` ```text theme={null} missing values: 0 of 400 ``` > **Related capabilities** > > * [Anomalies](anomalies) — transient outliers, as opposed to the > sustained regime changes here. > * [Missingness](missingness) — random, block, and seasonal gap > patterns. > * All changepoint parameters are documented in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Write your own generator Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/custom_generator.html The built-in generators cover a wide span of processes, but you will sometimes need a data-generating process they do not produce. `BaseGenerator` exists for that: subclass it, implement one method, and your generator inherits timestamp handling, reproducible seeding, parallel generation, pattern injection (changepoints, anomalies, missingness), exogenous variables, and every dataframe backend. This guide builds a small logistic-growth (“S-curve”) generator, a shape none of the built-ins produce directly. > **The contract** > > A generator needs three things: > > 1. Subclass `BaseGenerator`. > 2. Declare its parameters as Pydantic `Field` attributes. > 3. Implement > `generate_single_series(self, length: int) -> np.ndarray`, > returning one series of the requested length. Draw randomness from > `self.rng` so seeding and parallel generation stay deterministic. > > Everything else is inherited. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import polars as pl from pydantic import Field from synforecast.base import BaseGenerator class LogisticGrowthGenerator(BaseGenerator): """S-curve (logistic) growth with additive observation noise. y_t = capacity / (1 + exp(-growth_rate * (t - t_mid))) + noise, with the inflection point t_mid placed at ``midpoint_fraction`` of the series length. A reasonable model for adoption curves and saturating demand. """ capacity: float = Field( default=1000.0, gt=0, description="Saturation level the curve approaches" ) growth_rate: float = Field( default=0.1, gt=0, description="Steepness of the transition" ) midpoint_fraction: float = Field( default=0.5, ge=0.0, le=1.0, description="Inflection point as a fraction of the series length", ) noise_std: float = Field( default=10.0, ge=0, description="Standard deviation of additive noise" ) def generate_single_series(self, length: int) -> np.ndarray: t = np.arange(length) t_mid = self.midpoint_fraction * length curve = self.capacity / (1.0 + np.exp(-self.growth_rate * (t - t_mid))) noise = self.rng.normal(0.0, self.noise_std, size=length) return curve + noise ``` ## What you implement, and what you get `generate_single_series` is the only required method. It receives a target `length` — which varies per series between `min_length` and `max_length` — and returns a plain NumPy array. The base class attaches timestamps derived from `freq`, assigns the `unique_id`/`ds`/`y` columns, applies any requested pattern injection, and materializes the result in the chosen dataframe backend. The generator is a Pydantic model, so the shared parameters (`min_length`, `freq`, `seed`, `engine`, and the injection controls) are available without redeclaring them. ## Generate a panel ```python theme={null} generator = LogisticGrowthGenerator( min_length=100, max_length=160, freq="D", capacity=1000.0, growth_rate=0.08, noise_std=15.0, seed=0, ) df = generator.generate(n_series=4) df.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | ---------- | | 0 | 0 | 2000-01-01 | 23.631764 | | 1 | 0 | 2000-01-02 | -0.870651 | | 2 | 0 | 2000-01-03 | 10.503541 | | 3 | 0 | 2000-01-04 | -0.730577 | | 4 | 0 | 2000-01-05 | -21.566003 | ```python theme={null} fig, ax = plt.subplots(figsize=(10, 4)) for uid, series in df.groupby("unique_id", observed=True): ax.plot(series["ds"], series["y"], alpha=0.8, label=str(uid)) ax.set(title="Custom logistic-growth generator", xlabel="ds", ylabel="y") ax.legend(fontsize=8, title="unique_id") plt.tight_layout() plt.show() ``` ## Pattern injection is inherited Because changepoint, anomaly, and missingness injection live in `BaseGenerator`, they work on the custom generator with no extra code: enabling them is a matter of setting the same parameters the built-in generators use. Below the S-curve is generated with injected spikes and dips and a level changepoint, and the ground-truth `anomaly_flag` column is requested through `ExogenousConfig` so the injected points can be marked. ```python theme={null} from synforecast.exogenous import ExogenousConfig messy = LogisticGrowthGenerator( min_length=160, max_length=160, freq="D", capacity=1000.0, growth_rate=0.08, noise_std=10.0, anomalies=True, anomaly_fraction=0.04, anomaly_types=["spike", "dip"], spike_magnitude=150.0, dip_magnitude=-150.0, changepoints=True, num_changepoints=1, changepoint_type="level", exogenous=ExogenousConfig(anomaly_flags=True), engine="polars", seed=1, ) messy_df = messy.generate(n_series=1) messy_df.columns ``` ```text theme={null} ['unique_id', 'ds', 'y', 'anomaly_flag'] ``` ```python theme={null} series = messy_df.sort("ds") hits = series.filter(pl.col("anomaly_flag") == 1) fig, ax = plt.subplots(figsize=(10, 4)) ax.plot(series["ds"], series["y"], linewidth=1, alpha=0.85) ax.scatter(hits["ds"], hits["y"], color="crimson", s=25, zorder=3, label="injected anomaly") ax.set(title="Pattern injection on the custom generator", xlabel="ds", ylabel="y") ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` > **Draw from `self.rng`, not `np.random`** > > `self.rng` is a seeded NumPy `Generator`. Using it is what lets a > fixed `seed` reproduce identical output and keeps parallel generation > deterministic. Each series is generated with its own derived RNG > state, so series differ from one another while the whole panel stays > reproducible. ## Contributing a generator back To ship a generator as part of SynForecast rather than defining it inline, follow the same layout as the built-ins: 1. Add the module under `synforecast/generators/`. 2. Export the class from `synforecast/generators/__init__.py`. 3. Add tests under `tests/`, and a page under `nbs/docs/generators//`. See [CONTRIBUTING](https://github.com/Nixtla/synforecast/blob/main/CONTRIBUTING.md) and the [generator reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md) for the conventions and verification expectations. ## Related * [Changepoints](changepoints), [anomalies](anomalies), and [missingness](missingness) — the injection this generator inherits. * [Exogenous variables](exogenous) — datetime features, correlated regressors, and the ground-truth flags used above. * [Compose a dataset](dataset) — combine your generator with others in a `SynSet`. # Compose datasets with SynSet Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/dataset.html `SynSet` combines several generators into one long-format panel — the way to build a dataset that *mixes* behaviors (say, trending random walks alongside daily-seasonal demand) instead of repeating one shape. Each generator contributes a batch of series, and their ids are numbered sequentially so the result stays a single, model-ready frame. > **SynSet vs. balanced\_pool vs. generate\_series** > > `SynSet` is the low-level composer you control directly. > [`balanced_pool`](balanced_pool) is a ready-made list of 42 generators > you can drop into a `SynSet`, and > [`generate_series`](../getting-started/quickstart) wraps that pool in > a one-liner. Use `SynSet` when you want an explicit, curated mix of > generators. ```python theme={null} import matplotlib.pyplot as plt import polars as pl from synforecast import SynSet from synforecast.generators import RandomWalkGenerator, SeasonalGenerator ``` ## Define generators Create a random walk generator and a seasonal generator with different parameter configurations. ```python theme={null} rw_params = { "min_length": 100, "max_length": 150, "freq": "h", "drift": 0.1, "volatility": 1.5, "start_value": 100.0, "seed": 42, } seasonal_params = { "min_length": 100, "max_length": 150, "freq": "h", "seasonality_period": 24, "seasonality_amplitude": 15.0, "trend": 0.05, "noise_level": 2.0, "base_level": 50.0, "seed": 123, } rw_gen = RandomWalkGenerator(engine="polars", **rw_params) seasonal_gen = SeasonalGenerator(engine="polars", **seasonal_params) ``` ## Generate the dataset `generate(n_series_per_generator=3)` draws 3 series from each generator — 6 in total. Ids are assigned in generator order: `0-2` are the random walks, `3-5` the seasonal series. Pass `start_id` to offset the numbering when stitching several datasets together. ```python theme={null} dataset = SynSet([rw_gen, seasonal_gen]) df = dataset.generate(n_series_per_generator=3) print(f"Generated {df['unique_id'].n_unique()} time series") print(f"Total observations: {len(df)}") df.head(10) ``` ```text theme={null} Generated 6 time series Total observations: 740 ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 100.620958 | | "0" | 2000-01-01 01:00:00 | 102.500707 | | "0" | 2000-01-01 02:00:00 | 101.104897 | | "0" | 2000-01-01 03:00:00 | 100.564106 | | "0" | 2000-01-01 04:00:00 | 99.076724 | | "0" | 2000-01-01 05:00:00 | 97.674284 | | "0" | 2000-01-01 06:00:00 | 96.714608 | | "0" | 2000-01-01 07:00:00 | 97.294935 | | "0" | 2000-01-01 08:00:00 | 98.929896 | | "0" | 2000-01-01 09:00:00 | 97.918173 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 5)) unique_ids = df["unique_id"].unique().to_list() for uid in unique_ids: series = df.filter(pl.col("unique_id") == uid) # IDs 0, 1, 2 are RandomWalk; IDs 3, 4, 5 are Seasonal. gen_type = "RandomWalk" if int(uid) < 3 else "Seasonal" ax.plot(series["ds"].to_list(), series["y"].to_list(), label=f"{uid} ({gen_type})", alpha=0.8) ax.set_title("SynSet dataset: random-walk and seasonal generators") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## Statistics by series Compare summary statistics across all generated series. ```python theme={null} stats = ( df.group_by("unique_id") .agg( [ pl.col("y").count().alias("count"), pl.col("y").min().alias("min_value"), pl.col("y").max().alias("max_value"), pl.col("y").mean().alias("mean_value"), pl.col("y").std().alias("std_value"), ] ) .sort("unique_id") ) stats ``` | unique\_id | count | min\_value | max\_value | mean\_value | std\_value | | ---------- | ----- | ---------- | ---------- | ----------- | ---------- | | cat | u32 | f64 | f64 | f64 | f64 | | "0" | 104 | 94.028521 | 110.129544 | 100.735208 | 3.266454 | | "1" | 139 | 100.090121 | 151.940789 | 118.881905 | 13.27846 | | "2" | 133 | 89.176748 | 109.298167 | 100.537712 | 5.13869 | | "3" | 100 | 35.600282 | 70.029241 | 53.290106 | 10.264408 | | "4" | 134 | 34.431384 | 74.281368 | 54.466225 | 10.722442 | | "5" | 130 | 32.718865 | 74.773261 | 54.063785 | 11.005692 | ## Sample data Compare the first few rows from a random walk series and a seasonal series. ```python theme={null} print("Sample of series 0 (Random Walk - first 10 rows):") df.filter(pl.col("unique_id") == "0").head(10) ``` ```text theme={null} Sample of series 0 (Random Walk - first 10 rows): ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 100.620958 | | "0" | 2000-01-01 01:00:00 | 102.500707 | | "0" | 2000-01-01 02:00:00 | 101.104897 | | "0" | 2000-01-01 03:00:00 | 100.564106 | | "0" | 2000-01-01 04:00:00 | 99.076724 | | "0" | 2000-01-01 05:00:00 | 97.674284 | | "0" | 2000-01-01 06:00:00 | 96.714608 | | "0" | 2000-01-01 07:00:00 | 97.294935 | | "0" | 2000-01-01 08:00:00 | 98.929896 | | "0" | 2000-01-01 09:00:00 | 97.918173 | ```python theme={null} print("Sample of series 3 (Seasonal - first 10 rows):") df.filter(pl.col("unique_id") == "3").head(10) ``` ```text theme={null} Sample of series 3 (Seasonal - first 10 rows): ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "3" | 2000-01-01 00:00:00 | 48.634355 | | "3" | 2000-01-01 01:00:00 | 51.658968 | | "3" | 2000-01-01 02:00:00 | 56.909722 | | "3" | 2000-01-01 03:00:00 | 59.2974 | | "3" | 2000-01-01 04:00:00 | 62.751023 | | "3" | 2000-01-01 05:00:00 | 65.499531 | | "3" | 2000-01-01 06:00:00 | 67.897628 | | "3" | 2000-01-01 07:00:00 | 63.845217 | | "3" | 2000-01-01 08:00:00 | 65.0017 | | "3" | 2000-01-01 09:00:00 | 62.627031 | # Exogenous variables Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/exogenous.html Forecasts often improve when the model sees more than the target’s own past — calendar effects, known interventions, related drivers. Every SynForecast generator can attach these as extra columns via the `exogenous` parameter, so you can build test panels that exercise a model’s covariate handling end to end, all reproducible from the seed. > **Three kinds of covariate** > > 1. **Datetime features** — calendar and cyclical (sin/cos) time > encodings, added frequency-aware. > 2. **Pattern-injection flags** — binary `anomaly_flag` / > `changepoint_flag` / `missing_flag` columns marking exactly where > each injected pattern landed (ground-truth labels for detectors). > 3. **Correlated exogenous** — numeric columns statistically tied to > the target series. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import polars as pl from synforecast.exogenous import CorrelatedExogConfig, ExogenousConfig from synforecast.generators import RandomWalkGenerator ``` ## Datetime features Enable `datetime_features` for calendar columns (year, month, day\_of\_week, hour, etc.) and `datetime_cyclical` for sin/cos encodings. Features are frequency-aware: hourly data includes `hour`, `hour_sin/cos`; daily data omits them since they would be constant. ```python theme={null} gen = RandomWalkGenerator(engine="polars", **{ "min_length": 168, # 7 days of hourly data "max_length": 168, "freq": "h", "seed": 42, "exogenous": ExogenousConfig( datetime_features=True, datetime_cyclical=True, ), }) df = gen.generate(n_series=1) print(f"Columns: {df.columns}") df.head(10) ``` ```text theme={null} Columns: ['unique_id', 'ds', 'y', 'year', 'quarter', 'month', 'day_of_year', 'day_of_week', 'day_of_month', 'is_weekend', 'hour', 'hour_sin', 'hour_cos', 'dow_sin', 'dow_cos', 'month_sin', 'month_cos', 'doy_sin', 'doy_cos'] ``` | unique\_id | ds | y | year | quarter | month | day\_of\_year | day\_of\_week | day\_of\_month | is\_weekend | hour | hour\_sin | hour\_cos | dow\_sin | dow\_cos | month\_sin | month\_cos | doy\_sin | doy\_cos | | ---------- | ------------------- | --------- | ---- | ------- | ----- | ------------- | ------------- | -------------- | ----------- | ---- | --------- | ---------- | --------- | --------- | ---------- | ---------- | -------- | -------- | | cat | datetime\[ns] | f64 | i32 | i8 | i8 | i16 | i8 | i8 | i8 | i8 | f32 | f32 | f32 | f32 | f32 | f32 | f32 | f32 | | "0" | 2000-01-01 00:00:00 | -1.401594 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 0 | 0.0 | 1.0 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | | "0" | 2000-01-01 01:00:00 | -2.307539 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 1 | 0.258819 | 0.965926 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | | "0" | 2000-01-01 02:00:00 | -1.352484 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 2 | 0.5 | 0.866025 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | | "0" | 2000-01-01 03:00:00 | -1.011828 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 3 | 0.707107 | 0.707107 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | | "0" | 2000-01-01 04:00:00 | -1.511372 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 4 | 0.866025 | 0.5 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | | "0" | 2000-01-01 05:00:00 | -2.176202 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 5 | 0.965926 | 0.258819 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | | "0" | 2000-01-01 06:00:00 | -1.574639 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 6 | 1.0 | 6.1232e-17 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | | "0" | 2000-01-01 07:00:00 | -1.915517 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 7 | 0.965926 | -0.258819 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | | "0" | 2000-01-01 08:00:00 | -1.746987 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 8 | 0.866025 | -0.5 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | | "0" | 2000-01-01 09:00:00 | -1.566314 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 9 | 0.707107 | -0.707107 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | ```python theme={null} ts = df["ds"].to_list() y = df["y"].to_numpy() is_weekend = df["is_weekend"].to_numpy().astype(bool) fig, axes = plt.subplots(3, 1, figsize=(12, 7), sharex=True) # Panel 1: time series with weekend shading axes[0].plot(ts, y, linewidth=0.8, color="steelblue", label="y") axes[0].fill_between(ts, y.min(), y.max(), where=is_weekend, alpha=0.15, color="salmon", label="Weekend") axes[0].set_ylabel("y") axes[0].set_title("Time series with weekend shading") axes[0].legend(loc="upper right") # Panel 2: hour-of-day cyclical encoding axes[1].plot(ts, df["hour_sin"].to_numpy(), label="hour_sin", color="darkorange") axes[1].plot(ts, df["hour_cos"].to_numpy(), label="hour_cos", color="purple") axes[1].set_ylabel("Encoding") axes[1].set_title("Cyclical hour-of-day encoding") axes[1].legend(loc="upper right") axes[1].set_ylim(-1.15, 1.15) # Panel 3: day-of-week cyclical encoding axes[2].plot(ts, df["dow_sin"].to_numpy(), label="dow_sin", color="teal") axes[2].plot(ts, df["dow_cos"].to_numpy(), label="dow_cos", color="crimson") axes[2].set_ylabel("Encoding") axes[2].set_title("Cyclical day-of-week encoding") axes[2].legend(loc="upper right") axes[2].set_ylim(-1.15, 1.15) plt.tight_layout() plt.show() ``` ## Pattern injection flags When pattern injection is enabled (anomalies, changepoints, missing data), you can get binary flag columns indicating exactly where each pattern was injected. This is useful for training anomaly detectors or evaluating changepoint detection algorithms. ```python theme={null} gen = RandomWalkGenerator(engine="polars", **{ "min_length": 300, "max_length": 300, "freq": "D", "seed": 42, "drift": 0.1, "volatility": 1.5, "anomalies": True, "anomaly_fraction": 0.05, "anomaly_types": ["spike", "dip"], "spike_magnitude": 15.0, "dip_magnitude": -15.0, "changepoints": True, "num_changepoints": 3, "changepoint_type": "level", "missing_data": True, "missing_rate": 0.08, "missing_pattern": "block", "missing_block_size": 5, "exogenous": ExogenousConfig( anomaly_flags=True, changepoint_flags=True, missing_flags=True, ), }) df = gen.generate(n_series=1) print(f"Columns: {df.columns}") print(f"Anomalies: {df['anomaly_flag'].sum()}, " f"Changepoints: {df['changepoint_flag'].sum()}, " f"Missing: {df['missing_flag'].sum()}") df.head(10) ``` ```text theme={null} Columns: ['unique_id', 'ds', 'y', 'anomaly_flag', 'changepoint_flag', 'missing_flag'] Anomalies: 15, Changepoints: 3, Missing: 20 ``` | unique\_id | ds | y | anomaly\_flag | changepoint\_flag | missing\_flag | | ---------- | ------------------- | --------- | ------------- | ----------------- | ------------- | | cat | datetime\[ns] | f64 | i8 | i8 | i8 | | "0" | 2000-01-01 00:00:00 | -2.002391 | 0 | 0 | 0 | | "0" | 2000-01-02 00:00:00 | -3.261309 | 0 | 0 | 0 | | "0" | 2000-01-03 00:00:00 | -1.728725 | 0 | 0 | 0 | | "0" | 2000-01-04 00:00:00 | -1.117742 | 0 | 0 | 0 | | "0" | 2000-01-05 00:00:00 | -1.767059 | 0 | 0 | 0 | | "0" | 2000-01-06 00:00:00 | -2.664304 | 0 | 0 | 0 | | "0" | 2000-01-07 00:00:00 | -1.661958 | 0 | 0 | 0 | | "0" | 2000-01-08 00:00:00 | -2.073275 | 0 | 0 | 0 | | "0" | 2000-01-09 00:00:00 | -1.72048 | 0 | 0 | 0 | | "0" | 2000-01-10 00:00:00 | -1.34947 | 0 | 0 | 0 | ```python theme={null} ts = df["ds"].to_list() y = df["y"].to_numpy() anom = df["anomaly_flag"].to_numpy().astype(bool) cp = df["changepoint_flag"].to_numpy().astype(bool) miss = df["missing_flag"].to_numpy().astype(bool) fig, axes = plt.subplots(4, 1, figsize=(12, 8), sharex=True, gridspec_kw={"height_ratios": [3, 1, 1, 1]}) # Panel 1: time series with flagged points axes[0].plot(ts, y, linewidth=0.7, color="steelblue", label="y", zorder=1) if anom.any(): axes[0].scatter([ts[i] for i in range(len(ts)) if anom[i]], y[anom], color="red", s=30, zorder=3, label="Anomaly") if cp.any(): for i, t_cp in enumerate([ts[i] for i in range(len(ts)) if cp[i]]): axes[0].axvline(t_cp, color="green", linewidth=1.2, alpha=0.7, label="Changepoint" if i == 0 else None) if miss.any(): axes[0].scatter([ts[i] for i in range(len(ts)) if miss[i]], np.full(miss.sum(), np.nanmin(y) - 2), marker="|", color="orange", s=40, zorder=2, label="Missing") axes[0].set_ylabel("y") axes[0].set_title("Time series with pattern injection flags") axes[0].legend(loc="upper left") # Panels 2-4: binary flag traces for ax, (name, flag, color) in zip( axes[1:], [("anomaly_flag", anom, "red"), ("changepoint_flag", cp, "green"), ("missing_flag", miss, "orange")], ): ax.fill_between(ts, 0, flag.astype(int), color=color, alpha=0.5) ax.set_ylabel(name, fontsize=9) ax.set_ylim(-0.1, 1.3) ax.set_yticks([0, 1]) plt.tight_layout() plt.show() ``` ## Correlated exogenous variables Generate additional numeric columns that are statistically related to the target series. Three methods are available: | Method | Description | | ------------------ | ------------------------------------------------------ | | `correlated_noise` | Cholesky-based noise with a target Pearson correlation | | `lagged_copy` | Shifted copy of the target series with additive noise | | `trend_following` | Moving-average smoothing of the target | > **These are derived from the target** > > Each correlated column is computed *from* `y` — a correlated draw, a > lagged copy, or a smoothed trend — so it carries information about the > target by construction. That is exactly what you want when testing > whether a model can exploit covariates. But treat them accordingly: a > `lagged_copy` with `lag=7` is only a leak-free feature for a real > forecast if the lag exceeds the forecast horizon. ```python theme={null} gen = RandomWalkGenerator(engine="polars", **{ "min_length": 200, "max_length": 200, "freq": "D", "seed": 42, "drift": 0.05, "volatility": 1.0, "exogenous": ExogenousConfig( correlated=[ CorrelatedExogConfig( name="corr_noise", method="correlated_noise", correlation=0.8, ), CorrelatedExogConfig( name="lagged_y", method="lagged_copy", lag=7, noise_std=0.3, ), CorrelatedExogConfig( name="trend", method="trend_following", smoothing_window=14, trend_noise_std=0.1, ), ] ), }) df = gen.generate(n_series=1) print(f"Columns: {df.columns}") y = df["y"].to_numpy() corr = np.corrcoef(y, df["corr_noise"].to_numpy())[0, 1] print(f"Target correlation: 0.8, actual: {corr:.3f}") df.head(10) ``` ```text theme={null} Columns: ['unique_id', 'ds', 'y', 'corr_noise', 'lagged_y', 'trend'] Target correlation: 0.8, actual: 0.822 ``` | unique\_id | ds | y | corr\_noise | lagged\_y | trend | | ---------- | ------------------- | --------- | ----------- | --------- | --------- | | cat | datetime\[ns] | f64 | f64 | f64 | f64 | | "0" | 2000-01-01 00:00:00 | -1.351594 | -2.863126 | -1.966645 | -0.657573 | | "0" | 2000-01-02 00:00:00 | -2.207539 | -2.581491 | -2.222155 | -0.844525 | | "0" | 2000-01-03 00:00:00 | -1.202484 | -1.597043 | -1.455453 | -0.922337 | | "0" | 2000-01-04 00:00:00 | -0.811828 | -1.814348 | -1.177472 | -0.903796 | | "0" | 2000-01-05 00:00:00 | -1.261372 | -1.691236 | -1.524818 | -1.119365 | | "0" | 2000-01-06 00:00:00 | -1.876202 | -2.270328 | -1.976439 | -0.993435 | | "0" | 2000-01-07 00:00:00 | -1.224639 | -1.148895 | -0.949868 | -0.943064 | | "0" | 2000-01-08 00:00:00 | -1.515517 | -1.246452 | -1.749512 | -0.86473 | | "0" | 2000-01-09 00:00:00 | -1.296987 | -1.646017 | -2.19835 | -0.558921 | | "0" | 2000-01-10 00:00:00 | -1.066314 | -0.980272 | -1.347734 | -0.315824 | ```python theme={null} ts = df["ds"].to_list() y = df["y"].to_numpy() fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True) # Panel 1: correlated noise ax = axes[0] ax.plot(ts, y, linewidth=0.8, color="steelblue", label="y") ax2 = ax.twinx() ax2.plot(ts, df["corr_noise"].to_numpy(), linewidth=0.8, color="darkorange", alpha=0.8, label="corr_noise (r=0.8)") ax.set_ylabel("y", color="steelblue") ax2.set_ylabel("corr_noise", color="darkorange") corr = np.corrcoef(y, df["corr_noise"].to_numpy())[0, 1] ax.set_title(f"Correlated Noise (target r=0.8, actual r={corr:.3f})") lines1, labels1 = ax.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax.legend(lines1 + lines2, labels1 + labels2, loc="upper left") # Panel 2: lagged copy ax = axes[1] ax.plot(ts, y, linewidth=0.8, color="steelblue", label="y") ax.plot(ts, df["lagged_y"].to_numpy(), linewidth=0.8, color="green", alpha=0.8, linestyle="--", label="lagged_y (lag=7)") ax.set_ylabel("Value") ax.set_title("Lagged copy (lag=7 days, noise_std=0.3)") ax.legend(loc="upper left") # Panel 3: trend following ax = axes[2] ax.plot(ts, y, linewidth=0.8, color="steelblue", label="y") ax.plot(ts, df["trend"].to_numpy(), linewidth=1.5, color="crimson", alpha=0.9, label="trend (window=14)") ax.set_ylabel("Value") ax.set_title("Trend following (smoothing_window=14)") ax.legend(loc="upper left") plt.tight_layout() plt.show() ``` ## Combined: all exogenous types All exogenous types can be combined freely in a single generator call. ```python theme={null} gen = RandomWalkGenerator(engine="polars", **{ "min_length": 200, "max_length": 200, "freq": "h", "seed": 42, "drift": 0.02, "volatility": 1.0, "anomalies": True, "anomaly_fraction": 0.04, "spike_magnitude": 12.0, "dip_magnitude": -12.0, "changepoints": True, "num_changepoints": 2, "missing_data": True, "missing_rate": 0.05, "exogenous": ExogenousConfig( datetime_features=True, datetime_cyclical=True, anomaly_flags=True, changepoint_flags=True, missing_flags=True, correlated=[ CorrelatedExogConfig(name="price", correlation=0.7), CorrelatedExogConfig( name="trend", method="trend_following", smoothing_window=12, trend_noise_std=0.05, ), ], ), }) df = gen.generate(n_series=1) print(f"Generated DataFrame: {df.shape[0]} rows x {df.shape[1]} columns") print(f"Columns: {df.columns}") df.head(10) ``` ```text theme={null} Generated DataFrame: 200 rows x 24 columns Columns: ['unique_id', 'ds', 'y', 'anomaly_flag', 'changepoint_flag', 'missing_flag', 'year', 'quarter', 'month', 'day_of_year', 'day_of_week', 'day_of_month', 'is_weekend', 'hour', 'hour_sin', 'hour_cos', 'dow_sin', 'dow_cos', 'month_sin', 'month_cos', 'doy_sin', 'doy_cos', 'price', 'trend'] ``` | unique\_id | ds | y | anomaly\_flag | changepoint\_flag | missing\_flag | year | quarter | month | day\_of\_year | day\_of\_week | day\_of\_month | is\_weekend | hour | hour\_sin | hour\_cos | dow\_sin | dow\_cos | month\_sin | month\_cos | doy\_sin | doy\_cos | price | trend | | ---------- | ------------------- | --------- | ------------- | ----------------- | ------------- | ---- | ------- | ----- | ------------- | ------------- | -------------- | ----------- | ---- | --------- | ---------- | --------- | --------- | ---------- | ---------- | -------- | -------- | --------- | --------- | | cat | datetime\[ns] | f64 | i8 | i8 | i8 | i32 | i8 | i8 | i16 | i8 | i8 | i8 | i8 | f32 | f32 | f32 | f32 | f32 | f32 | f32 | f32 | f64 | f64 | | "0" | 2000-01-01 00:00:00 | -1.381594 | 0 | 0 | 0 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 0 | 0.0 | 1.0 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | -2.731882 | -0.880927 | | "0" | 2000-01-01 01:00:00 | -2.267539 | 0 | 0 | 0 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 1 | 0.258819 | 0.965926 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | -2.306626 | -0.900407 | | "0" | 2000-01-01 02:00:00 | -1.292484 | 0 | 0 | 0 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 2 | 0.5 | 0.866025 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | -1.243434 | -0.086426 | | "0" | 2000-01-01 03:00:00 | -0.931828 | 0 | 0 | 0 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 3 | 0.707107 | 0.707107 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | -1.545055 | -0.105205 | | "0" | 2000-01-01 04:00:00 | -1.411372 | 0 | 0 | 0 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 4 | 0.866025 | 0.5 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | -1.351844 | -0.202032 | | "0" | 2000-01-01 05:00:00 | -2.056202 | 0 | 0 | 0 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 5 | 0.965926 | 0.258819 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | -1.976786 | -0.124526 | | "0" | 2000-01-01 06:00:00 | -1.434639 | 0 | 0 | 0 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 6 | 1.0 | 6.1232e-17 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | -0.712831 | -0.026375 | | "0" | 2000-01-01 07:00:00 | 10.244483 | 1 | 0 | 0 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 7 | 0.965926 | -0.258819 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | -0.282877 | 0.024575 | | "0" | 2000-01-01 08:00:00 | NaN | 0 | 0 | 1 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 8 | 0.866025 | -0.5 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | 0.047155 | 0.437111 | | "0" | 2000-01-01 09:00:00 | -1.366314 | 0 | 0 | 0 | 2000 | 1 | 1 | 1 | 5 | 1 | 1 | 9 | 0.707107 | -0.707107 | -0.974928 | -0.222521 | 0.0 | 1.0 | 0.0 | 1.0 | -0.532896 | 0.532599 | ```python theme={null} ts = df["ds"].to_list() y = df["y"].to_numpy() anom = df["anomaly_flag"].to_numpy().astype(bool) cp = df["changepoint_flag"].to_numpy().astype(bool) miss = df["missing_flag"].to_numpy().astype(bool) fig, axes = plt.subplots(4, 1, figsize=(12, 10), sharex=True, gridspec_kw={"height_ratios": [3, 1.5, 1.5, 1]}) # Panel 1: main series + trend + anomaly/changepoint markers ax = axes[0] ax.plot(ts, y, linewidth=0.7, color="steelblue", label="y") ax.plot(ts, df["trend"].to_numpy(), linewidth=1.5, color="crimson", alpha=0.8, label="trend_following") if anom.any(): ax.scatter([ts[i] for i in range(len(ts)) if anom[i]], y[anom], color="red", s=25, zorder=3, label="Anomaly") if cp.any(): for i, t_cp in enumerate([ts[i] for i in range(len(ts)) if cp[i]]): ax.axvline(t_cp, color="green", linewidth=1, alpha=0.6, label="Changepoint" if i == 0 else None) ax.set_ylabel("y") ax.set_title("Combined exogenous: series + trend + flags") ax.legend(loc="upper left", fontsize=8) # Panel 2: correlated price ax = axes[1] ax.plot(ts, y, linewidth=0.6, color="steelblue", alpha=0.5, label="y") ax2 = ax.twinx() ax2.plot(ts, df["price"].to_numpy(), linewidth=0.7, color="darkorange", label="price (r=0.7)") ax.set_ylabel("y", color="steelblue") ax2.set_ylabel("price", color="darkorange") lines1, labels1 = ax.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax.legend(lines1 + lines2, labels1 + labels2, loc="upper left", fontsize=8) ax.set_title("Correlated exogenous: price") # Panel 3: cyclical hour encoding ax = axes[2] ax.plot(ts, df["hour_sin"].to_numpy(), linewidth=0.8, color="darkorange", label="hour_sin") ax.plot(ts, df["hour_cos"].to_numpy(), linewidth=0.8, color="purple", label="hour_cos") ax.set_ylabel("Encoding") ax.set_ylim(-1.15, 1.15) ax.set_title("Cyclical hour encoding") ax.legend(loc="upper right", fontsize=8) # Panel 4: combined binary flags ax = axes[3] ax.fill_between(ts, 0, anom.astype(int) * 0.9 + 2.0, color="red", alpha=0.5, label="anomaly") ax.fill_between(ts, 0, cp.astype(int) * 0.9 + 1.0, color="green", alpha=0.5, label="changepoint") ax.fill_between(ts, 0, miss.astype(int) * 0.9, color="orange", alpha=0.5, label="missing") ax.set_ylabel("Flags") ax.set_yticks([0.45, 1.45, 2.45]) ax.set_yticklabels(["missing", "changepoint", "anomaly"], fontsize=8) ax.set_ylim(-0.1, 3.2) ax.set_title("Pattern injection flags") plt.tight_layout() plt.show() ``` # Missing data patterns Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/missingness.html Real time series arrive with gaps — sensor outages, weekend reporting holidays, dropped records. SynForecast injects missing values so you can test imputation and check that a model tolerates gaps rather than silently mishandling them. Three patterns cover the common shapes. > **Choosing a pattern** > > * `"random"` — points drop independently (transient dropouts). > * `"block"` — contiguous runs go missing (outages); set > `missing_block_size`. > * `"seasonal"` — gaps recur on a cycle (e.g. weekends); set > `missing_seasonal_period`. > > `missing_rate` is the target fraction missing. Missing values appear > as `NaN` in `y`; the plots below mark them with red ticks along the > bottom. ```python theme={null} import matplotlib.pyplot as plt import polars as pl from synforecast.generators import RandomWalkGenerator, SeasonalGenerator def plot_missing(df, title): """Plot each series (gaps where missing) and rug-mark missing timestamps.""" fig, ax = plt.subplots(figsize=(11, 4)) for uid in df["unique_id"].unique(maintain_order=True).to_list(): s = df.filter(pl.col("unique_id") == uid).sort("ds") ax.plot(s["ds"], s["y"], linewidth=1, alpha=0.85, label=str(uid)) missing = df.filter(pl.col("y").is_nan()) if missing.height: y0 = df["y"].min() ax.scatter(missing["ds"], [y0] * missing.height, marker="|", color="crimson", s=40, label="missing") ax.set(title=title, xlabel="ds", ylabel="y") ax.legend(fontsize=8) plt.tight_layout() plt.show() def missing_rate(df): return df["y"].is_nan().mean() ``` ## Random gaps Independent dropouts scattered through the series — the simplest pattern, and a reasonable default for unreliable feeds. ```python theme={null} random_df = RandomWalkGenerator( engine="polars", min_length=200, max_length=200, freq="D", missing_data=True, missing_pattern="random", missing_rate=0.15, seed=42, ).generate(n_series=1) print(f"target rate 15%, actual {missing_rate(random_df):.1%}") plot_missing(random_df, "Random missing (15%)") ``` ```text theme={null} target rate 15%, actual 15.0% ``` ## Block gaps (outages) Real outages remove *consecutive* observations. `missing_block_size` sets the typical run length, so the same overall rate now arrives in a few long stretches instead of many isolated points. ```python theme={null} block_df = RandomWalkGenerator( engine="polars", min_length=200, max_length=200, freq="D", missing_data=True, missing_pattern="block", missing_rate=0.2, missing_block_size=5, seed=123, ).generate(n_series=1) print(f"target rate 20%, actual {missing_rate(block_df):.1%}, block size 5") plot_missing(block_df, "Block missing (outages, ~5-day blocks)") ``` ```text theme={null} target rate 20%, actual 20.0%, block size 5 ``` ## Seasonal gaps (recurring holidays) With `missing_pattern="seasonal"` gaps recur on a fixed cycle — the classic weekend-reporting gap on daily data (`missing_seasonal_period=7`). ```python theme={null} seasonal_df = SeasonalGenerator( engine="polars", min_length=364, max_length=364, freq="D", seasonality_period=7, seasonality_amplitude=10.0, base_level=100.0, missing_data=True, missing_pattern="seasonal", missing_rate=0.12, missing_seasonal_period=7, seed=456, ).generate(n_series=1) print(f"target rate 12%, actual {missing_rate(seasonal_df):.1%}") plot_missing(seasonal_df.head(84), "Seasonal missing (first 12 weeks)") ``` ```text theme={null} target rate 12%, actual 10.2% ``` The gaps concentrate on particular days of the week rather than spreading evenly — the breakdown below makes that concrete. ```python theme={null} ( seasonal_df.with_columns( (pl.col("ds").dt.weekday()).alias("weekday"), pl.col("y").is_nan().alias("is_missing"), ) .group_by("weekday") .agg((pl.col("is_missing").mean() * 100).round(1).alias("missing_pct")) .sort("weekday") ) ``` | weekday | missing\_pct | | ------- | ------------ | | i8 | f64 | | 1 | 26.9 | | 2 | 9.6 | | 3 | 7.7 | | 4 | 0.0 | | 5 | 5.8 | | 6 | 7.7 | | 7 | 13.5 | ## Rate and multiple series `missing_rate` scales the amount missing, and each series in a panel gets its own independent gaps. ```python theme={null} for rate in (0.05, 0.15, 0.30): df = RandomWalkGenerator( engine="polars", min_length=300, max_length=300, freq="D", missing_data=True, missing_pattern="random", missing_rate=rate, seed=789, ).generate(n_series=1) print(f"target {rate:>4.0%} -> actual {missing_rate(df):.1%}") multi_df = RandomWalkGenerator( engine="polars", min_length=100, max_length=100, freq="D", missing_data=True, missing_pattern="random", missing_rate=0.2, seed=1234, ).generate(n_series=3) print("\nper-series missing rate:") print( multi_df.group_by("unique_id") .agg((pl.col("y").is_nan().mean() * 100).round(1).alias("missing_pct")) .sort("unique_id") ) ``` ```text theme={null} target 5% -> actual 5.0% target 15% -> actual 15.0% target 30% -> actual 30.0% per-series missing rate: shape: (3, 2) ┌───────────┬─────────────┐ │ unique_id ┆ missing_pct │ │ --- ┆ --- │ │ cat ┆ f64 │ ╞═══════════╪═════════════╡ │ 0 ┆ 20.0 │ │ 1 ┆ 20.0 │ │ 2 ┆ 20.0 │ └───────────┴─────────────┘ ``` > **Related capabilities** > > * [Multivariate missingness](multivariate_missingness) — coordinated > gaps across channels (e.g. a whole sensor dropping out). > * [Anomalies](anomalies) and [changepoints](changepoints) compose > with missing data for realistic test series. # Multivariate missing data Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/multivariate_missingness.html Multivariate feeds lose data too — a sensor drops out, a market halts, a region stops reporting. SynForecast applies the same [missingness](missingness) patterns to multivariate generators (`VAR`, `CopulaGenerator`), so you can test multivariate imputation and check that a model still recovers the cross-series structure through the gaps. > **Independent gaps, shared structure** > > Each channel receives its *own* missing pattern — the series don’t > drop out in lockstep — while the correlation between them is left > intact, because missingness is applied after the correlated values are > generated. The final section confirms the cross-series correlation > survives. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import polars as pl from synforecast.generators import CopulaGenerator, VARGenerator ``` ## VAR generator with random missing data Generate 3 correlated series from a VAR(1) model with 15% random missing data. ```python theme={null} var_params = { "min_length": 200, "max_length": 200, "freq": "D", "lag_order": 1, "missing_data": True, "missing_pattern": "random", "missing_rate": 0.15, "seed": 42, } var_gen = VARGenerator(engine="polars", **var_params) df_var = var_gen.generate(n_series=3) print( f"Generated {df_var['unique_id'].n_unique()} correlated series with VAR(1) model" ) print(f"Total observations: {len(df_var)}") df_var.head(30) ``` ```text theme={null} Generated 3 correlated series with VAR(1) model Total observations: 600 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 2.057667 | | "0" | 2000-01-02 00:00:00 | 0.295293 | | "0" | 2000-01-03 00:00:00 | 0.893053 | | "0" | 2000-01-04 00:00:00 | 0.235888 | | "0" | 2000-01-05 00:00:00 | NaN | | … | … | … | | "0" | 2000-01-26 00:00:00 | 0.474074 | | "0" | 2000-01-27 00:00:00 | NaN | | "0" | 2000-01-28 00:00:00 | 0.068059 | | "0" | 2000-01-29 00:00:00 | NaN | | "0" | 2000-01-30 00:00:00 | -0.438549 | ```python theme={null} print("Missing data statistics by series:") for series_id in df_var["unique_id"].unique().sort(): series_df = df_var.filter(pl.col("unique_id") == series_id) values = series_df["y"].to_numpy() nan_count = np.sum(np.isnan(values)) nan_rate = nan_count / len(values) print(f" {series_id}: {nan_count} missing ({nan_rate:.1%})") ``` ```text theme={null} Missing data statistics by series: 0: 30 missing (15.0%) 1: 30 missing (15.0%) 2: 30 missing (15.0%) ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_var["unique_id"].unique().to_list(): series = df_var.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8, marker=".", markersize=2, linewidth=0.8) ax.set_title("VAR series with random missing data (15%)") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.legend() plt.tight_layout() plt.show() ``` ## Copula generator with block missing data Generate correlated series using a Gaussian copula with week-long missing blocks, simulating synchronized outages. ```python theme={null} correlation_matrix = np.array([[1.0, 0.7, 0.3], [0.7, 1.0, 0.5], [0.3, 0.5, 1.0]]) copula_params = { "min_length": 200, "max_length": 200, "freq": "D", "copula_type": "gaussian", "correlation_matrix": correlation_matrix, "missing_data": True, "missing_pattern": "block", "missing_rate": 0.2, "missing_block_size": 7, "seed": 123, } copula_gen = CopulaGenerator(engine="polars", **copula_params) df_copula = copula_gen.generate(n_series=3) print( f"Generated {df_copula['unique_id'].n_unique()} correlated series with Gaussian copula" ) print(f"Missing blocks of size: {copula_params['missing_block_size']} days") df_copula.head(40) ``` ```text theme={null} Generated 3 correlated series with Gaussian copula Missing blocks of size: 7 days ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 1.05608 | | "0" | 2000-01-02 00:00:00 | 0.436385 | | "0" | 2000-01-03 00:00:00 | 0.680307 | | "0" | 2000-01-04 00:00:00 | -0.158773 | | "0" | 2000-01-05 00:00:00 | -0.99138 | | … | … | … | | "0" | 2000-02-05 00:00:00 | 0.293606 | | "0" | 2000-02-06 00:00:00 | 0.504607 | | "0" | 2000-02-07 00:00:00 | NaN | | "0" | 2000-02-08 00:00:00 | NaN | | "0" | 2000-02-09 00:00:00 | NaN | ```python theme={null} print("Block missing statistics by series:") for series_id in df_copula["unique_id"].unique().sort(): series_df = df_copula.filter(pl.col("unique_id") == series_id) values = series_df["y"].to_numpy() nan_count = np.sum(np.isnan(values)) nan_rate = nan_count / len(values) max_consecutive = 0 current_consecutive = 0 for val in values: if np.isnan(val): current_consecutive += 1 max_consecutive = max(max_consecutive, current_consecutive) else: current_consecutive = 0 print( f" {series_id}: {nan_count} missing ({nan_rate:.1%}), " f"max block: {max_consecutive} days" ) ``` ```text theme={null} Block missing statistics by series: 0: 33 missing (16.5%), max block: 12 days 1: 35 missing (17.5%), max block: 7 days 2: 30 missing (15.0%), max block: 9 days ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_copula["unique_id"].unique().to_list(): series = df_copula.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8, marker=".", markersize=2, linewidth=0.8) ax.set_title("Copula series with block missing data (7-day blocks)") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.legend() plt.tight_layout() plt.show() ``` ## VAR with seasonal missing data (weekend gaps) Simulate a year of VAR(2) data with weekly seasonal missing patterns, representing weekend reporting gaps. ```python theme={null} var_seasonal_params = { "min_length": 365, "max_length": 365, "freq": "D", "lag_order": 2, "missing_data": True, "missing_pattern": "seasonal", "missing_rate": 0.12, "missing_seasonal_period": 7, "seed": 456, } var_seasonal_gen = VARGenerator(engine="polars", **var_seasonal_params) df_var_seasonal = var_seasonal_gen.generate(n_series=2) print( f"Generated {df_var_seasonal['unique_id'].n_unique()} correlated series (1 year)" ) print( f"Seasonal period: {var_seasonal_params['missing_seasonal_period']} days (weekly)" ) print("\nSeasonal missing statistics:") for series_id in df_var_seasonal["unique_id"].unique().sort(): series_df = df_var_seasonal.filter(pl.col("unique_id") == series_id) values = series_df["y"].to_numpy() nan_count = np.sum(np.isnan(values)) nan_rate = nan_count / len(values) print(f" {series_id}: {nan_count} missing ({nan_rate:.1%})") df_var_seasonal.head(30) ``` ```text theme={null} Generated 2 correlated series (1 year) Seasonal period: 7 days (weekly) Seasonal missing statistics: 0: 44 missing (12.1%) 1: 46 missing (12.6%) ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 0.891458 | | "0" | 2000-01-02 00:00:00 | -1.258571 | | "0" | 2000-01-03 00:00:00 | NaN | | "0" | 2000-01-04 00:00:00 | -0.421688 | | "0" | 2000-01-05 00:00:00 | -0.204434 | | … | … | … | | "0" | 2000-01-26 00:00:00 | 0.997592 | | "0" | 2000-01-27 00:00:00 | 2.656017 | | "0" | 2000-01-28 00:00:00 | 0.936677 | | "0" | 2000-01-29 00:00:00 | 1.093172 | | "0" | 2000-01-30 00:00:00 | -0.687454 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_var_seasonal["unique_id"].unique().to_list(): series = df_var_seasonal.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8, marker=".", markersize=2, linewidth=0.8) ax.set_title("VAR series with seasonal missing data (weekend gaps)") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.legend() plt.tight_layout() plt.show() ``` ## Correlation across the gaps Estimate the cross-series correlation with and without missing data (dropping only the timestamps missing in either series). The estimate stays close, confirming the gaps are missing-at-random with respect to the dependence structure rather than distorting it. ```python theme={null} params_complete = { "min_length": 500, "max_length": 500, "freq": "D", "lag_order": 1, "missing_data": False, "seed": 789, } params_missing = { "min_length": 500, "max_length": 500, "freq": "D", "lag_order": 1, "missing_data": True, "missing_pattern": "random", "missing_rate": 0.25, "seed": 789, } gen_complete = VARGenerator(engine="polars", **params_complete) df_complete = gen_complete.generate(n_series=2) gen_missing = VARGenerator(engine="polars", **params_missing) df_missing = gen_missing.generate(n_series=2) # Calculate correlations series_0_complete = df_complete.filter(pl.col("unique_id") == "0")[ "y" ].to_numpy() series_1_complete = df_complete.filter(pl.col("unique_id") == "1")[ "y" ].to_numpy() corr_complete = np.corrcoef(series_0_complete, series_1_complete)[0, 1] series_0_missing = df_missing.filter(pl.col("unique_id") == "0")[ "y" ].to_numpy() series_1_missing = df_missing.filter(pl.col("unique_id") == "1")[ "y" ].to_numpy() mask = ~(np.isnan(series_0_missing) | np.isnan(series_1_missing)) corr_missing = np.corrcoef(series_0_missing[mask], series_1_missing[mask])[0, 1] print(f"Correlation (complete data): {corr_complete:.3f}") print(f"Correlation (25% missing): {corr_missing:.3f}") print(f"Correlation preserved: {abs(corr_complete - corr_missing) < 0.1}") ``` ```text theme={null} Correlation (complete data): 0.069 Correlation (25% missing): 0.041 Correlation preserved: True ``` ```python theme={null} fig, axes = plt.subplots(1, 2, figsize=(14, 4)) for uid in df_complete["unique_id"].unique().to_list(): series = df_complete.filter(pl.col("unique_id") == uid) axes[0].plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) axes[0].set_title(f"Complete Data (corr={corr_complete:.3f})") axes[0].set_xlabel("Timestamp") axes[0].set_ylabel("Value") axes[0].legend() for uid in df_missing["unique_id"].unique().to_list(): series = df_missing.filter(pl.col("unique_id") == uid) axes[1].plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8, marker=".", markersize=2, linewidth=0.8) axes[1].set_title(f"25% Missing Data (corr={corr_missing:.3f})") axes[1].set_xlabel("Timestamp") axes[1].set_ylabel("Value") axes[1].legend() plt.tight_layout() plt.show() ``` # Multivariatize a univariate generator Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/multivariatize.html `Multivariatizer` turns any univariate generator into a set of *cross-dependent* channels — the realistic case where series move together (a store’s product lines, correlated sensors, coupled macro indicators) rather than in isolation. It draws independent series from the wrapped generator, couples them, and preserves the wrapped generator’s per-series diversity. > **Two couplings** > > * **`mixing`** (contemporaneous): each output channel becomes a > linear blend of the base series through a well-conditioned mixing > matrix — instantaneous correlation. > * **`leadlag`** (temporal): a channel becomes a lagged, scaled, > noise-perturbed copy of another — one series leads, the other > follows. > > Both are sampled per call and can compose. For processes that are > multivariate *by construction*, see the [multivariate > generators](../generators/multivariate/var) instead. ```python theme={null} import matplotlib.pyplot as plt import numpy as np from synforecast import Multivariatizer from synforecast.generators import TSIGenerator ``` ```python theme={null} base = TSIGenerator( min_length=256, max_length=256, freq="h", engine="polars", seed=7, ) coupled = Multivariatizer(base=base, seed=42) df = coupled.generate(n_series=4) df.head() ``` | unique\_id | ds | y | | ---------- | ------------------- | ----------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -73.483746 | | "0" | 2000-01-01 01:00:00 | -250.816501 | | "0" | 2000-01-01 02:00:00 | -158.711667 | | "0" | 2000-01-01 03:00:00 | 26.946948 | | "0" | 2000-01-01 04:00:00 | -250.057768 | ## Inspect the recipe Every panel records exactly how it was built — the mixing matrix and each lead-lag edge — so a generated dataset is reproducible from its seed and auditable after the fact. ```python theme={null} coupled.last_recipe ``` ```text theme={null} {'couplings': ['mixing', 'leadlag'], 'length': 256, 'mixing': {'strength': 0.5072149078264366, 'matrix': array([[ 1. , 0. , 0. , 0. ], [ 0.19725864, 0.98035148, 0. , 0. ], [-0.02423582, -0.37406155, 0.92708715, 0. ], [ 0.00973988, 0.45183168, -0.12849622, 0.88274684]])}, 'leadlag': [{'src': 2, 'dst': 3, 'lag': 9, 'sign': -1.0, 'noise': 0.1807618018379956}]} ``` ```python theme={null} fig, ax = plt.subplots(figsize=(10, 4)) for series_id in df["unique_id"].unique().sort().to_list(): series = df.filter(df["unique_id"] == series_id).sort("ds") ax.plot(series["ds"].to_list(), series["y"].to_list(), label=series_id) ax.set(title="Coupled synthetic series", xlabel="Time", ylabel="y") ax.legend() plt.show() ``` ```python theme={null} wide = df.pivot(on="unique_id", index="ds", values="y").sort("ds") np.corrcoef(wide.drop("ds").to_numpy().T) ``` ```text theme={null} array([[ 1. , 0.13490276, -0.03079865, 0.01106283], [ 0.13490276, 1. , -0.39781199, -0.1085008 ], [-0.03079865, -0.39781199, 1. , -0.15540655], [ 0.01106283, -0.1085008 , -0.15540655, 1. ]]) ``` The off-diagonal terms are non-zero and asymmetric: the couplings induced genuine cross-series dependence, including the negative lead-lag link between channels 2 and 3 recorded in the recipe above. Because the whole panel derives from one seed, this structure regenerates exactly. # Robustness testing with known ground truth Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/robustness_testing.html When SynForecast injects an anomaly, changepoint, or gap, it also keeps the label and the uncorrupted series. That lets you measure two things you normally can’t: how well a detector finds the corruptions, and how much they cost a forecaster. Both need answers that real data doesn’t hand you. > **Why synthetic data** > > A detector’s precision and recall are only defined if you know which > points are anomalies. Measuring the cost of contamination needs a > clean copy of the same series. On real data you have neither; the > generator supplies both. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import polars as pl from utilsforecast.losses import mase from synforecast.exogenous import ExogenousConfig from synforecast.generators import SeasonalGenerator def robust_zscores(y: np.ndarray, window: int = 25) -> np.ndarray: """Rolling robust z-score: |y - median| / (1.4826 * MAD) in a window.""" n = len(y) z = np.zeros(n) half = window // 2 for i in range(n): lo, hi = max(0, i - half), min(n, i + half + 1) w = y[lo:hi] med = np.median(w) mad = np.median(np.abs(w - med)) scale = 1.4826 * mad if mad > 1e-9 else (np.std(w) + 1e-9) z[i] = abs(y[i] - med) / scale return z ``` ## Detecting anomalies We make weekly-seasonal series with 4% of points hit by spikes and dips, and record their positions with `ExogenousConfig(anomaly_flags=True)`. A rolling robust z-score scores each point; varying the threshold moves along the precision/recall trade-off. ```python theme={null} FLAGS = ExogenousConfig(anomaly_flags=True) panel = SeasonalGenerator( engine='polars', min_length=400, max_length=400, freq='D', seasonality_period=7, seasonality_amplitude=8.0, base_level=100.0, noise_level=2.0, anomalies=True, anomaly_fraction=0.04, anomaly_types=['spike', 'dip'], spike_magnitude=30.0, dip_magnitude=-30.0, exogenous=FLAGS, seed=7, ).generate(n_series=25) # Pool robust z-scores and ground-truth labels across every series. scores, labels = [], [] for uid in panel['unique_id'].unique(maintain_order=True): s = panel.filter(pl.col('unique_id') == uid) scores.append(robust_zscores(s['y'].to_numpy())) labels.append(s['anomaly_flag'].to_numpy()) scores = np.concatenate(scores) labels = np.concatenate(labels).astype(bool) print(f'{labels.sum()} injected anomalies across {len(labels)} points ' f'({labels.mean():.1%})') ``` ```text theme={null} 400 injected anomalies across 10000 points (4.0%) ``` ```python theme={null} thresholds = np.linspace(1.0, 8.0, 40) precision, recall, f1 = [], [], [] for t in thresholds: pred = scores > t tp = int((pred & labels).sum()) fp = int((pred & ~labels).sum()) fn = int((~pred & labels).sum()) p = tp / (tp + fp) if tp + fp else 1.0 r = tp / (tp + fn) if tp + fn else 0.0 precision.append(p) recall.append(r) f1.append(2 * p * r / (p + r) if p + r else 0.0) best = int(np.argmax(f1)) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) ax1.plot(recall, precision, marker='.', color='steelblue') ax1.scatter([recall[best]], [precision[best]], color='crimson', zorder=5, label=f'best F1={f1[best]:.2f} @ z>{thresholds[best]:.1f}') ax1.set(xlabel='recall', ylabel='precision', title='Precision-recall (labels known)') ax1.legend(); ax1.grid(alpha=0.3) ax2.plot(thresholds, f1, color='seagreen') ax2.axvline(thresholds[best], color='crimson', ls='--', alpha=0.7) ax2.set(xlabel='z-score threshold', ylabel='F1', title='F1 vs detector threshold') ax2.grid(alpha=0.3) plt.tight_layout(); plt.show() ``` The labels make the operating point measurable rather than assumed: the marked threshold is the one with the best F1. Higher thresholds buy precision at the cost of recall. Substitute another detector to see where it sits on the same axes. ## The cost of contamination We generate clean seasonal series, hold out the last 14 days, then corrupt a growing fraction of the *training* history and refit a trend-plus-seasonal-means model. That model reads its slope and seasonal profile off every training point, so outliers shift the fit — a seasonal-naive rule, which only repeats recent values, would barely register them. > **Scale against the clean series** > > Each forecast is scaled by an in-sample seasonal error taken from the > **clean** training series and held fixed across contamination levels. > Scale by the contaminated series instead and the spikes inflate the > denominator, so error appears to drop as contamination rises. ```python theme={null} H, SEASON = 14, 7 clean = SeasonalGenerator( engine='polars', min_length=200, max_length=200, freq='D', seasonality_period=SEASON, seasonality_amplitude=8.0, base_level=100.0, noise_level=2.0, seed=11, ).generate(n_series=40) rng = np.random.default_rng(0) ids = clean['unique_id'].unique(maintain_order=True).to_list() series = [clean.filter(pl.col('unique_id') == u)['y'].to_numpy() for u in ids] dates = clean.filter(pl.col('unique_id') == ids[0])['ds'].to_list() def trend_seasonal_forecast(train, h, season): """Least-squares trend + per-position seasonal means (outlier-sensitive).""" n = len(train) t = np.arange(n) slope, intercept = np.polyfit(t, train, 1) resid = train - (slope * t + intercept) seasonal = np.array([resid[k::season].mean() for k in range(season)]) fut = np.arange(n, n + h) return slope * fut + intercept + seasonal[fut % season] # `mase` takes its scale from train_df. Passing the CLEAN training frame, the # same one at every contamination level, is what keeps the denominator honest: # scaling by the contaminated series would inflate it and make error appear to # fall as noise is added. clean_train_df = pl.DataFrame( { 'unique_id': [uid for uid in ids for _ in dates[:-H]], 'ds': [d for _ in ids for d in dates[:-H]], 'y': np.concatenate([y[:-H] for y in series]), } ) levels = [0.0, 0.02, 0.05, 0.08, 0.12, 0.16] curve = [] for contamination in levels: forecasts = [] for y in series: train, test = y[:-H].copy(), y[-H:] k = int(round(contamination * len(train))) if k: idx = rng.choice(len(train), size=k, replace=False) train[idx] += rng.choice([-1, 1], size=k) * rng.uniform(15, 35, size=k) forecasts.append(trend_seasonal_forecast(train, H, SEASON)) scored = mase( pl.DataFrame( { 'unique_id': [uid for uid in ids for _ in range(H)], 'ds': [d for _ in ids for d in dates[-H:]], 'y': np.concatenate([y[-H:] for y in series]), 'forecast': np.concatenate(forecasts), } ), models=['forecast'], seasonality=SEASON, train_df=clean_train_df, ) curve.append(float(scored['forecast'].mean())) fig, ax = plt.subplots(figsize=(8, 4.5)) ax.plot([c * 100 for c in levels], curve, marker='o', color='crimson') ax.set(xlabel='% of training points corrupted', ylabel='mean MASE on clean holdout', title='Forecast error vs training contamination') ax.grid(alpha=0.3) plt.tight_layout(); plt.show() print('MASE at 0% / 16% contamination: ' f'{curve[0]:.3f} / {curve[-1]:.3f}') ``` ```text theme={null} MASE at 0% / 16% contamination: 0.722 / 1.157 ``` Error rises with contamination, measured on a holdout we know is clean and against a scale the noise can’t move. The same setup works for any pipeline: perturb the input by a known amount and watch a trustworthy metric respond. > **Related capabilities** > > * [Anomalies](anomalies), [changepoints](changepoints), and > [missingness](missingness) — the injection knobs used here, each > with a ground-truth flag column. > * [When does synthetic data help?](when_synthetic_helps) — the > accuracy picture, and the uses that sit outside it. # When does synthetic data help? Source: https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/when_synthetic_helps.html Synthetic data is often assumed to be a free accuracy boost: generate more series, get a better model. The two benchmarks on this page bound that claim. Adding synthetic series to a model that already has enough real data does not reliably lower error. Pretraining on synthetic data and applying it zero-shot does lower error for series with little history. Synthetic data is also useful for coverage, robustness testing, and privacy, which accuracy numbers on a single panel do not capture. > **Summary** > > Two paired, multi-seed results: > > * **Augmentation is neutral.** On M4 Monthly, adding `SynAugment` > series to a properly-scaled global forecaster did not reliably > beat the observed data alone. Across panel sizes 3–30 and 20 seeds > the win rate sat at a coin flip with no significant effect. > * **Zero-shot pretraining helps cold-start.** On M3 Monthly, a model > pretrained on synthetic data and used zero-shot beat training from > scratch by \~20% when each series kept only \~48 months of history, > in every one of 10 seeds. The edge reversed once history was > ample. > > Synthetic data lowers error where the model is short of data, not > where it already has enough. ## The benchmark We sample panels of `n` M4 Monthly series and compare a global gradient-boosted forecaster (`HistGradientBoostingRegressor` via MLForecast, with per-series standardization — the correct configuration for M4’s varied scales) trained on: * **observed** — the real series only; * **augmented** — real series plus `SynAugment` counterparts, fit on the training split only (leakage-safe). We forecast the official 18-month M4 holdout and score seasonal MASE, paired within each seed. The full sweep (20 seeds) lives in [`benchmarks/benchmark_augmentation.py`](https://github.com/Nixtla/synforecast/blob/main/benchmarks/benchmark_augmentation.py); here we load its committed summary. ```python theme={null} import json from pathlib import Path import matplotlib.pyplot as plt import pandas as pd summary_path = Path("benchmarks/data/augmentation_summary.json") if not summary_path.exists(): summary_path = Path("../../benchmarks/data/augmentation_summary.json") summary = json.loads(summary_path.read_text()) table = pd.DataFrame( { "series in panel": e["n_series"], "observed MASE": round(e["observed_mase"], 3), "augmented MASE": round(e["augmented_mase"], 3), "improvement %": round(e["mean_dmase_pct"], 1), "win rate": f"{e['win_rate']:.0%}", "Wilcoxon p": round(e["wilcoxon_p"], 3), } for e in summary["by_size"] ) print(f"{summary['dataset']} | {summary['model']} | {summary['seeds']} seeds") table ``` ```text theme={null} M4 Monthly (official 18-month holdout) | HistGradientBoostingRegressor via MLForecast, per-series scaling | 20 seeds ``` | | series in panel | observed MASE | augmented MASE | improvement % | win rate | Wilcoxon p | | - | --------------- | ------------- | -------------- | ------------- | -------- | ---------- | | 0 | 3 | 1.444 | 1.326 | 4.7 | 60% | 0.143 | | 1 | 5 | 1.336 | 1.393 | -5.9 | 45% | 0.784 | | 2 | 10 | 1.467 | 1.409 | 2.2 | 60% | 0.261 | | 3 | 30 | 1.301 | 1.248 | 2.9 | 65% | 0.294 | ```python theme={null} fig, ax = plt.subplots(figsize=(8, 5)) sizes = [e["n_series"] for e in summary["by_size"]] xs = range(len(sizes)) for cond, color in [("observed", "steelblue"), ("augmented", "crimson")]: means = [e[f"{cond}_mase"] for e in summary["by_size"]] los = [e[f"{cond}_ci"][0] for e in summary["by_size"]] his = [e[f"{cond}_ci"][1] for e in summary["by_size"]] ax.plot(list(xs), means, marker="o", color=color, label=cond) ax.fill_between(list(xs), los, his, color=color, alpha=0.15) ax.set_xticks(list(xs)) ax.set_xticklabels([str(n) for n in sizes]) ax.set(xlabel="number of observed series in the panel", ylabel="mean seasonal MASE (95% CI)", title="Seasonal MASE by panel size, observed vs augmented") ax.legend() plt.tight_layout() plt.show() ``` The confidence intervals overlap everywhere and the paired win rate never departs meaningfully from 50%. Two caveats about what the result does and does not show: > **Scope of the result** > > * **This is not evidence that synthetic data is useless.** It shows > that augmenting a model that already has enough signal does not > lower error. A gradient-boosted global model with a few full M4 > histories is not short of data. > * **A win is easy to manufacture.** An earlier version of this > benchmark showed a large apparent gain that came from an unscaled > baseline, where augmentation was compensating for the model’s poor > scale handling, and from too few seeds. With per-series scaling > and 20 seeds the effect vanished. Treat augmentation benchmarks > that omit either with suspicion. ## Other uses Synthetic data is useful in ways a single-panel accuracy number does not measure: > **Four use cases** > > * **Coverage and diversity.** > [`generate_series`](../getting-started/quickstart) and the > [balanced pool](balanced_pool) span behaviors — volatility > clustering, long memory, intermittency, chaos — that no single > real dataset contains. Useful for stress-testing a pipeline and > for pretraining breadth. > * **Robustness testing.** Inject [anomalies](anomalies), > [changepoints](changepoints), and [missingness](missingness) with > known ground truth to measure how a model degrades. > * **Privacy.** Share a reproducible, statistically-similar panel > without exposing proprietary series. > * **Pretraining at scale.** The diversity-targeted generators — > [TSI](../generators/pretraining/tsi), > [TCM](../generators/pretraining/tcm), and > [KernelSynth](../generators/pretraining/kernel_synth) — exist to > pretrain models that transfer to unseen series. The section below > measures this: a synthetic-pretrained model used zero-shot beats > training from scratch when the real history is short. Coverage is straightforward to check. One call yields a panel of visibly different data-generating processes: ```python theme={null} from synforecast import generate_series pool = generate_series( n_series=6, freq="D", min_length=200, max_length=200, engine="polars", seed=7 ) import polars as pl fig, axes = plt.subplots(3, 2, figsize=(12, 7), sharex=True) for ax, uid in zip(axes.flat, pool["unique_id"].unique(maintain_order=True).to_list()): s = pool.filter(pl.col("unique_id") == uid) ax.plot(s["ds"], s["y"], linewidth=1) ax.set_title(f"series {uid}", fontsize=9) fig.suptitle("Six series from one generate_series call") plt.tight_layout() plt.show() ``` ## Pretraining: a cold-start win The augmentation result above concerns a model that already has enough real data. Pretraining tests the opposite case: a model with too little real history to learn from. We pretrain a small NHITS on a synthetic corpus from [`pretraining_pool()`](balanced_pool), then forecast M3 Monthly series while varying how much history each series keeps, from 48 months up to the full record. The corpus is generated independently and never sees M3. Three ways to use the pretrained model, compared against the same 18-month holdout: * **from-scratch** — ignore pretraining, train NHITS on the real history alone. * **zero-shot** — apply the synthetic-pretrained model directly, no real training. * **pretrain + fine-tune** — continue training it on the real history at a reduced learning rate. ```python theme={null} pt_path = Path('benchmarks/data/pretraining_summary.json') if not pt_path.exists(): pt_path = Path('../../benchmarks/data/pretraining_summary.json') pt = json.loads(pt_path.read_text()) def _hist_label(h): return 'full' if h >= 9999 else f'{h} mo' pt_table = pd.DataFrame( { 'history kept': _hist_label(e['hist_len']), 'from-scratch MASE': round(e['from_scratch_mase'], 3), 'zero-shot MASE': round(e['zero_shot_mase'], 3), 'pretrain+ft MASE': round(e['pretrain_ft_mase'], 3), 'zero-shot improve %': round(e['zero_shot_mean_improve_pct'], 1), 'zero-shot win rate': f"{e['zero_shot_win_rate']:.0%}", 'zero-shot p': f"{e['zero_shot_wilcoxon_p']:.1e}", } for e in pt['by_hist_len'] ) print(f"{pt['dataset']} | {pt['model']} | {pt['seeds']} seeds x " f"{pt['n_series']} series") pt_table ``` ```text theme={null} M3 Monthly (last 18 months held out) | NHITS via NeuralForecast, per-series standardization | 10 seeds x 300 series ``` | | history kept | from-scratch MASE | zero-shot MASE | pretrain+ft MASE | zero-shot improve % | zero-shot win rate | zero-shot p | | - | ------------ | ----------------- | -------------- | ---------------- | ------------------- | ------------------ | ----------- | | 0 | 48 mo | 1.632 | 1.300 | 1.621 | 20.3 | 100% | 2.0e-03 | | 1 | 72 mo | 1.441 | 1.288 | 1.435 | 10.6 | 100% | 2.0e-03 | | 2 | 108 mo | 1.308 | 1.405 | 1.322 | -7.6 | 10% | 3.9e-03 | | 3 | full | 1.238 | 1.395 | 1.261 | -12.8 | 0% | 2.0e-03 | ```python theme={null} fig, ax = plt.subplots(figsize=(8, 5)) labels = [_hist_label(e['hist_len']) for e in pt['by_hist_len']] xs = range(len(labels)) conds = [ ('from_scratch', 'from-scratch (real only)', 'steelblue'), ('zero_shot', 'zero-shot (synthetic-pretrained)', 'crimson'), ('pretrain_ft', 'pretrain + fine-tune', 'goldenrod'), ] for key, label, color in conds: means = [e[f'{key}_mase'] for e in pt['by_hist_len']] los = [e[f'{key}_ci'][0] for e in pt['by_hist_len']] his = [e[f'{key}_ci'][1] for e in pt['by_hist_len']] ax.plot(list(xs), means, marker='o', color=color, label=label) ax.fill_between(list(xs), los, his, color=color, alpha=0.12) ax.set_xticks(list(xs)) ax.set_xticklabels(labels) ax.set(xlabel='real history kept per series', ylabel='mean seasonal MASE (95% CI)', title='Seasonal MASE by history length kept per series') ax.legend() plt.tight_layout() plt.show() ``` With only 48 months of history the zero-shot model cuts seasonal MASE by about 20% against training from scratch, winning in all 10 seeds (p = 0.002, the smallest value a 10-seed paired test can produce). The from-scratch model has too few windows to learn the seasonality the pretrained model already carries. The gap narrows as history grows and reverses past roughly 100 months, where a from-scratch model has enough data and wins outright. Fine-tuning lands in between and rarely wins: on short histories it drags the pretrained model toward the data-starved fit, and on long ones it cannot beat from-scratch. Use zero-shot for cold-start series and from-scratch once history is ample. ## Reproduce ```bash theme={null} uv run python benchmarks/benchmark_augmentation.py --seeds 20 \ --save benchmarks/data/augmentation_results.csv \ --save-summary benchmarks/data/augmentation_summary.json ``` Use `--quick` for a fast smoke run. The script is model- and dataset-agnostic enough to re-point at another panel. To decide whether augmentation helps on a given problem, run the same paired, multi-seed comparison on that data, never on the final holdout. And the pretraining sweep (needs a GPU): ```bash theme={null} uv run python benchmarks/benchmark_pretraining.py --seeds 10 \ --save-summary benchmarks/data/pretraining_summary.json ``` # Clickstream Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/domain/clickstream.html Web clickstream data is bursty and sessionized: visits cluster into sessions, sessions convert at some rate, and traffic ebbs and flows with time of day and day of week. `ClickstreamGenerator` reproduces these session and conversion dynamics for web-analytics modeling. > **The model** > > Human sessions per time bin are Poisson-distributed around > `base_sessions`, modulated by hour-of-day and day-of-week seasonality > and a slow log-random-walk trend. Bot traffic — a flatter profile with > occasional crawl spikes — is added on top when `include_bots` is set. > Pageviews (a geometric page depth for engaged sessions), bounces, and > conversions are derived from the human sessions, with multipliers set > by `traffic_source`. `output_type` chooses which of the four series to > return. > > The seasonality assumes hourly data: with `freq='h'` the day/night and > weekday patterns are correct, and other frequencies distort them. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import ClickstreamGenerator ``` ## Session counts (1 week hourly) Generate hourly session data for 3 series with mixed traffic sources, bot detection, and conversion tracking. ```python theme={null} params = { "min_length": 168, "max_length": 168, "freq": "h", "base_sessions": 500, "traffic_source": "mixed", "conversion_rate": 0.03, "bounce_rate": 0.40, "include_seasonality": True, "include_bots": True, "bot_fraction": 0.15, "output_type": "sessions", "seed": 42, } generator = ClickstreamGenerator(engine="polars", **params) df = generator.generate(n_series=3) print(f"Generated {df['unique_id'].n_unique()} time series") print(f"Total hourly observations: {len(df)}") stats = df.group_by("unique_id").agg( [ pl.col("y").sum().alias("total_sessions"), pl.col("y").mean().alias("avg_per_hour"), pl.col("y").max().alias("peak_hour"), ] ) stats ``` ```text theme={null} Generated 3 time series Total hourly observations: 504 ``` | unique\_id | total\_sessions | avg\_per\_hour | peak\_hour | | ---------- | --------------- | -------------- | ---------- | | cat | f64 | f64 | f64 | | "0" | 97521.0 | 580.482143 | 1246.0 | | "1" | 97112.0 | 578.047619 | 1149.0 | | "2" | 100944.0 | 600.857143 | 1218.0 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df["unique_id"].unique().to_list(): series = df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Sessions") ax.set_title("Clickstream sessions (1 week hourly)") ax.legend() plt.tight_layout() plt.show() ``` ## Traffic source comparison Compare conversion rates across different traffic sources: organic, paid, direct, and referral. ```python theme={null} sources = ["organic", "paid", "direct", "referral"] for source in sources: gen = ClickstreamGenerator(engine="polars", **{ "min_length": 168, "max_length": 168, "freq": "h", "base_sessions": 500, "traffic_source": source, "output_type": "conversions", "seed": 42, } ) source_df = gen.generate(n_series=1) conv_total = source_df["y"].sum() gen_sessions = ClickstreamGenerator(engine="polars", **{ "min_length": 168, "max_length": 168, "freq": "h", "base_sessions": 500, "traffic_source": source, "output_type": "sessions", "seed": 42, } ) session_df = gen_sessions.generate(n_series=1) session_total = session_df["y"].sum() conv_rate = conv_total / session_total * 100 if session_total > 0 else 0 print( f"{source:10s}: {session_total:,.0f} sessions, {conv_total:,.0f} conversions ({conv_rate:.2f}%)" ) ``` ```text theme={null} organic : 97,521 sessions, 2,083 conversions (2.14%) paid : 97,602 sessions, 2,715 conversions (2.78%) direct : 97,507 sessions, 3,576 conversions (3.67%) referral : 97,833 sessions, 1,065 conversions (1.09%) ``` ## Complete metrics Generate a full set of web analytics metrics including sessions, pageviews, conversions, bounces, and derived rates. ```python theme={null} full_metrics = generator.generate_full_metrics(n_series=1) print(f"Total sessions: {full_metrics['sessions'].sum():,.0f}") print(f"Total pageviews: {full_metrics['pageviews'].sum():,.0f}") print(f"Total conversions: {full_metrics['conversions'].sum():,.0f}") print(f"Total bounces: {full_metrics['bounces'].sum():,.0f}") print(f"Avg bounce rate: {full_metrics['bounce_rate'].mean() * 100:.1f}%") print(f"Avg conversion rate: {full_metrics['conversion_rate'].mean() * 100:.2f}%") print(f"Avg pages/session: {full_metrics['pages_per_session'].mean():.2f}") ``` ```text theme={null} Total sessions: 101,797 Total pageviews: 293,073 Total conversions: 1,531 Total bounces: 34,682 Avg bounce rate: 34.1% Avg conversion rate: 1.52% Avg pages/session: 2.89 ``` ```python theme={null} fig, axes = plt.subplots(2, 2, figsize=(14, 8)) metrics = [("sessions", "Sessions"), ("pageviews", "Pageviews"), ("conversions", "Conversions"), ("bounce_rate", "Bounce Rate")] for ax, (col, title) in zip(axes.flat, metrics): ax.plot(full_metrics[col], alpha=0.8) ax.set_xlabel("Observation") ax.set_ylabel(title) ax.set_title(title) ax.tick_params(axis="x", rotation=45) plt.suptitle("Full web analytics metrics", fontsize=14) plt.tight_layout() plt.show() ``` ## Conversion funnel Simulate a conversion funnel with 10,000 sessions to see drop-off rates at each stage. ```python theme={null} funnel = generator.generate_funnel(n_sessions=10000) print("Stage | Count | Rate") print("-" * 45) prev_count = None for stage, count in funnel.items(): if prev_count is None: rate = 100.0 else: rate = count / prev_count * 100 print(f"{stage:20s} | {count:8,d} | {rate:5.1f}%") prev_count = count overall_conv = funnel[list(funnel.keys())[-1]] / funnel["visit"] * 100 print(f"\nOverall funnel conversion: {overall_conv:.2f}%") ``` ```text theme={null} Stage | Count | Rate --------------------------------------------- visit | 10,000 | 100.0% product_view | 4,680 | 46.8% add_to_cart | 1,899 | 40.6% checkout_start | 1,201 | 63.2% checkout_complete | 905 | 75.4% Overall funnel conversion: 9.05% ``` ```python theme={null} fig, ax = plt.subplots(figsize=(10, 5)) stages = list(funnel.keys()) counts = list(funnel.values()) bars = ax.barh(stages[::-1], counts[::-1], color=plt.cm.Blues( [0.3 + 0.7 * i / (len(stages) - 1) for i in range(len(stages))] )) for bar, count in zip(bars, counts[::-1]): ax.text(bar.get_width() + 50, bar.get_y() + bar.get_height() / 2, f"{count:,}", va="center") ax.set_xlabel("Count") ax.set_title("Conversion funnel") plt.tight_layout() plt.show() ``` ## Hour-of-day pattern Analyze the average session count by hour of day over 4 weeks to reveal the daily traffic pattern. ```python theme={null} hourly_gen = ClickstreamGenerator(engine="polars", **{ "min_length": 168 * 4, "max_length": 168 * 4, "freq": "h", "base_sessions": 1000, "include_seasonality": True, "include_bots": False, "seed": 42, } ) hourly_df = hourly_gen.generate(n_series=1) sessions = hourly_df["y"].to_numpy() hourly_avg = [sessions[i::24].mean() for i in range(24)] print("Hour | Avg Sessions | Relative") print("-" * 35) mean_hourly = sum(hourly_avg) / 24 for hour, avg in enumerate(hourly_avg): relative = avg / mean_hourly bar = "*" * int(relative * 10) print(f" {hour:02d} | {avg:7.1f} | {bar}") ``` ```text theme={null} Hour | Avg Sessions | Relative ----------------------------------- 00 | 364.1 | **** 01 | 247.8 | ** 02 | 182.5 | ** 03 | 126.5 | * 04 | 125.5 | * 05 | 180.3 | ** 06 | 370.1 | **** 07 | 611.0 | ****** 08 | 849.5 | ********* 09 | 1102.0 | ************ 10 | 1209.0 | ************* 11 | 1337.9 | *************** 12 | 1216.2 | ************* 13 | 1157.9 | ************* 14 | 1100.3 | ************ 15 | 1047.2 | *********** 16 | 1093.8 | ************ 17 | 1217.2 | ************* 18 | 1339.8 | *************** 19 | 1470.6 | **************** 20 | 1579.6 | ***************** 21 | 1452.0 | **************** 22 | 1090.0 | ************ 23 | 615.1 | ******* ``` ## Model information Inspect the generator’s configuration and parameters. ```python theme={null} info = generator.get_model_info() for key, value in info.items(): if key != "source_params": print(f"{key}: {value}") ``` ```text theme={null} base_sessions: 500.0 traffic_source: mixed conversion_rate: 0.03 bounce_rate: 0.4 avg_session_depth: 3.5 include_seasonality: True include_bots: True bot_fraction: 0.15 output_type: sessions ``` > **Related generators** > > * [Daily active users](daily_active_users) — the aggregate > engagement metric. > * [Poisson process](../stochastic/poisson_process) — a baseline > arrival model. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Daily active users Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/domain/daily_active_users.html Product-engagement metrics like daily active users have a recognizable shape: an underlying growth (or decay) trend, a strong weekly rhythm, and noise. `DailyActiveUsersGenerator` reproduces this for testing growth-analytics dashboards and anomaly detection on product metrics. > **The model** > > $y_t = \text{base}\,(1 + g)^{\,d(t)} \cdot w(t) + \text{boost}_t + \varepsilon_t, \qquad \text{boost decays at } (1-\delta)$ > > The level is `base_users * (1 + growth_rate)^day`, scaled on weekends > by `weekend_factor` (1.2 for gaming, 0.8 otherwise). With probability > `event_probability` per step an event adds `(impact - 1) * base` to a > boost that decays geometrically at `event_decay_rate`, where `impact` > is drawn between `event_impact_min` and `event_impact_max`. > Proportional Gaussian noise (`noise_std`) is added and the result is > clipped at zero. > > Set `growth_rate_std` to perturb the growth rate per series, and > `app_type` (`consumer`, `business`, `gaming`) to pick the defaults for > a product profile. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import DailyActiveUsersGenerator ``` ## App type `app_type` sets the engagement profile, mainly through the weekend multiplier: a consumer app dips slightly at weekends, a B2B app drops sharply, and a gaming app rises. The seed and base level are shared. ```python theme={null} consumer_df = DailyActiveUsersGenerator( engine="polars", min_length=120, max_length=120, freq="D", base_users=10000.0, growth_rate=0.002, app_type="consumer", seed=42, ).generate(n_series=1) business_df = DailyActiveUsersGenerator( engine="polars", min_length=120, max_length=120, freq="D", base_users=10000.0, growth_rate=0.002, app_type="business", seed=42, ).generate(n_series=1) gaming_df = DailyActiveUsersGenerator( engine="polars", min_length=120, max_length=120, freq="D", base_users=10000.0, growth_rate=0.002, app_type="gaming", seed=42, ).generate(n_series=1) panels = [ ("consumer", consumer_df), ("business (B2B)", business_df), ("gaming", gaming_df), ] fig, axes = plt.subplots(3, 1, figsize=(12, 7.5), sharex=True) for ax, (label, df) in zip(axes, panels): ax.plot(df["ds"].to_list(), df["y"].to_list(), alpha=0.85, linewidth=1) ax.set(ylabel="Active users", title=label) axes[-1].set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## Analyzing event impact Compare DAU on event days versus normal days to measure the lift from marketing events. ```python theme={null} event_days = consumer_df.filter(pl.col("event") == 1) if len(event_days) > 0: print("Days when events occurred:") print(event_days) avg_event_day = consumer_df.filter(pl.col("event") == 1)["y"].mean() avg_normal_day = consumer_df.filter(pl.col("event") == 0)["y"].mean() print(f"\nAverage DAU on event days: {avg_event_day:,.0f}") print(f"Average DAU on normal days: {avg_normal_day:,.0f}") print(f"Event day lift: {(avg_event_day/avg_normal_day - 1)*100:.1f}%") ``` ```text theme={null} Days when events occurred: shape: (4, 4) ┌───────────┬─────────────────────┬──────────────┬───────┐ │ unique_id ┆ ds ┆ y ┆ event │ │ --- ┆ --- ┆ --- ┆ --- │ │ cat ┆ datetime[ns] ┆ f64 ┆ i32 │ ╞═══════════╪═════════════════════╪══════════════╪═══════╡ │ 0 ┆ 2000-01-20 00:00:00 ┆ 10816.372729 ┆ 1 │ │ 0 ┆ 2000-03-22 00:00:00 ┆ 15008.853745 ┆ 1 │ │ 0 ┆ 2000-04-08 00:00:00 ┆ 21994.846532 ┆ 1 │ │ 0 ┆ 2000-04-20 00:00:00 ┆ 22292.652679 ┆ 1 │ └───────────┴─────────────────────┴──────────────┴───────┘ Average DAU on event days: 17,528 Average DAU on normal days: 12,089 Event day lift: 45.0% ``` ## Hourly active users The generator also supports sub-daily frequencies like hourly data. ```python theme={null} hourly_params = { "min_length": 168, "max_length": 168, "freq": "h", "app_type": "consumer", "base_users": 10000.0, "event_probability": 0.01, "noise_std": 0.08, "seed": 42, } hourly_gen = DailyActiveUsersGenerator(engine="polars", **hourly_params) hourly_df = hourly_gen.generate(n_series=1) print(f"Generated {len(hourly_df)} hourly observations") hourly_df.head(24) ``` ```text theme={null} Generated 168 hourly observations ``` | unique\_id | ds | y | event | | ---------- | ------------------- | ------------ | ----- | | cat | datetime\[ns] | f64 | i32 | | "0" | 2000-01-01 00:00:00 | 9275.243589 | 0 | | "0" | 2000-01-01 01:00:00 | 10272.524421 | 0 | | "0" | 2000-01-01 02:00:00 | 9468.136049 | 0 | | "0" | 2000-01-01 03:00:00 | 9727.297606 | 0 | | "0" | 2000-01-01 04:00:00 | 10144.538427 | 0 | | … | … | … | … | | "0" | 2000-01-01 19:00:00 | 13346.974807 | 1 | | "0" | 2000-01-01 20:00:00 | 12068.154452 | 0 | | "0" | 2000-01-01 21:00:00 | 10815.974473 | 0 | | "0" | 2000-01-01 22:00:00 | 11608.360831 | 0 | | "0" | 2000-01-01 23:00:00 | 10668.204221 | 0 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(hourly_df["ds"].to_list(), hourly_df["y"].to_list(), alpha=0.8, color="C4") ax.set_xlabel("Timestamp") ax.set_ylabel("Active Users") ax.set_title("Hourly active users — consumer app (1 week)") plt.tight_layout() plt.show() ``` ## Multiple products/apps Generate DAU data for multiple products simultaneously and compare event distributions. ```python theme={null} multi_params = { "min_length": 30, "max_length": 30, "freq": "D", "app_type": "consumer", "base_users": 20000.0, "event_probability": 0.03, "seed": 42, } multi_gen = DailyActiveUsersGenerator(engine="polars", **multi_params) multi_df = multi_gen.generate(n_series=5) print(f"Generated 5 products with {len(multi_df)} total observations") print("\nProduct 0 preview:") print(multi_df.filter(pl.col("unique_id") == "0").head(10)) total_events = multi_df.group_by("unique_id").agg(pl.col("event").sum()) print("\nEvents per product:") print(total_events) ``` ```text theme={null} Generated 5 products with 150 total observations Product 0 preview: shape: (10, 4) ┌───────────┬─────────────────────┬──────────────┬───────┐ │ unique_id ┆ ds ┆ y ┆ event │ │ --- ┆ --- ┆ --- ┆ --- │ │ cat ┆ datetime[ns] ┆ f64 ┆ i32 │ ╞═══════════╪═════════════════════╪══════════════╪═══════╡ │ 0 ┆ 2000-01-01 00:00:00 ┆ 19094.054486 ┆ 0 │ │ 0 ┆ 2000-01-02 00:00:00 ┆ 20350.825855 ┆ 0 │ │ 0 ┆ 2000-01-03 00:00:00 ┆ 19354.510065 ┆ 0 │ │ 0 ┆ 2000-01-04 00:00:00 ┆ 19688.625437 ┆ 0 │ │ 0 ┆ 2000-01-05 00:00:00 ┆ 20221.064661 ┆ 0 │ │ 0 ┆ 2000-01-06 00:00:00 ┆ 15882.965767 ┆ 0 │ │ 0 ┆ 2000-01-07 00:00:00 ┆ 17069.908021 ┆ 0 │ │ 0 ┆ 2000-01-08 00:00:00 ┆ 18599.75721 ┆ 0 │ │ 0 ┆ 2000-01-09 00:00:00 ┆ 21305.138222 ┆ 0 │ │ 0 ┆ 2000-01-10 00:00:00 ┆ 22202.888052 ┆ 0 │ └───────────┴─────────────────────┴──────────────┴───────┘ Events per product: shape: (5, 2) ┌───────────┬───────┐ │ unique_id ┆ event │ │ --- ┆ --- │ │ cat ┆ i32 │ ╞═══════════╪═══════╡ │ 3 ┆ 1 │ │ 1 ┆ 1 │ │ 0 ┆ 1 │ │ 2 ┆ 1 │ │ 4 ┆ 1 │ └───────────┴───────┘ ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.7) ax.set_xlabel("Timestamp") ax.set_ylabel("DAU") ax.set_title("Multiple products — daily active users") ax.legend() plt.tight_layout() plt.show() ``` ## Custom column names Customize the output column names to match your application’s naming conventions. ```python theme={null} custom_params = { "min_length": 30, "max_length": 30, "freq": "D", "base_users": 15000.0, "event_probability": 0.05, "event_col": "marketing_campaign", "id_col": "product_id", "time_col": "date", "target_col": "dau", "seed": 42, } custom_gen = DailyActiveUsersGenerator(engine="polars", **custom_params) custom_df = custom_gen.generate(n_series=1) print(f"Columns: {custom_df.columns}") custom_df.head(10) ``` ```text theme={null} Columns: ['product_id', 'date', 'dau', 'marketing_campaign'] ``` | product\_id | date | dau | marketing\_campaign | | ----------- | ------------------- | ------------ | ------------------- | | cat | datetime\[ns] | f64 | i32 | | "0" | 2000-01-01 00:00:00 | 14320.540864 | 0 | | "0" | 2000-01-02 00:00:00 | 15263.119391 | 0 | | "0" | 2000-01-03 00:00:00 | 14515.882549 | 0 | | "0" | 2000-01-04 00:00:00 | 14766.469078 | 0 | | "0" | 2000-01-05 00:00:00 | 15165.798496 | 0 | | "0" | 2000-01-06 00:00:00 | 11912.224325 | 0 | | "0" | 2000-01-07 00:00:00 | 12802.431016 | 0 | | "0" | 2000-01-08 00:00:00 | 18654.126716 | 1 | | "0" | 2000-01-09 00:00:00 | 18991.33087 | 0 | | "0" | 2000-01-10 00:00:00 | 18312.235462 | 0 | > **Related generators** > > * [Clickstream](clickstream) — the sessionized activity beneath > engagement metrics. > * [Seasonal](../statistical/seasonal) — the weekly rhythm in > isolation. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Energy load Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/domain/energy_load.html Electricity load has strong, *layered* seasonality — a daily usage rhythm nested inside a weekly one — plus consumer-driven shape variation. `EnergyLoadGenerator` reproduces these multi-seasonal profiles for grid-demand forecasting. > **The model** > > $y_t = \text{base} + d(t) + w(t) + a(t) + \beta\,|T_t - T_{\text{base}}| + \varepsilon_t$ > > The series combines daily and weekly cycles with a load shape and > noise appropriate to `load_type` — `residential` (sharp morning and > evening peaks) or `industrial` (flatter, weekday-driven). The > overlapping periods make it a good test of multi-seasonal models. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import EnergyLoadGenerator ``` ## 1. Load type `load_type` selects the daily profile: residential peaks morning and evening, commercial has one broad midday peak, and industrial is nearly flat with a night dip. One week of hourly data makes the daily and weekly shapes visible at once. ```python theme={null} residential_df = EnergyLoadGenerator( engine="polars", min_length=168, max_length=168, freq="h", base_load=100.0, load_type="residential", seed=42, ).generate(n_series=1) commercial_df = EnergyLoadGenerator( engine="polars", min_length=168, max_length=168, freq="h", base_load=100.0, load_type="commercial", seed=42, ).generate(n_series=1) industrial_df = EnergyLoadGenerator( engine="polars", min_length=168, max_length=168, freq="h", base_load=100.0, load_type="industrial", seed=42, ).generate(n_series=1) panels = [ ("residential", residential_df), ("commercial", commercial_df), ("industrial", industrial_df), ] fig, axes = plt.subplots(3, 1, figsize=(12, 7.5), sharex=True) for ax, (label, df) in zip(axes, panels): ax.plot(df["ds"].to_list(), df["y"].to_list(), alpha=0.85, linewidth=1) ax.set(ylabel="Load", title=label) axes[-1].set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## 2. Extreme weather Simulate load spikes during extreme weather events like heat waves or cold snaps. ```python theme={null} extreme_weather_params = { "min_length": 336, "max_length": 336, "freq": "h", "load_type": "residential", "base_load": 2.0, "temperature_sensitivity": 0.05, "extreme_weather_prob": 0.1, "extreme_weather_impact": 2.5, "seed": 42, } extreme_gen = EnergyLoadGenerator(engine="polars", **extreme_weather_params) extreme_df = extreme_gen.generate(n_series=1) print(f"Generated {len(extreme_df)} hourly observations with extreme weather events") print( f"Statistics: Mean={extreme_df['y'].mean():.2f} kW, " f"Min={extreme_df['y'].min():.2f} kW, Max={extreme_df['y'].max():.2f} kW" ) extreme_df.head(24) ``` ```text theme={null} Generated 336 hourly observations with extreme weather events Statistics: Mean=62.60 kW, Min=20.00 kW, Max=205.00 kW ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 50.647778 | | "0" | 2000-01-01 01:00:00 | 33.903193 | | "0" | 2000-01-01 02:00:00 | 45.368096 | | "0" | 2000-01-01 03:00:00 | 45.453387 | | "0" | 2000-01-01 04:00:00 | 50.819034 | | … | … | … | | "0" | 2000-01-01 19:00:00 | 76.554611 | | "0" | 2000-01-01 20:00:00 | 74.492959 | | "0" | 2000-01-01 21:00:00 | 65.825395 | | "0" | 2000-01-01 22:00:00 | 136.884119 | | "0" | 2000-01-01 23:00:00 | 43.017805 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(extreme_df["ds"].to_list(), extreme_df["y"].to_list(), alpha=0.8, color="C3") ax.set_xlabel("Timestamp") ax.set_ylabel("Load (kW)") ax.set_title("Residential load with extreme weather events (2 weeks)") plt.tight_layout() plt.show() ``` ## 3. Holiday effect Model reduced commercial demand during holidays and weekends. ```python theme={null} holiday_params = { "min_length": 336, "max_length": 336, "freq": "h", "load_type": "commercial", "base_load": 50.0, "temperature_sensitivity": 0.08, "holiday_effect": 0.3, "seed": 42, } holiday_gen = EnergyLoadGenerator(engine="polars", **holiday_params) holiday_df = holiday_gen.generate(n_series=1) print(f"Generated {len(holiday_df)} hourly observations with holiday/weekend effects") print( f"Statistics: Mean={holiday_df['y'].mean():.2f} kW, " f"Min={holiday_df['y'].min():.2f} kW, Max={holiday_df['y'].max():.2f} kW" ) holiday_df.head(24) ``` ```text theme={null} Generated 336 hourly observations with holiday/weekend effects Statistics: Mean=101.44 kW, Min=69.02 kW, Max=128.92 kW ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 99.763688 | | "0" | 2000-01-01 01:00:00 | 83.336362 | | "0" | 2000-01-01 02:00:00 | 94.98038 | | "0" | 2000-01-01 03:00:00 | 94.749185 | | "0" | 2000-01-01 04:00:00 | 97.870654 | | … | … | … | | "0" | 2000-01-01 19:00:00 | 102.963149 | | "0" | 2000-01-01 20:00:00 | 102.179314 | | "0" | 2000-01-01 21:00:00 | 101.246752 | | "0" | 2000-01-01 22:00:00 | 97.070383 | | "0" | 2000-01-01 23:00:00 | 91.96729 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(holiday_df["ds"].to_list(), holiday_df["y"].to_list(), alpha=0.8, color="C4") ax.set_xlabel("Timestamp") ax.set_ylabel("Load (kW)") ax.set_title("Commercial load with holiday effect (2 weeks)") plt.tight_layout() plt.show() ``` ## 4. Temperature sensitivity Simulate a household with heavy AC/heating usage that is very sensitive to temperature changes. ```python theme={null} high_temp_params = { "min_length": 168, "max_length": 168, "freq": "h", "load_type": "residential", "base_load": 3.0, "temperature_sensitivity": 0.15, "seed": 42, } high_temp_gen = EnergyLoadGenerator(engine="polars", **high_temp_params) high_temp_df = high_temp_gen.generate(n_series=1) print(f"Generated {len(high_temp_df)} hourly observations with high temperature sensitivity") print( f"Statistics: Mean={high_temp_df['y'].mean():.2f} kW, " f"Min={high_temp_df['y'].min():.2f} kW, Max={high_temp_df['y'].max():.2f} kW" ) high_temp_df.head(24) ``` ```text theme={null} Generated 168 hourly observations with high temperature sensitivity Statistics: Mean=56.94 kW, Min=20.36 kW, Max=99.35 kW ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 40.255913 | | "0" | 2000-01-01 01:00:00 | 46.889278 | | "0" | 2000-01-01 02:00:00 | 48.506536 | | "0" | 2000-01-01 03:00:00 | 51.234452 | | "0" | 2000-01-01 04:00:00 | 51.012296 | | … | … | … | | "0" | 2000-01-01 19:00:00 | 83.921064 | | "0" | 2000-01-01 20:00:00 | 81.484842 | | "0" | 2000-01-01 21:00:00 | 63.461836 | | "0" | 2000-01-01 22:00:00 | 58.506393 | | "0" | 2000-01-01 23:00:00 | 61.645597 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(high_temp_df["ds"].to_list(), high_temp_df["y"].to_list(), alpha=0.8, color="C5") ax.set_xlabel("Timestamp") ax.set_ylabel("Load (kW)") ax.set_title("Residential load with high temperature sensitivity (1 week)") plt.tight_layout() plt.show() ``` ## 5. Multiple customers Generate load profiles for 5 residential customers simultaneously. ```python theme={null} multi_params = { "min_length": 168, "max_length": 168, "freq": "h", "load_type": "residential", "base_load": 2.5, "temperature_sensitivity": 0.05, "seed": 42, } multi_gen = EnergyLoadGenerator(engine="polars", **multi_params) multi_df = multi_gen.generate(n_series=5) print(f"Generated 5 residential customers with {len(multi_df)} total observations") print( f"Overall Statistics: Mean={multi_df['y'].mean():.2f} kW, " f"Total Load={multi_df.group_by('ds').agg(pl.col('y').sum()).select('y').mean().item():.2f} kW" ) multi_df.filter(pl.col("unique_id") == "0").head(24) ``` ```text theme={null} Generated 5 residential customers with 840 total observations Overall Statistics: Mean=55.30 kW, Total Load=276.52 kW ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 37.975594 | | "0" | 2000-01-01 01:00:00 | 44.725126 | | "0" | 2000-01-01 02:00:00 | 46.764535 | | "0" | 2000-01-01 03:00:00 | 49.44903 | | "0" | 2000-01-01 04:00:00 | 49.162387 | | … | … | … | | "0" | 2000-01-01 19:00:00 | 82.712299 | | "0" | 2000-01-01 20:00:00 | 79.705961 | | "0" | 2000-01-01 21:00:00 | 61.551004 | | "0" | 2000-01-01 22:00:00 | 56.473929 | | "0" | 2000-01-01 23:00:00 | 59.725813 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.7) ax.set_xlabel("Timestamp") ax.set_ylabel("Load (kW)") ax.set_title("Multiple residential customers (1 week)") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Seasonal](../statistical/seasonal) — a single seasonal cycle. > * [SARIMA](../statistical/sarima) — seasonal ARMA dynamics. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Intermittent demand Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/domain/intermittent_demand.html Intermittent demand is *sparse*: long runs of zeros punctuated by occasional nonzero orders — the reality for spare parts and slow-moving SKUs. Standard forecasters trained on smooth series fail here, which makes this generator the test bed for Croston-type methods and zero-inflated models. > **The model** > > $y_t = b_t \cdot z_t, \qquad b_t \sim \mathrm{Bernoulli}(p), \qquad z_t \sim \text{size distribution}$ > > Nonzero demands are separated by random gaps, with order sizes drawn > independently. `intermittent_pattern` shapes *when* orders occur — > `random`, `clustered` (bursts of demand), or `seasonal` (periodic > demand windows). ```python theme={null} import numpy as np import polars as pl import matplotlib.pyplot as plt from synforecast.generators import IntermittentDemandGenerator ``` ## 1. Occurrence pattern `intermittent_pattern` decides *when* demand happens, holding the size distribution fixed. Random gives i.i.d. Bernoulli periods, clustered groups them into runs, and seasonal concentrates them in recurring windows. ```python theme={null} random_df = IntermittentDemandGenerator( engine="polars", min_length=180, max_length=180, freq="D", demand_probability=0.2, demand_distribution="poisson", demand_mean=10.0, intermittent_pattern="random", seed=42, ).generate(n_series=1) clustered_df = IntermittentDemandGenerator( engine="polars", min_length=180, max_length=180, freq="D", demand_probability=0.2, demand_distribution="poisson", demand_mean=10.0, intermittent_pattern="clustered", cluster_size=5, seed=42, ).generate(n_series=1) seasonal_df = IntermittentDemandGenerator( engine="polars", min_length=180, max_length=180, freq="D", demand_probability=0.2, demand_distribution="poisson", demand_mean=10.0, intermittent_pattern="seasonal", seasonal_period=30, seed=42, ).generate(n_series=1) fig, axes = plt.subplots(3, 1, figsize=(12, 7.5), sharex=True) panels = [("random", random_df), ("clustered", clustered_df), ("seasonal", seasonal_df)] for ax, (label, df) in zip(axes, panels): values = df["y"].to_list() nonzero = sum(1 for value in values if value > 0) ax.stem(df["ds"].to_list(), values, linefmt="C0-", markerfmt="C0o", basefmt="k-") ax.set(ylabel="Demand", title=f"{label} ({nonzero / len(values):.0%} of periods non-zero)") axes[-1].set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## 2. Demand size distributions Compare Poisson, negative binomial, lognormal, and gamma distributions for demand sizes. ```python theme={null} distributions = ["poisson", "negative_binomial", "lognormal", "gamma"] results = {} for dist in distributions: params = { "min_length": 300, "max_length": 300, "freq": "D", "demand_probability": 0.3, "demand_distribution": dist, "demand_mean": 7.0, "demand_std": 3.0, "seed": 789, } gen = IntermittentDemandGenerator(engine="polars", **params) df = gen.generate(n_series=1) non_zero = df.filter(pl.col("y") > 0)["y"].to_numpy() if len(non_zero) > 0: results[dist] = { "count": len(non_zero), "mean": non_zero.mean(), "std": non_zero.std(), "min": non_zero.min(), "max": non_zero.max(), } print(f"Distribution comparison (when demand > 0):") print(f"{'Distribution':<20} {'Count':<8} {'Mean':<8} {'Std':<8} {'Min':<8} {'Max':<8}") print("-" * 60) for dist, stats in results.items(): print( f"{dist:<20} {stats['count']:<8} {stats['mean']:<8.2f} {stats['std']:<8.2f} {stats['min']:<8.0f} {stats['max']:<8.0f}" ) ``` ```text theme={null} Distribution comparison (when demand > 0): Distribution Count Mean Std Min Max ------------------------------------------------------------ poisson 86 6.80 2.48 3 15 negative_binomial 86 6.43 2.82 1 15 lognormal 86 6.55 2.62 2 18 gamma 86 6.47 2.73 1 17 ``` ## 3. Bulk orders Simulate rare but large orders with a minimum order quantity constraint. ```python theme={null} params_bulk = { "min_length": 200, "max_length": 200, "freq": "D", "demand_probability": 0.1, "demand_distribution": "gamma", "demand_mean": 50.0, "demand_std": 20.0, "min_demand": 20, "seed": 999, } gen_bulk = IntermittentDemandGenerator(engine="polars", **params_bulk) df_bulk = gen_bulk.generate(n_series=1) print(f"Generated {len(df_bulk)} daily observations") print(f"Minimum order quantity: {params_bulk['min_demand']}") non_zero_bulk = df_bulk.filter(pl.col("y") > 0)["y"].to_numpy() if len(non_zero_bulk) > 0: print(f"\nBulk order statistics:") print(f" Number of orders: {len(non_zero_bulk)}") print(f" Mean order size: {non_zero_bulk.mean():.2f}") print(f" Min order size: {non_zero_bulk.min():.0f}") print(f" Max order size: {non_zero_bulk.max():.0f}") print(f" All orders >= minimum: {np.all(non_zero_bulk >= params_bulk['min_demand'])}") df_bulk.head(20) ``` ```text theme={null} Generated 200 daily observations Minimum order quantity: 20 Bulk order statistics: Number of orders: 24 Mean order size: 50.34 Min order size: 24 Max order size: 92 All orders >= minimum: True ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 36.316717 | | "0" | 2000-01-02 00:00:00 | 0.0 | | "0" | 2000-01-03 00:00:00 | 54.317364 | | "0" | 2000-01-04 00:00:00 | 0.0 | | "0" | 2000-01-05 00:00:00 | 0.0 | | … | … | … | | "0" | 2000-01-16 00:00:00 | 0.0 | | "0" | 2000-01-17 00:00:00 | 0.0 | | "0" | 2000-01-18 00:00:00 | 0.0 | | "0" | 2000-01-19 00:00:00 | 0.0 | | "0" | 2000-01-20 00:00:00 | 0.0 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) series = df_bulk.filter(pl.col("unique_id") == "0") ax.stem(series["ds"].to_list(), series["y"].to_list(), linefmt="C3-", markerfmt="C3o", basefmt="k-") ax.set_xlabel("Timestamp") ax.set_ylabel("Demand") ax.set_title("Bulk orders with minimum demand") plt.tight_layout() plt.show() ``` ## 4. Multiple series Generate multiple independent intermittent demand series and compare their statistics. ```python theme={null} params_multi = { "min_length": 100, "max_length": 100, "freq": "D", "demand_probability": 0.25, "demand_distribution": "poisson", "demand_mean": 6.0, "seed": 1234, } gen_multi = IntermittentDemandGenerator(engine="polars", **params_multi) df_multi = gen_multi.generate(n_series=3) print(f"Generated 3 intermittent demand series") print(f"Total rows: {len(df_multi)}") print(f"Unique series IDs: {df_multi['unique_id'].unique().to_list()}") series_stats = ( df_multi.group_by("unique_id") .agg( [ (pl.col("y") == 0).sum().alias("zero_days"), (pl.col("y") > 0).sum().alias("demand_days"), pl.col("y").sum().alias("total_demand"), pl.col("y") .filter(pl.col("y") > 0) .mean() .alias("avg_demand_when_nonzero"), ] ) .sort("unique_id") ) series_stats ``` ```text theme={null} Generated 3 intermittent demand series Total rows: 300 Unique series IDs: ['0', '1', '2'] ``` | unique\_id | zero\_days | demand\_days | total\_demand | avg\_demand\_when\_nonzero | | ---------- | ---------- | ------------ | ------------- | -------------------------- | | cat | u32 | u32 | f64 | f64 | | "0" | 83 | 17 | 82.0 | 4.823529 | | "1" | 78 | 22 | 142.0 | 6.454545 | | "2" | 69 | 31 | 167.0 | 5.387097 | ```python theme={null} fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True) for ax, uid in zip(axes, df_multi["unique_id"].unique().to_list()): series = df_multi.filter(pl.col("unique_id") == uid) ax.stem(series["ds"].to_list(), series["y"].to_list(), linefmt="C0-", markerfmt="C0o", basefmt="k-") ax.set_ylabel("Demand") ax.set_title(uid) axes[-1].set_xlabel("Timestamp") plt.suptitle("Multiple intermittent demand series", fontsize=14) plt.tight_layout() plt.show() ``` > **Related generators** > > * [INAR](../statistical/inar) — autocorrelated integer counts. > * [Poisson process](../stochastic/poisson_process) — event arrivals > over time. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # IoT sensor Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/domain/iot_sensor.html A real sensor feed is more than a clean signal: it drifts as the device ages, grows noisier as the battery drains, and occasionally fails. `IoTSensorGenerator` reproduces these artifacts so you can test monitoring and anomaly-detection pipelines against realistic device behavior. > **The model** > > A baseline signal is corrupted by calibration `drift_rate`, > `battery_degradation_rate`, and intermittent or permanent faults set > by `failure_probability` and `failure_type`. Leave them at zero for a > healthy sensor, or turn them up to simulate a degrading or failing > one. ```python theme={null} import numpy as np import polars as pl import matplotlib.pyplot as plt from synforecast.generators import IoTSensorGenerator ``` ## 1. Temperature sensor with gradual drift Simulate a temperature sensor that gradually drifts over time due to calibration issues. ```python theme={null} params_temp = { "min_length": 500, "max_length": 500, "freq": "min", "n_sensors": 1, "sensor_type": "temperature", "base_value": 22.0, "drift_rate": 0.002, "measurement_noise": 0.1, "calibration_error": 0.5, "seed": 42, } gen_temp = IoTSensorGenerator(engine="polars", **params_temp) df_temp = gen_temp.generate(n_series=1) print(f"Generated {len(df_temp)} temperature readings") df_temp.head(20) ``` ```text theme={null} Generated 500 temperature readings ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 22.359841 | | "0" | 2000-01-01 00:01:00 | 22.588446 | | "0" | 2000-01-01 00:02:00 | 22.448393 | | "0" | 2000-01-01 00:03:00 | 22.553855 | | "0" | 2000-01-01 00:04:00 | 22.509143 | | … | … | … | | "0" | 2000-01-01 00:15:00 | 22.626105 | | "0" | 2000-01-01 00:16:00 | 22.531311 | | "0" | 2000-01-01 00:17:00 | 22.691271 | | "0" | 2000-01-01 00:18:00 | 22.582547 | | "0" | 2000-01-01 00:19:00 | 22.36304 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(df_temp["ds"].to_list(), df_temp["y"].to_list(), alpha=0.8, linewidth=0.7) ax.set_xlabel("Timestamp") ax.set_ylabel("Temperature (C)") ax.set_title("Temperature sensor with gradual drift") plt.tight_layout() plt.show() ``` ```python theme={null} values = df_temp["y"].to_numpy() print(f"Temperature statistics:") print(f" Mean: {np.mean(values):.2f} C") print(f" Min: {np.min(values):.2f} C") print(f" Max: {np.max(values):.2f} C") print(f" Std: {np.std(values):.2f} C") first_100 = np.mean(values[:100]) last_100 = np.mean(values[-100:]) print(f" Drift: {last_100 - first_100:.2f} C (first 100 vs last 100)") ``` ```text theme={null} Temperature statistics: Mean: 23.14 C Min: 22.34 C Max: 24.00 C Std: 0.39 C Drift: 1.02 C (first 100 vs last 100) ``` ## 2. Humidity sensor with daily cycle Generate 24 hours of humidity readings at 1-minute intervals with a daily seasonal pattern. ```python theme={null} params_humidity = { "min_length": 1440, "max_length": 1440, "freq": "min", "n_sensors": 1, "sensor_type": "humidity", "base_value": 60.0, "seasonal_period": 1440, "seasonal_amplitude": 15.0, "measurement_noise": 1.0, "seed": 123, } gen_humidity = IoTSensorGenerator(engine="polars", **params_humidity) df_humidity = gen_humidity.generate(n_series=1) print(f"Generated 24 hours of humidity readings (1-minute intervals)") values_humidity = df_humidity["y"].to_numpy() print(f"\nHumidity statistics:") print(f" Mean: {np.mean(values_humidity):.1f}%") print(f" Min: {np.min(values_humidity):.1f}%") print(f" Max: {np.max(values_humidity):.1f}%") df_humidity.filter(pl.col("ds").dt.minute() == 0).filter( pl.col("ds").dt.hour() % 2 == 0 ).head(12) ``` ```text theme={null} Generated 24 hours of humidity readings (1-minute intervals) Humidity statistics: Mean: 60.3% Min: 43.1% Max: 76.6% ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 58.351737 | | "0" | 2000-01-01 02:00:00 | 67.881279 | | "0" | 2000-01-01 04:00:00 | 71.417134 | | "0" | 2000-01-01 06:00:00 | 75.064438 | | "0" | 2000-01-01 08:00:00 | 70.703289 | | … | … | … | | "0" | 2000-01-01 14:00:00 | 55.208747 | | "0" | 2000-01-01 16:00:00 | 47.778869 | | "0" | 2000-01-01 18:00:00 | 44.222302 | | "0" | 2000-01-01 20:00:00 | 49.031773 | | "0" | 2000-01-01 22:00:00 | 53.124016 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(df_humidity["ds"].to_list(), df_humidity["y"].to_list(), alpha=0.8, linewidth=0.5, color="C1") ax.set_xlabel("Timestamp") ax.set_ylabel("Humidity (%)") ax.set_title("Humidity sensor with daily cycle") plt.tight_layout() plt.show() ``` ## 3. Pressure sensor with battery degradation Simulate a pressure sensor where measurement quality degrades as battery life decreases. ```python theme={null} params_pressure = { "min_length": 400, "max_length": 400, "freq": "min", "n_sensors": 1, "sensor_type": "pressure", "base_value": 1013.25, "measurement_noise": 0.5, "battery_life": 200, "battery_degradation_rate": 0.005, "seed": 456, } gen_pressure = IoTSensorGenerator(engine="polars", **params_pressure) df_pressure = gen_pressure.generate(n_series=1) print(f"Generated {len(df_pressure)} pressure readings") print(f"Battery degradation starts at reading 200") values_pressure = df_pressure["y"].to_numpy() first_half_std = np.std(values_pressure[:200]) second_half_std = np.std(values_pressure[200:]) print(f"\nPressure quality:") print(f" Before battery degradation (readings 0-200):") print(f" Std: {first_half_std:.2f} hPa") print(f" After battery degradation (readings 200-400):") print(f" Std: {second_half_std:.2f} hPa") print(f" Quality degradation: {((second_half_std/first_half_std - 1) * 100):.1f}% increase in noise") ``` ```text theme={null} Generated 400 pressure readings Battery degradation starts at reading 200 Pressure quality: Before battery degradation (readings 0-200): Std: 0.54 hPa After battery degradation (readings 200-400): Std: 29.20 hPa Quality degradation: 5341.6% increase in noise ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(df_pressure["ds"].to_list(), df_pressure["y"].to_list(), alpha=0.8, linewidth=0.7, color="C2") ax.axvline(x=df_pressure["ds"].to_list()[200], color="red", linestyle="--", alpha=0.5, label="Battery degradation start") ax.set_xlabel("Timestamp") ax.set_ylabel("Pressure (hPa)") ax.set_title("Pressure sensor with battery degradation") ax.legend() plt.tight_layout() plt.show() ``` ## 4. Light sensor with intermittent failures Simulate a light sensor that experiences random intermittent outages. ```python theme={null} params_light = { "min_length": 300, "max_length": 300, "freq": "1s", "n_sensors": 1, "sensor_type": "light", "base_value": 800.0, "measurement_noise": 10.0, "failure_probability": 0.05, "failure_type": "intermittent", "failure_duration": 5, "seed": 789, } gen_light = IoTSensorGenerator(engine="polars", **params_light) df_light = gen_light.generate(n_series=1) print(f"Generated {len(df_light)} light readings (1-second intervals)") values_light = df_light["y"].to_numpy() nan_count = np.sum(np.isnan(values_light)) valid_count = len(values_light) - nan_count print(f"\nFailure statistics:") print(f" Valid readings: {valid_count} ({valid_count/len(values_light)*100:.1f}%)") print(f" Failed readings: {nan_count} ({nan_count/len(values_light)*100:.1f}%)") df_light.head(50) ``` ```text theme={null} Generated 300 light readings (1-second intervals) Failure statistics: Valid readings: 210 (70.0%) Failed readings: 90 (30.0%) ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 794.117333 | | "0" | 2000-01-01 00:00:01 | 817.596792 | | "0" | 2000-01-01 00:00:02 | 812.608478 | | "0" | 2000-01-01 00:00:03 | 775.054655 | | "0" | 2000-01-01 00:00:04 | 786.852539 | | … | … | … | | "0" | 2000-01-01 00:00:45 | 800.829929 | | "0" | 2000-01-01 00:00:46 | 805.540488 | | "0" | 2000-01-01 00:00:47 | 806.466231 | | "0" | 2000-01-01 00:00:48 | 797.781416 | | "0" | 2000-01-01 00:00:49 | 797.409746 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(df_light["ds"].to_list(), df_light["y"].to_list(), alpha=0.8, linewidth=0.7, color="C4") ax.set_xlabel("Timestamp") ax.set_ylabel("Light (lux)") ax.set_title("Light sensor with intermittent failures") plt.tight_layout() plt.show() ``` ## 5. Temperature sensor network (multivariate) Generate a network of 4 spatially correlated temperature sensors. ```python theme={null} params_network = { "min_length": 200, "max_length": 200, "freq": "min", "n_sensors": 4, "sensor_type": "temperature", "base_value": 20.0, "spatial_correlation": 0.7, "measurement_noise": 0.5, "seed": 1011, } gen_network = IoTSensorGenerator(engine="polars", **params_network) df_network = gen_network.generate(n_series=1) print(f"Generated sensor network with {df_network['unique_id'].n_unique()} sensors") print(f"Total readings: {len(df_network)}") df_network.head(30) ``` ```text theme={null} Generated sensor network with 4 sensors Total readings: 800 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 20.452877 | | "0" | 2000-01-01 00:01:00 | 19.287093 | | "0" | 2000-01-01 00:02:00 | 19.120498 | | "0" | 2000-01-01 00:03:00 | 21.071625 | | "0" | 2000-01-01 00:04:00 | 20.195943 | | … | … | … | | "0" | 2000-01-01 00:25:00 | 19.595728 | | "0" | 2000-01-01 00:26:00 | 20.554394 | | "0" | 2000-01-01 00:27:00 | 19.770506 | | "0" | 2000-01-01 00:28:00 | 19.758732 | | "0" | 2000-01-01 00:29:00 | 20.05044 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_network["unique_id"].unique().to_list(): series = df_network.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8, linewidth=0.7) ax.set_xlabel("Timestamp") ax.set_ylabel("Temperature (C)") ax.set_title("Temperature sensor network (4 spatially correlated sensors)") ax.legend() plt.tight_layout() plt.show() ``` ```python theme={null} sensor_0 = df_network.filter(pl.col("unique_id") == "0")["y"].to_numpy() sensor_1 = df_network.filter(pl.col("unique_id") == "1")["y"].to_numpy() sensor_2 = df_network.filter(pl.col("unique_id") == "2")["y"].to_numpy() sensor_3 = df_network.filter(pl.col("unique_id") == "3")["y"].to_numpy() corr_01 = np.corrcoef(sensor_0, sensor_1)[0, 1] corr_12 = np.corrcoef(sensor_1, sensor_2)[0, 1] corr_23 = np.corrcoef(sensor_2, sensor_3)[0, 1] corr_03 = np.corrcoef(sensor_0, sensor_3)[0, 1] print(f"Spatial correlation between sensors:") print(f" Sensor 0 <-> Sensor 1 (adjacent): {corr_01:.3f}") print(f" Sensor 1 <-> Sensor 2 (adjacent): {corr_12:.3f}") print(f" Sensor 2 <-> Sensor 3 (adjacent): {corr_23:.3f}") print(f" Sensor 0 <-> Sensor 3 (distant): {corr_03:.3f}") ``` ```text theme={null} Spatial correlation between sensors: Sensor 0 <-> Sensor 1 (adjacent): 0.588 Sensor 1 <-> Sensor 2 (adjacent): 0.683 Sensor 2 <-> Sensor 3 (adjacent): 0.668 Sensor 0 <-> Sensor 3 (distant): 0.338 ``` ## 6. Motion sensor with complete failure Simulate a motion sensor that may experience a complete failure, after which all readings are lost. ```python theme={null} params_motion = { "min_length": 200, "max_length": 200, "freq": "100ms", "n_sensors": 1, "sensor_type": "motion", "base_value": 0.5, "measurement_noise": 0.2, "failure_probability": 0.3, "failure_type": "complete", "seed": 1213, } gen_motion = IoTSensorGenerator(engine="polars", **params_motion) df_motion = gen_motion.generate(n_series=1) print(f"Generated {len(df_motion)} motion sensor readings (100ms intervals)") values_motion = df_motion["y"].to_numpy() nan_count_motion = np.sum(np.isnan(values_motion)) if nan_count_motion > 0: first_nan = np.where(np.isnan(values_motion))[0][0] print(f"\nSensor failed at reading {first_nan}") print(f"Valid readings before failure: {first_nan}") print(f"Failed readings after failure: {nan_count_motion}") else: print(f"\nSensor operated normally (no failure occurred)") ``` ```text theme={null} Generated 200 motion sensor readings (100ms intervals) Sensor failed at reading 0 Valid readings before failure: 0 Failed readings after failure: 200 ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(df_motion["ds"].to_list(), df_motion["y"].to_list(), alpha=0.8, linewidth=0.7, color="C5") ax.set_xlabel("Timestamp") ax.set_ylabel("Motion") ax.set_title("Motion sensor with complete failure") plt.tight_layout() plt.show() ``` ## 7. Multiple independent temperature sensors Generate multiple independent sensors using the univariate mode. ```python theme={null} params_multi = { "min_length": 100, "max_length": 100, "freq": "min", "n_sensors": 1, "sensor_type": "temperature", "base_value": 20.0, "measurement_noise": 0.3, "seed": 1415, } gen_multi = IoTSensorGenerator(engine="polars", **params_multi) df_multi = gen_multi.generate(n_series=3) print(f"Generated {df_multi['unique_id'].n_unique()} independent sensors") print(f"\nStatistics per sensor:") for series_id in df_multi["unique_id"].unique().sort(): series_df = df_multi.filter(pl.col("unique_id") == series_id) values_series = series_df["y"].to_numpy() print( f" {series_id}: Mean={np.mean(values_series):.2f} C, Std={np.std(values_series):.2f} C" ) ``` ```text theme={null} Generated 3 independent sensors Statistics per sensor: 0: Mean=20.01 C, Std=0.35 C 1: Mean=19.96 C, Std=0.27 C 2: Mean=19.95 C, Std=0.28 C ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_multi["unique_id"].unique().to_list(): series = df_multi.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8, linewidth=0.7) ax.set_xlabel("Timestamp") ax.set_ylabel("Temperature (C)") ax.set_title("Multiple independent temperature sensors") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Anomalies](../../capabilities/anomalies) — inject labelled > outliers on top of any generator. > * [Missingness](../../capabilities/missingness) — dropout gaps from > outages. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # State space Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/domain/state_space.html A state-space model separates a hidden *state* that evolves over time from the *observations* it emits. It is the general framework underlying ARIMA, ETS, and Kalman filtering; `StateSpaceGenerator` lets you specify the transition and observation dynamics directly to build custom linear systems. > **The model** > > $x_t = F x_{t-1} + w_t, \quad w_t \sim (0, Q), \qquad y_t = H x_t + v_t, \quad v_t \sim \mathcal{N}(0, R)$ > > A latent state evolves by a linear transition with process noise, and > is mapped to the observed series by an observation equation with > measurement noise. Choosing these dynamics reproduces local-level, > local-trend, and other structural time-series models. ```python theme={null} import numpy as np import polars as pl import matplotlib.pyplot as plt from synforecast.generators import StateSpaceGenerator ``` ## 1. Basic local level model (random walk with noise) The simplest state space model: a hidden random walk observed with measurement noise. ```python theme={null} local_level_params = { "min_length": 200, "max_length": 200, "freq": "D", "state_dim": 1, "obs_dim": 1, "seed": 42, } local_level_gen = StateSpaceGenerator(engine="polars", **local_level_params) local_level_df = local_level_gen.generate(n_series=1) print(f"Generated {len(local_level_df)} observations from local level model") print( f"Statistics: Mean={local_level_df['y'].mean():.4f}, " f"Std={local_level_df['y'].std():.4f}" ) local_level_df.head(10) ``` ```text theme={null} Generated 200 observations from local level model Statistics: Mean=-0.0235, Std=0.4556 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -1.477252 | | "0" | 2000-01-02 00:00:00 | 0.466308 | | "0" | 2000-01-03 00:00:00 | -0.351479 | | "0" | 2000-01-04 00:00:00 | -0.460238 | | "0" | 2000-01-05 00:00:00 | -0.088038 | | "0" | 2000-01-06 00:00:00 | -0.199117 | | "0" | 2000-01-07 00:00:00 | 0.23307 | | "0" | 2000-01-08 00:00:00 | -0.331227 | | "0" | 2000-01-09 00:00:00 | -0.634035 | | "0" | 2000-01-10 00:00:00 | -0.79934 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(local_level_df["ds"].to_list(), local_level_df["y"].to_list(), alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Local level model (random walk with noise)") plt.tight_layout() plt.show() ``` ## 2. 2D state space model (level + trend) A two-dimensional state captures both the level and its trend (rate of change). ```python theme={null} two_dim_params = { "min_length": 200, "max_length": 200, "freq": "D", "state_dim": 2, "obs_dim": 1, "seed": 42, } two_dim_gen = StateSpaceGenerator(engine="polars", **two_dim_params) two_dim_df = two_dim_gen.generate(n_series=1) print(f"Generated {len(two_dim_df)} observations from 2D state space model") print( f"Statistics: Mean={two_dim_df['y'].mean():.4f}, " f"Std={two_dim_df['y'].std():.4f}" ) two_dim_df.head(10) ``` ```text theme={null} Generated 200 observations from 2D state space model Statistics: Mean=-0.0315, Std=0.4513 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 2.035213 | | "0" | 2000-01-02 00:00:00 | 0.342724 | | "0" | 2000-01-03 00:00:00 | -0.130302 | | "0" | 2000-01-04 00:00:00 | -0.445831 | | "0" | 2000-01-05 00:00:00 | 0.159239 | | "0" | 2000-01-06 00:00:00 | -0.206335 | | "0" | 2000-01-07 00:00:00 | 0.401635 | | "0" | 2000-01-08 00:00:00 | -0.350341 | | "0" | 2000-01-09 00:00:00 | 0.077724 | | "0" | 2000-01-10 00:00:00 | -0.512018 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(two_dim_df["ds"].to_list(), two_dim_df["y"].to_list(), alpha=0.8, color="C1") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("2D state space model (level + trend)") plt.tight_layout() plt.show() ``` ## 3. Custom transition matrix (AR-like behavior) Specify a custom state transition matrix to create AR-like dynamics in the hidden state. ```python theme={null} transition_matrix = np.array([[0.9, 0.1], [0.0, 0.8]]) custom_transition_params = { "min_length": 200, "max_length": 200, "freq": "D", "state_dim": 2, "obs_dim": 1, "transition_matrix": transition_matrix.tolist(), "seed": 42, } custom_transition_gen = StateSpaceGenerator(engine="polars", **custom_transition_params) custom_transition_df = custom_transition_gen.generate(n_series=1) print(f"Generated {len(custom_transition_df)} observations with custom transition matrix") print( f"Statistics: Mean={custom_transition_df['y'].mean():.4f}, " f"Std={custom_transition_df['y'].std():.4f}" ) custom_transition_df.head(10) ``` ```text theme={null} Generated 200 observations with custom transition matrix Statistics: Mean=0.2258, Std=0.7764 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -1.099579 | | "0" | 2000-01-02 00:00:00 | -1.454542 | | "0" | 2000-01-03 00:00:00 | -0.964621 | | "0" | 2000-01-04 00:00:00 | -1.002322 | | "0" | 2000-01-05 00:00:00 | -1.355461 | | "0" | 2000-01-06 00:00:00 | -0.768567 | | "0" | 2000-01-07 00:00:00 | -1.013438 | | "0" | 2000-01-08 00:00:00 | -0.786117 | | "0" | 2000-01-09 00:00:00 | 0.361217 | | "0" | 2000-01-10 00:00:00 | -0.043312 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(custom_transition_df["ds"].to_list(), custom_transition_df["y"].to_list(), alpha=0.8, color="C2") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Custom transition matrix (AR-like behavior)") plt.tight_layout() plt.show() ``` ## 4. Custom observation matrix (weighted state observation) Observe a weighted combination of the hidden states. ```python theme={null} observation_matrix = np.array([[1.0, 0.5]]) custom_obs_params = { "min_length": 200, "max_length": 200, "freq": "D", "state_dim": 2, "obs_dim": 1, "observation_matrix": observation_matrix.tolist(), "seed": 42, } custom_obs_gen = StateSpaceGenerator(engine="polars", **custom_obs_params) custom_obs_df = custom_obs_gen.generate(n_series=1) print(f"Generated {len(custom_obs_df)} observations with custom observation matrix") print( f"Statistics: Mean={custom_obs_df['y'].mean():.4f}, " f"Std={custom_obs_df['y'].std():.4f}" ) custom_obs_df.head(10) ``` ```text theme={null} Generated 200 observations with custom observation matrix Statistics: Mean=-0.0163, Std=0.4940 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 2.24795 | | "0" | 2000-01-02 00:00:00 | 0.496252 | | "0" | 2000-01-03 00:00:00 | 0.135206 | | "0" | 2000-01-04 00:00:00 | -0.172047 | | "0" | 2000-01-05 00:00:00 | 0.132115 | | "0" | 2000-01-06 00:00:00 | -0.300323 | | "0" | 2000-01-07 00:00:00 | 0.495517 | | "0" | 2000-01-08 00:00:00 | -0.444273 | | "0" | 2000-01-09 00:00:00 | -0.004085 | | "0" | 2000-01-10 00:00:00 | -0.554032 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(custom_obs_df["ds"].to_list(), custom_obs_df["y"].to_list(), alpha=0.8, color="C3") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Custom observation matrix (weighted state observation)") plt.tight_layout() plt.show() ``` ## 5. High process noise (volatile state evolution) Increase the process noise covariance to create more volatile hidden state dynamics. ```python theme={null} high_process_noise = np.array([[5.0, 0.0], [0.0, 5.0]]) high_noise_params = { "min_length": 200, "max_length": 200, "freq": "D", "state_dim": 2, "obs_dim": 1, "state_covariance": high_process_noise.tolist(), "seed": 42, } high_noise_gen = StateSpaceGenerator(engine="polars", **high_noise_params) high_noise_df = high_noise_gen.generate(n_series=1) print(f"Generated {len(high_noise_df)} observations with high process noise") print( f"Statistics: Mean={high_noise_df['y'].mean():.4f}, " f"Std={high_noise_df['y'].std():.4f}" ) high_noise_df.head(10) ``` ```text theme={null} Generated 200 observations with high process noise Statistics: Mean=-0.1518, Std=2.2105 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 2.035213 | | "0" | 2000-01-02 00:00:00 | 2.980453 | | "0" | 2000-01-03 00:00:00 | 2.307622 | | "0" | 2000-01-04 00:00:00 | -1.815772 | | "0" | 2000-01-05 00:00:00 | 1.32836 | | "0" | 2000-01-06 00:00:00 | -2.890637 | | "0" | 2000-01-07 00:00:00 | -0.526938 | | "0" | 2000-01-08 00:00:00 | -2.710086 | | "0" | 2000-01-09 00:00:00 | -1.37662 | | "0" | 2000-01-10 00:00:00 | -1.535066 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(high_noise_df["ds"].to_list(), high_noise_df["y"].to_list(), alpha=0.8, color="C4") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("High process noise (volatile state evolution)") plt.tight_layout() plt.show() ``` ## 6. High observation noise (noisy measurements) Increase the observation noise to simulate noisy measurement conditions. ```python theme={null} high_obs_noise = np.array([[10.0]]) noisy_obs_params = { "min_length": 200, "max_length": 200, "freq": "D", "state_dim": 2, "obs_dim": 1, "obs_covariance": high_obs_noise.tolist(), "seed": 42, } noisy_obs_gen = StateSpaceGenerator(engine="polars", **noisy_obs_params) noisy_obs_df = noisy_obs_gen.generate(n_series=1) print(f"Generated {len(noisy_obs_df)} observations with high observation noise") print( f"Statistics: Mean={noisy_obs_df['y'].mean():.4f}, " f"Std={noisy_obs_df['y'].std():.4f}" ) noisy_obs_df.head(10) ``` ```text theme={null} Generated 200 observations with high observation noise Statistics: Mean=-0.2227, Std=2.9604 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 5.679936 | | "0" | 2000-01-02 00:00:00 | -2.754326 | | "0" | 2000-01-03 00:00:00 | -5.158032 | | "0" | 2000-01-04 00:00:00 | -2.433461 | | "0" | 2000-01-05 00:00:00 | -0.135867 | | "0" | 2000-01-06 00:00:00 | 1.917522 | | "0" | 2000-01-07 00:00:00 | 5.393208 | | "0" | 2000-01-08 00:00:00 | -0.005179 | | "0" | 2000-01-09 00:00:00 | 2.933222 | | "0" | 2000-01-10 00:00:00 | -3.603567 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(noisy_obs_df["ds"].to_list(), noisy_obs_df["y"].to_list(), alpha=0.8, color="C5") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("High observation noise (noisy measurements)") plt.tight_layout() plt.show() ``` ## 7. Generate with hidden states Return both observations and hidden states to inspect the latent dynamics. ```python theme={null} with_states_params = { "min_length": 100, "max_length": 100, "freq": "D", "state_dim": 2, "obs_dim": 1, "seed": 42, } with_states_gen = StateSpaceGenerator(engine="polars", **with_states_params) obs_df, states_df = with_states_gen.generate_with_states(n_series=1) print(f"Generated observations and hidden states") print( f"Observation Statistics: Mean={obs_df['y'].mean():.4f}, " f"Std={obs_df['y'].std():.4f}" ) print("\nObservations:") print(obs_df.head(10)) print("\nHidden States:") print(states_df.head(10)) ``` ```text theme={null} Generated observations and hidden states Observation Statistics: Mean=-0.0286, Std=0.4412 Observations: shape: (10, 3) ┌───────────┬─────────────────────┬───────────┐ │ unique_id ┆ ds ┆ y │ │ --- ┆ --- ┆ --- │ │ cat ┆ datetime[ns] ┆ f64 │ ╞═══════════╪═════════════════════╪═══════════╡ │ 0 ┆ 2000-01-01 00:00:00 ┆ -1.910609 │ │ 0 ┆ 2000-01-02 00:00:00 ┆ -0.642705 │ │ 0 ┆ 2000-01-03 00:00:00 ┆ 0.258913 │ │ 0 ┆ 2000-01-04 00:00:00 ┆ 0.120297 │ │ 0 ┆ 2000-01-05 00:00:00 ┆ 0.451117 │ │ 0 ┆ 2000-01-06 00:00:00 ┆ -0.195514 │ │ 0 ┆ 2000-01-07 00:00:00 ┆ 0.256028 │ │ 0 ┆ 2000-01-08 00:00:00 ┆ 0.070385 │ │ 0 ┆ 2000-01-09 00:00:00 ┆ 0.791294 │ │ 0 ┆ 2000-01-10 00:00:00 ┆ -0.372822 │ └───────────┴─────────────────────┴───────────┘ Hidden States: shape: (10, 4) ┌───────────┬─────────────────────┬───────────┬───────────┐ │ unique_id ┆ ds ┆ state_0 ┆ state_1 │ │ --- ┆ --- ┆ --- ┆ --- │ │ cat ┆ datetime[ns] ┆ f64 ┆ f64 │ ╞═══════════╪═════════════════════╪═══════════╪═══════════╡ │ 0 ┆ 2000-01-01 00:00:00 ┆ -1.951035 ┆ -1.30218 │ │ 0 ┆ 2000-01-02 00:00:00 ┆ -0.372949 ┆ -0.5793 │ │ 0 ┆ 2000-01-03 00:00:00 ┆ 0.238032 ┆ 0.097115 │ │ 0 ┆ 2000-01-04 00:00:00 ┆ 0.39203 ┆ 0.210554 │ │ 0 ┆ 2000-01-05 00:00:00 ┆ 0.173327 ┆ -0.193943 │ │ 0 ┆ 2000-01-06 00:00:00 ┆ 0.019815 ┆ -0.044133 │ │ 0 ┆ 2000-01-07 00:00:00 ┆ 0.391477 ┆ -0.049829 │ │ 0 ┆ 2000-01-08 00:00:00 ┆ -0.045179 ┆ 0.24666 │ │ 0 ┆ 2000-01-09 00:00:00 ┆ 0.114046 ┆ 0.155727 │ │ 0 ┆ 2000-01-10 00:00:00 ┆ -0.115485 ┆ -0.119006 │ └───────────┴─────────────────────┴───────────┴───────────┘ ``` ```python theme={null} fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True) axes[0].plot(obs_df["ds"].to_list(), obs_df["y"].to_list(), alpha=0.8, label="Observations") axes[0].set_ylabel("Observed Value") axes[0].set_title("Observations vs hidden states") axes[0].legend() state_cols = [c for c in states_df.columns if c.startswith("state_")] for col in state_cols: axes[1].plot(states_df["ds"].to_list(), states_df[col].to_list(), alpha=0.8, label=col) axes[1].set_xlabel("Timestamp") axes[1].set_ylabel("State Value") axes[1].legend() plt.tight_layout() plt.show() ``` ## 8. Multiple state space series Generate multiple independent state space series. ```python theme={null} multi_params = { "min_length": 150, "max_length": 150, "freq": "D", "state_dim": 2, "obs_dim": 1, "seed": 42, } multi_gen = StateSpaceGenerator(engine="polars", **multi_params) multi_df = multi_gen.generate(n_series=3) print(f"Generated 3 series with {len(multi_df)} total observations") print( f"Overall Statistics: Mean={multi_df['y'].mean():.4f}, " f"Std={multi_df['y'].std():.4f}" ) multi_df.filter(pl.col("unique_id") == "0").head(10) ``` ```text theme={null} Generated 3 series with 450 total observations Overall Statistics: Mean=-0.0026, Std=0.4818 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 2.035213 | | "0" | 2000-01-02 00:00:00 | 0.342724 | | "0" | 2000-01-03 00:00:00 | -0.130302 | | "0" | 2000-01-04 00:00:00 | -0.445831 | | "0" | 2000-01-05 00:00:00 | 0.159239 | | "0" | 2000-01-06 00:00:00 | -0.206335 | | "0" | 2000-01-07 00:00:00 | 0.401635 | | "0" | 2000-01-08 00:00:00 | -0.350341 | | "0" | 2000-01-09 00:00:00 | 0.077724 | | "0" | 2000-01-10 00:00:00 | -0.512018 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple state space series") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [SARIMA](../statistical/sarima) and [ETS](../statistical/ets) — > specific state-space families. > * [Chaotic system](../stochastic/chaotic_system) — deterministic > nonlinear dynamics. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Vital signs Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/domain/vital_signs.html Physiological signals — heart rate, respiration, and the like — have characteristic rhythms that shift with a patient’s condition. `VitalSignsGenerator` produces these for testing clinical monitoring and early-warning models without touching protected health data. > **The model** > > Each series is a per-series baseline plus a slow random-walk drift, a > circadian rhythm, heart-rate variability (for heart rate and blood > pressure), random physiological events (activity bursts, rest periods, > spikes), and measurement noise. The non-heart-rate signals also carry > a correlation with heart rate. `vital_sign` chooses which of the six > signals to emit, and `patient_type` — `healthy`, `cardiac`, `sepsis`, > `respiratory`, or `hypertensive` — sets the baselines and the > physiological bounds that values are clipped to. > > The circadian and HRV components assume one step is one minute > (`freq='min'`); other frequencies distort those cycle periods. ```python theme={null} import matplotlib.pyplot as plt from synforecast.generators import VitalSignsGenerator ``` ## Heart rate - healthy patient Generate 24 hours of per-minute heart rate data for a healthy patient with circadian rhythm, HRV, and clinical events enabled. ```python theme={null} params = { "min_length": 1440, "max_length": 1440, "freq": "min", "patient_type": "healthy", "vital_sign": "heart_rate", "include_circadian": True, "include_hrv": True, "include_events": True, "seed": 42, } generator = VitalSignsGenerator(engine="polars", **params) df = generator.generate(n_series=1) values = df["y"].to_numpy() print(f"Heart rate range: [{values.min():.1f}, {values.max():.1f}] bpm") print(f"Mean heart rate: {values.mean():.1f} bpm") print(f"Std deviation: {values.std():.1f} bpm") ``` ```text theme={null} Heart rate range: [50.0, 89.9] bpm Mean heart rate: 61.8 bpm Std deviation: 7.1 bpm ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(df["ds"].to_list(), df["y"].to_list(), alpha=0.8, linewidth=0.5) ax.set_xlabel("Timestamp") ax.set_ylabel("Heart Rate (bpm)") ax.set_title("Heart rate — healthy patient (24 hours)") plt.tight_layout() plt.show() ``` ## Comparing patient types Compare heart rate distributions across different patient conditions. ```python theme={null} patient_types = ["healthy", "cardiac", "sepsis", "respiratory", "hypertensive"] for ptype in patient_types: gen = VitalSignsGenerator(engine="polars", **{ "min_length": 1440, "max_length": 1440, "freq": "min", "patient_type": ptype, "vital_sign": "heart_rate", "seed": 42, } ) df_patient = gen.generate(n_series=1) hr = df_patient["y"].to_numpy() print( f"{ptype:15s}: mean={hr.mean():.1f}, std={hr.std():.1f}, range=[{hr.min():.0f}, {hr.max():.0f}]" ) ``` ```text theme={null} healthy : mean=61.8, std=7.1, range=[50, 90] cardiac : mean=69.5, std=11.0, range=[50, 125] sepsis : mean=85.3, std=12.8, range=[70, 150] respiratory : mean=67.9, std=8.8, range=[55, 111] hypertensive : mean=67.8, std=8.0, range=[55, 103] ``` ```python theme={null} fig, axes = plt.subplots(len(patient_types), 1, figsize=(12, 2.5 * len(patient_types)), sharex=True) for ax, ptype in zip(axes, patient_types): gen = VitalSignsGenerator(engine="polars", **{"min_length": 1440, "max_length": 1440, "freq": "min", "patient_type": ptype, "vital_sign": "heart_rate", "seed": 42}) df_p = gen.generate(n_series=1) ax.plot(df_p["ds"].to_list(), df_p["y"].to_list(), alpha=0.8, linewidth=0.5) ax.set_ylabel("HR (bpm)") ax.set_title(ptype) plt.xlabel("Timestamp") plt.suptitle("Heart rate by patient type") plt.tight_layout() plt.show() ``` ## All vital signs - sepsis patient Generate all six vital signs simultaneously for a sepsis patient over 8 hours. ```python theme={null} sepsis_gen = VitalSignsGenerator(engine="polars", **{ "min_length": 480, "max_length": 480, "freq": "min", "patient_type": "sepsis", "seed": 42, } ) all_vitals_df = sepsis_gen.generate_all_vitals(n_series=1) print("Vital sign statistics for sepsis patient:") vital_cols = [ "heart_rate", "systolic_bp", "diastolic_bp", "respiratory_rate", "spo2", "temperature", ] for col in vital_cols: vals = all_vitals_df[col].to_numpy() print( f" {col:18s}: mean={vals.mean():.1f}, range=[{vals.min():.1f}, {vals.max():.1f}]" ) ``` ```text theme={null} Vital sign statistics for sepsis patient: heart_rate : mean=92.9, range=[70.0, 118.9] systolic_bp : mean=90.3, range=[70.0, 129.8] diastolic_bp : mean=55.0, range=[42.9, 69.2] respiratory_rate : mean=30.8, range=[21.9, 35.0] spo2 : mean=93.2, range=[89.8, 98.0] temperature : mean=39.2, range=[38.2, 40.4] ``` ```python theme={null} vital_cols = ["heart_rate", "systolic_bp", "diastolic_bp", "respiratory_rate", "spo2", "temperature"] fig, axes = plt.subplots(2, 3, figsize=(16, 8)) for ax, col in zip(axes.flat, vital_cols): ax.plot(all_vitals_df["ds"].to_list(), all_vitals_df[col].to_list(), alpha=0.8, linewidth=0.5) ax.set_title(col.replace("_", " ").title()) ax.set_xlabel("Timestamp") ax.tick_params(axis="x", rotation=45) plt.suptitle("All vital signs — sepsis patient (8 hours)", fontsize=14) plt.tight_layout() plt.show() ``` ## Oxygen saturation (SpO2) comparison Compare SpO2 levels across healthy, respiratory, and sepsis patients. Lower SpO2 and more time below 95% indicates worse oxygenation. ```python theme={null} for ptype in ["healthy", "respiratory", "sepsis"]: gen = VitalSignsGenerator(engine="polars", **{ "min_length": 1440, "max_length": 1440, "freq": "min", "patient_type": ptype, "vital_sign": "spo2", "seed": 42, } ) df_spo2 = gen.generate(n_series=1) spo2 = df_spo2["y"].to_numpy() below_95 = (spo2 < 95).sum() / len(spo2) * 100 print( f"{ptype:12s}: mean={spo2.mean():.1f}%, min={spo2.min():.1f}%, time <95%: {below_95:.1f}%" ) ``` ```text theme={null} healthy : mean=97.0%, min=95.2%, time <95%: 0.0% respiratory : mean=89.2%, min=85.0%, time <95%: 99.7% sepsis : mean=90.2%, min=85.0%, time <95%: 98.5% ``` ## Model information Inspect the generator configuration and baseline values for each vital sign. ```python theme={null} info = generator.get_model_info() print(f"Patient type: {info['patient_type']}") print(f"Current vital sign: {info['vital_sign']}") print(f"Circadian rhythm: {info['include_circadian']}") print(f"HRV included: {info['include_hrv']}") print("\nBaseline values for healthy patient:") for vital, params in info["baselines"].items(): print( f" {vital}: mean={params['mean']}, range=[{params['min']}, {params['max']}]" ) ``` ```text theme={null} Patient type: healthy Current vital sign: heart_rate Circadian rhythm: True HRV included: True Baseline values for healthy patient: heart_rate: mean=70, range=[50, 100] systolic_bp: mean=120, range=[90, 140] diastolic_bp: mean=80, range=[60, 90] respiratory_rate: mean=14, range=[10, 20] spo2: mean=98, range=[95, 100] temperature: mean=36.8, range=[36.0, 37.5] ``` > **Related generators** > > * [Anomalies](../../capabilities/anomalies) — inject labelled events > for detector benchmarks. > * [IoT sensor](iot_sensor) — another instrument-style signal with > artifacts. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Copula (dependency structure) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/multivariate/copula.html A copula separates *what each series looks like* (its marginal distribution) from *how the series move together* (their dependence). `CopulaGenerator` imposes a target correlation structure across channels while leaving each channel’s marginal free — the right tool when the joint dependence matters more than any single series’ dynamics. > **The model** > > $z \sim \mathcal{N}(0, R), \qquad u_i = \Phi(z_i), \qquad x_i = F_i^{-1}(u_i)$ > > A copula (`copula_type="gaussian"` or `"t"`) couples the channels > through a `correlation_matrix`, then maps each channel back to its > marginal. A t-copula adds tail dependence — extremes that co-occur — > which a Gaussian copula misses. ```python theme={null} import numpy as np import polars as pl import matplotlib.pyplot as plt from synforecast.generators import CopulaGenerator ``` ## 1. Gaussian copula with correlated normal variables Generate three correlated variables with specified pairwise correlations using a Gaussian copula. ```python theme={null} corr_matrix = np.array([[1.0, 0.8, 0.6], [0.8, 1.0, 0.7], [0.6, 0.7, 1.0]]) params_gaussian = { "min_length": 200, "max_length": 200, "freq": "D", "copula_type": "gaussian", "correlation_matrix": corr_matrix, "marginal_distributions": [ {"type": "normal", "loc": 100.0, "scale": 10.0}, {"type": "normal", "loc": 50.0, "scale": 5.0}, {"type": "normal", "loc": 200.0, "scale": 20.0}, ], "seed": 42, } gen_gaussian = CopulaGenerator(engine="polars", **params_gaussian) df_gaussian = gen_gaussian.generate(n_series=3) print(f"Generated {len(df_gaussian)} observations with 3 correlated variables") df_gaussian.head(10) ``` ```text theme={null} Generated 600 observations with 3 correlated variables ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 95.378555 | | "0" | 2000-01-02 00:00:00 | 81.197776 | | "0" | 2000-01-03 00:00:00 | 97.662273 | | "0" | 2000-01-04 00:00:00 | 112.804514 | | "0" | 2000-01-05 00:00:00 | 104.679034 | | "0" | 2000-01-06 00:00:00 | 106.63344 | | "0" | 2000-01-07 00:00:00 | 91.473154 | | "0" | 2000-01-08 00:00:00 | 110.159648 | | "0" | 2000-01-09 00:00:00 | 103.90346 | | "0" | 2000-01-10 00:00:00 | 99.305584 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_gaussian["unique_id"].unique().to_list(): series = df_gaussian.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Gaussian copula — correlated series") ax.legend() plt.tight_layout() plt.show() ``` ### Verify correlations Check that the empirical correlations match the specified correlation matrix. ```python theme={null} df_wide = df_gaussian.pivot(on="unique_id", index="ds", values="y") series_cols = sorted([col for col in df_wide.columns if col != "ds"]) if len(series_cols) >= 3: corr_0_1 = np.corrcoef( df_wide[series_cols[0]].to_numpy(), df_wide[series_cols[1]].to_numpy() )[0, 1] corr_0_2 = np.corrcoef( df_wide[series_cols[0]].to_numpy(), df_wide[series_cols[2]].to_numpy() )[0, 1] corr_1_2 = np.corrcoef( df_wide[series_cols[1]].to_numpy(), df_wide[series_cols[2]].to_numpy() )[0, 1] print(f"Correlations (compared to specified):") print(f" 0 vs 1: {corr_0_1:.3f} (specified: 0.800)") print(f" 0 vs 2: {corr_0_2:.3f} (specified: 0.600)") print(f" 1 vs 2: {corr_1_2:.3f} (specified: 0.700)") ``` ```text theme={null} Correlations (compared to specified): 0 vs 1: 0.809 (specified: 0.800) 0 vs 2: 0.627 (specified: 0.600) 1 vs 2: 0.745 (specified: 0.700) ``` ```python theme={null} fig, axes = plt.subplots(1, 3, figsize=(14, 4)) cols = sorted([c for c in df_wide.columns if c != "ds"]) pairs = [(0, 1), (0, 2), (1, 2)] for ax, (i, j) in zip(axes, pairs): ax.scatter(df_wide[cols[i]].to_list(), df_wide[cols[j]].to_list(), alpha=0.4, s=10) ax.set_xlabel(cols[i]) ax.set_ylabel(cols[j]) corr_val = np.corrcoef(df_wide[cols[i]].to_numpy(), df_wide[cols[j]].to_numpy())[0, 1] ax.set_title(f"Correlation: {corr_val:.2f}") plt.suptitle("Gaussian copula — pairwise scatter plots") plt.tight_layout() plt.show() ``` ## 2. t-copula with heavy tail dependence A t-copula with the same correlation as a Gaussian copula makes joint extremes more likely: every time step, all channels share one chi-square variance-mixing draw, so large deviations tend to arrive together. That shared mixing also means a t-copula exhibits some dependence even at zero nominal correlation — always pass an explicit `correlation_matrix` so the dependence you observe is the dependence you configured. Below, both copulas use the same 0.7 correlation and standard-normal marginals. Correlation alone cannot tell them apart; the joint tails can: we measure how often one channel sits in its worst (or best) 5% of values *given* that the other one does. ```python theme={null} corr_pair = np.array([[1.0, 0.7], [0.7, 1.0]]) common_params = { "min_length": 2000, "max_length": 2000, "freq": "D", "correlation_matrix": corr_pair, "marginal_distributions": [ {"type": "normal", "loc": 0.0, "scale": 1.0}, {"type": "normal", "loc": 0.0, "scale": 1.0}, ], "seed": 123, } gen_gauss_pair = CopulaGenerator( engine="polars", copula_type="gaussian", **common_params ) gen_t = CopulaGenerator(engine="polars", copula_type="t", df=3.0, **common_params) wide = {} for name, gen in [("gaussian", gen_gauss_pair), ("t", gen_t)]: df_pair = gen.generate(n_series=2) w = df_pair.pivot(on="unique_id", index="ds", values="y") cols = sorted(c for c in w.columns if c != "ds") wide[name] = (w[cols[0]].to_numpy(), w[cols[1]].to_numpy()) q = 0.05 print(f"Same specified correlation (0.7), joint {q:.0%}-tail behavior:\n") for name, (x, y) in wide.items(): pearson = np.corrcoef(x, y)[0, 1] lo = np.mean((x <= np.quantile(x, q)) & (y <= np.quantile(y, q))) hi = np.mean((x >= np.quantile(x, 1 - q)) & (y >= np.quantile(y, 1 - q))) cond = (lo + hi) / (2 * q) print( f" {name:8s} copula: correlation {pearson:.2f}, " f"P(other channel also in its {q:.0%} tail) = {cond:.0%}" ) print(f"\n independent channels would give {q:.0%}") ``` ```text theme={null} Same specified correlation (0.7), joint 5%-tail behavior: gaussian copula: correlation 0.70, P(other channel also in its 5% tail) = 39% t copula: correlation 0.70, P(other channel also in its 5% tail) = 51% independent channels would give 5% ``` ```python theme={null} fig, axes = plt.subplots(1, 2, figsize=(11, 4.5), sharex=True, sharey=True) for ax, (name, (x, y)) in zip(axes, wide.items()): lo_x, lo_y = np.quantile(x, q), np.quantile(y, q) hi_x, hi_y = np.quantile(x, 1 - q), np.quantile(y, 1 - q) joint = ((x <= lo_x) & (y <= lo_y)) | ((x >= hi_x) & (y >= hi_y)) ax.scatter(x[~joint], y[~joint], alpha=0.25, s=8, label="body") ax.scatter(x[joint], y[joint], alpha=0.9, s=14, label="joint tail") ax.set_title(f"{name} copula ({joint.sum()} joint-tail points)") ax.set_xlabel("channel 0") ax.legend(fontsize=8) axes[0].set_ylabel("channel 1") plt.suptitle("Same correlation, different tails: joint extremes under each copula") plt.tight_layout() plt.show() ``` ## 3. Gaussian copula with mixed marginal distributions Copulas decouple the dependency structure from the marginals, allowing different distribution types for each variable. ```python theme={null} params_mixed = { "min_length": 200, "max_length": 200, "freq": "D", "copula_type": "gaussian", "marginal_distributions": [ {"type": "normal", "loc": 100.0, "scale": 15.0}, {"type": "lognormal", "mean": 4.0, "sigma": 0.3}, {"type": "gamma", "shape": 2.0, "scale": 10.0}, {"type": "uniform", "low": 0.0, "high": 100.0}, ], "seed": 456, } gen_mixed = CopulaGenerator(engine="polars", **params_mixed) df_mixed = gen_mixed.generate(n_series=4) print(f"Generated {len(df_mixed)} observations with mixed marginals") stats = df_mixed.group_by("unique_id").agg( pl.col("y").mean().alias("mean"), pl.col("y").std().alias("std") ) stats.sort("unique_id") ``` ```text theme={null} Generated 800 observations with mixed marginals ``` | unique\_id | mean | std | | ---------- | ---------- | --------- | | cat | f64 | f64 | | "0" | 100.361521 | 15.150987 | | "1" | 56.092974 | 16.038571 | | "2" | 20.491118 | 15.704863 | | "3" | 49.569035 | 28.428938 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_mixed["unique_id"].unique().to_list(): series = df_mixed.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Gaussian copula — mixed marginal distributions") ax.legend() plt.tight_layout() plt.show() ``` ## 4. Generating multiple correlated series Generate multiple independent draws of correlated multivariate series. ```python theme={null} params_multi = { "min_length": 100, "max_length": 100, "freq": "h", "copula_type": "gaussian", "seed": 789, } gen_multi = CopulaGenerator(engine="polars", **params_multi) df_multi = gen_multi.generate(n_series=3) print(f"Generated 3 multivariate series") print(f"Total rows: {len(df_multi)}") print(f"Unique series IDs: {df_multi['unique_id'].unique().to_list()}") df_multi.filter(pl.col("unique_id") == "0").head(5) ``` ```text theme={null} Generated 3 multivariate series Total rows: 300 Unique series IDs: ['0', '1', '2'] ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -1.177967 | | "0" | 2000-01-01 01:00:00 | -1.662594 | | "0" | 2000-01-01 02:00:00 | 1.704117 | | "0" | 2000-01-01 03:00:00 | 2.0864 | | "0" | 2000-01-01 04:00:00 | 0.423711 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_multi["unique_id"].unique().to_list(): series = df_multi.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple correlated series") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [VAR](var) — dependence that plays out *over time* (lead-lag), not > just contemporaneously. > * [Multivariatize](../../capabilities/multivariatize) — turn any > univariate generator into coupled channels. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Gaussian process (kernel-defined structure) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/multivariate/gaussian_process.html A Gaussian process specifies a series entirely through its *covariance kernel*: choose the kernel and you choose the smoothness, length scale, and periodicity of the samples. It is the most direct way to dial in a precise correlation structure, from rough and jagged to infinitely smooth. > **Kernels** > > $f \sim \mathcal{GP}(m, k), \qquad k_{\mathrm{rbf}}(r) = a^2 \exp\!\Big(\!-\frac{r^2}{2\ell^2}\Big), \qquad r = |t - t'|$ > > `kernel` selects the covariance function — `rbf` (infinitely smooth), > `matern_0.5` / `matern_1.5` / `matern_2.5` (increasingly smooth), or > `periodic` (exact periodicity). `length_scale` sets how quickly > correlation decays with distance, and `amplitude` sets the output > scale. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import GaussianProcessGenerator ``` ## 1. Kernel choice The kernel is the whole model: it fixes smoothness, length scale, and periodicity. All four draws share a seed and a length scale, so the difference is entirely the covariance function. ```python theme={null} rbf_df = GaussianProcessGenerator( engine="polars", min_length=200, max_length=200, freq="D", kernel="rbf", length_scale=15.0, seed=42, ).generate(n_series=1) rough_df = GaussianProcessGenerator( engine="polars", min_length=200, max_length=200, freq="D", kernel="matern_0.5", length_scale=15.0, seed=42, ).generate(n_series=1) smooth_df = GaussianProcessGenerator( engine="polars", min_length=200, max_length=200, freq="D", kernel="matern_2.5", length_scale=15.0, seed=42, ).generate(n_series=1) periodic_df = GaussianProcessGenerator( engine="polars", min_length=200, max_length=200, freq="D", kernel="periodic", length_scale=15.0, period=30.0, seed=42, ).generate(n_series=1) panels = [ ("rbf (infinitely smooth)", rbf_df), ("matern_0.5 (rough, OU-like)", rough_df), ("matern_2.5 (twice differentiable)", smooth_df), ("periodic (period=30)", periodic_df), ] fig, axes = plt.subplots(4, 1, figsize=(12, 9), sharex=True) for ax, (label, df) in zip(axes, panels): ax.plot(df["ds"].to_list(), df["y"].to_list(), alpha=0.85, linewidth=1) ax.set(ylabel="Value", title=label) axes[-1].set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## 2. Multiple series Generate multiple independent GP realizations. ```python theme={null} multi_gen = GaussianProcessGenerator(engine="polars", min_length=150, max_length=150, freq="D", kernel="matern_2.5", length_scale=15.0, seed=42, ) multi_df = multi_gen.generate(n_series=3) fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple GP realizations (Matern 2.5)") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [KernelSynth](../pretraining/kernel_synth) — *random compositions* > of these kernels for pretraining corpora. > * [Cyclic](../stochastic/cyclic) — irregular oscillation without a > fixed kernel. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # VAR (vector autoregression) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/multivariate/var.html A vector autoregression models several series that influence *each other over time*: every channel is a linear function of the recent past of all channels. It is the standard multivariate linear model for coupled economic and sensor series, capturing the lead-lag feedback that a set of independent univariate models cannot. > **The model** > > $y_t = c + A_1 y_{t-1} + \dots + A_p y_{t-p} + e_t, \qquad e_t \sim (0, \Sigma)$ > > At each step every series is a linear combination of the last > `lag_order` values of all series, plus correlated innovations. > `generate(n_series)` returns the coupled channels of one system, so > the cross-series dynamics are shared rather than independent. ```python theme={null} import numpy as np import polars as pl import matplotlib.pyplot as plt from synforecast.generators import VARGenerator ``` ## 1. VAR(1) with auto-generated stable coefficients Generate a 3-variable VAR(1) model with automatically generated stable coefficient matrices. ```python theme={null} params_auto = { "min_length": 200, "max_length": 200, "freq": "D", "lag_order": 1, "seed": 42, } gen_auto = VARGenerator(engine="polars", **params_auto) df_auto = gen_auto.generate(n_series=3) print(f"Generated {len(df_auto)} observations for 3 variables") df_auto.head(10) ``` ```text theme={null} Generated 600 observations for 3 variables ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 2.057667 | | "0" | 2000-01-02 00:00:00 | 0.295293 | | "0" | 2000-01-03 00:00:00 | 0.893053 | | "0" | 2000-01-04 00:00:00 | 0.235888 | | "0" | 2000-01-05 00:00:00 | 1.876363 | | "0" | 2000-01-06 00:00:00 | -0.051986 | | "0" | 2000-01-07 00:00:00 | 1.583556 | | "0" | 2000-01-08 00:00:00 | -1.001584 | | "0" | 2000-01-09 00:00:00 | -0.94835 | | "0" | 2000-01-10 00:00:00 | 0.680186 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_auto["unique_id"].unique().to_list(): series = df_auto.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("VAR(1) with auto-generated coefficients") ax.legend() plt.tight_layout() plt.show() ``` ### Summary statistics and correlations ```python theme={null} stats = df_auto.group_by("unique_id").agg( pl.col("y").mean().alias("mean"), pl.col("y").std().alias("std"), ) print("Summary statistics:") print(stats) df_wide = df_auto.pivot(on="unique_id", index="ds", values="y") series_cols = [col for col in df_wide.columns if col != "ds"] if len(series_cols) >= 2: corr_01 = np.corrcoef( df_wide[series_cols[0]].to_numpy(), df_wide[series_cols[1]].to_numpy() )[0, 1] print(f"\nVariable correlations:") print(f" 0 vs 1: {corr_01:.3f}") ``` ```text theme={null} Summary statistics: shape: (3, 3) ┌───────────┬───────────┬──────────┐ │ unique_id ┆ mean ┆ std │ │ --- ┆ --- ┆ --- │ │ cat ┆ f64 ┆ f64 │ ╞═══════════╪═══════════╪══════════╡ │ 0 ┆ -0.060332 ┆ 1.168862 │ │ 2 ┆ -0.037405 ┆ 1.077848 │ │ 1 ┆ -0.081108 ┆ 1.0294 │ └───────────┴───────────┴──────────┘ Variable correlations: 0 vs 1: 0.218 ``` ## 2. VAR(1) with custom coefficients (strong cross-effects) Design a coefficient matrix where each variable depends strongly on the other’s lagged values. ```python theme={null} coef_matrix = np.array( [ [0.3, 0.6], [0.5, 0.2], ] ) params_custom = { "min_length": 300, "max_length": 300, "freq": "D", "lag_order": 1, "coef_matrices": [coef_matrix], "intercept": np.array([1.0, 2.0]), "seed": 123, } gen_custom = VARGenerator(engine="polars", **params_custom) df_custom = gen_custom.generate(n_series=2) print(f"Generated {len(df_custom)} observations") print(f"Coefficient matrix:\n{coef_matrix}") df_custom.head(10) ``` ```text theme={null} Generated 600 observations Coefficient matrix: [[0.3 0.6] [0.5 0.2]] ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 6.160864 | | "0" | 2000-01-02 00:00:00 | 8.747224 | | "0" | 2000-01-03 00:00:00 | 9.36052 | | "0" | 2000-01-04 00:00:00 | 6.545609 | | "0" | 2000-01-05 00:00:00 | 11.293658 | | "0" | 2000-01-06 00:00:00 | 9.197751 | | "0" | 2000-01-07 00:00:00 | 10.410391 | | "0" | 2000-01-08 00:00:00 | 8.974064 | | "0" | 2000-01-09 00:00:00 | 7.905556 | | "0" | 2000-01-10 00:00:00 | 8.612394 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_custom["unique_id"].unique().to_list(): series = df_custom.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("VAR(1) with custom coefficients (strong cross-effects)") ax.legend() plt.tight_layout() plt.show() ``` ```python theme={null} df_custom_wide = df_custom.pivot(on="unique_id", index="ds", values="y") custom_cols = [col for col in df_custom_wide.columns if col != "ds"] if len(custom_cols) >= 2: corr_stocks = np.corrcoef( df_custom_wide[custom_cols[0]].to_numpy(), df_custom_wide[custom_cols[1]].to_numpy(), )[0, 1] print(f"Series correlation: {corr_stocks:.3f}") print(f"Note: Strong cross-dependencies create correlation between variables") ``` ```text theme={null} Series correlation: 0.414 Note: Strong cross-dependencies create correlation between variables ``` ## 3. Higher-order VAR(3) model A VAR(3) model uses three lags of each variable, capturing longer-range dependencies. ```python theme={null} params_var3 = { "min_length": 200, "max_length": 200, "freq": "h", "lag_order": 3, "seed": 456, } gen_var3 = VARGenerator(engine="polars", **params_var3) df_var3 = gen_var3.generate(n_series=2) print(f"Generated {len(df_var3)} hourly observations") print(f"Model: VAR(3) - uses 3 lags of each variable") df_var3.head(10) ``` ```text theme={null} Generated 400 hourly observations Model: VAR(3) - uses 3 lags of each variable ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -0.221184 | | "0" | 2000-01-01 01:00:00 | -0.122691 | | "0" | 2000-01-01 02:00:00 | 0.572251 | | "0" | 2000-01-01 03:00:00 | -2.239382 | | "0" | 2000-01-01 04:00:00 | 1.699675 | | "0" | 2000-01-01 05:00:00 | 0.831221 | | "0" | 2000-01-01 06:00:00 | 0.521463 | | "0" | 2000-01-01 07:00:00 | -0.893806 | | "0" | 2000-01-01 08:00:00 | -0.722153 | | "0" | 2000-01-01 09:00:00 | -1.019791 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_var3["unique_id"].unique().to_list(): series = df_var3.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("VAR(3) higher-order model") ax.legend() plt.tight_layout() plt.show() ``` ## 4. VAR with correlated innovations Specify a custom innovation covariance matrix to add contemporaneous correlation between variables. ```python theme={null} innov_cov = np.array([[1.0, 0.7], [0.7, 1.0]]) params_corr_innov = { "min_length": 200, "max_length": 200, "freq": "D", "lag_order": 1, "innovation_covariance": innov_cov, "seed": 789, } gen_corr_innov = VARGenerator(engine="polars", **params_corr_innov) df_corr_innov = gen_corr_innov.generate(n_series=2) print(f"Generated {len(df_corr_innov)} observations") print(f"Innovation covariance matrix:\n{innov_cov}") df_corr_wide = df_corr_innov.pivot(on="unique_id", index="ds", values="y") corr_cols = [col for col in df_corr_wide.columns if col != "ds"] if len(corr_cols) >= 2: corr_markets = np.corrcoef( df_corr_wide[corr_cols[0]].to_numpy(), df_corr_wide[corr_cols[1]].to_numpy() )[0, 1] print(f"\nSeries correlation: {corr_markets:.3f}") print(f"Note: Correlated innovations create additional correlation") ``` ```text theme={null} Generated 400 observations Innovation covariance matrix: [[1. 0.7] [0.7 1. ]] Series correlation: 0.705 Note: Correlated innovations create additional correlation ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_corr_innov["unique_id"].unique().to_list(): series = df_corr_innov.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("VAR with correlated innovations") ax.legend() plt.tight_layout() plt.show() ``` ## 5. Generating multiple VAR series Generate multiple independent draws of bivariate VAR series. ```python theme={null} params_multi = { "min_length": 100, "max_length": 100, "freq": "D", "lag_order": 1, "seed": 999, } gen_multi = VARGenerator(engine="polars", **params_multi) df_multi = gen_multi.generate(n_series=3) print(f"Generated 3 bivariate VAR series") print(f"Total rows: {len(df_multi)}") print(f"Unique series IDs: {df_multi['unique_id'].unique().to_list()}") df_multi.filter(pl.col("unique_id") == "0").head(5) ``` ```text theme={null} Generated 3 bivariate VAR series Total rows: 300 Unique series IDs: ['0', '1', '2'] ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -1.098665 | | "0" | 2000-01-02 00:00:00 | -0.519748 | | "0" | 2000-01-03 00:00:00 | 2.138136 | | "0" | 2000-01-04 00:00:00 | -0.6182 | | "0" | 2000-01-05 00:00:00 | -0.871331 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_multi["unique_id"].unique().to_list(): series = df_multi.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple bivariate VAR series") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Copula](copula) — contemporaneous dependence without temporal > dynamics. > * [Multivariatize](../../capabilities/multivariatize) — add coupling > to any univariate generator. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # KernelSynth Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/pretraining/kernel_synth.html `KernelSynthGenerator` samples each series from a Gaussian-process prior whose covariance is a *random composition* of simple base kernels. For every series it draws `1..max_kernels` kernels from a fixed bank, folds them together with randomly chosen `+` and `*` operators, and draws one path from the resulting prior. Addition mixes behaviors (trend + seasonality); multiplication modulates them (locally periodic, amplitude-varying seasonality). $k = k_1 \star k_2 \star \dots \star k_n, \quad \star \in \{+, \times\}, \qquad f \sim \mathcal{GP}(0, k)$ This adapts the KernelSynth recipe introduced for pretraining the Chronos forecasting models (Ansari et al. 2024, [*Chronos: Learning the Language of Time Series*](https://arxiv.org/abs/2403.07815)) and its Apache-2.0-licensed [reference implementation](https://github.com/amazon-science/chronos-forecasting/blob/main/scripts/kernel-synth.py). SynForecast makes the bank configurable, expresses seasonal periods in time steps on a normalized grid, and adds bounded retries, divergence guards, and optional standardization. ```python theme={null} import matplotlib.pyplot as plt import polars as pl from synforecast.generators import KernelSynthGenerator ``` ## Generate a diverse pool With the default bank, each series is a different kernel composition, so one call yields a varied pool. Lengths are fixed here to make the paths easy to compare; the composition is what varies. ```python theme={null} generator = KernelSynthGenerator( engine="polars", min_length=256, max_length=256, freq="h", seed=42, ) ks_df = generator.generate(n_series=6) ks_df.group_by("unique_id").agg( pl.len().alias("length"), pl.col("y").mean().round(3).alias("mean"), pl.col("y").std().round(3).alias("std"), ).sort("unique_id") ``` | unique\_id | length | mean | std | | ---------- | ------ | ---- | ----- | | cat | u32 | f64 | f64 | | "0" | 256 | 0.0 | 1.002 | | "1" | 256 | 0.0 | 1.002 | | "2" | 256 | -0.0 | 1.002 | | "3" | 256 | 0.0 | 1.002 | | "4" | 256 | -0.0 | 1.002 | | "5" | 256 | 0.0 | 1.002 | ```python theme={null} fig, axes = plt.subplots(6, 1, figsize=(12, 11), sharex=True) for ax, uid in zip(axes, ks_df["unique_id"].unique(maintain_order=True), strict=True): series = ks_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"], series["y"], linewidth=1) ax.set_ylabel(str(uid)) axes[-1].set_xlabel("Timestamp") fig.suptitle("KernelSynth: random kernel compositions (standardized)") plt.tight_layout() plt.show() ``` ## Constrain the kernel bank Every kernel family is a list you can narrow or extend. To bias the pool toward a specific inductive structure, restrict the bank — here to a single daily period plus a smooth RBF trend, composed one or two at a time — so the draws concentrate on smooth, near-daily-periodic shapes. ```python theme={null} periodic = KernelSynthGenerator( engine="polars", min_length=336, max_length=336, freq="h", max_kernels=2, seasonal_periods=[24.0], rbf_length_scales=[0.3], rational_quadratic_alphas=[], linear_sigmas=[], white_noise_levels=[0.05], include_constant=False, seed=7, ) periodic_df = periodic.generate(n_series=3) fig, ax = plt.subplots(figsize=(12, 4)) for uid in periodic_df["unique_id"].unique(maintain_order=True): s = periodic_df.filter(pl.col("unique_id") == uid) ax.plot(s["ds"], s["y"], linewidth=1, label=str(uid)) ax.set_title("Constrained bank: daily-periodic + smooth RBF") ax.set_xlabel("Timestamp") ax.legend() plt.tight_layout() plt.show() ``` ## Use in a pretraining corpus `standardize=True` (the default) rescales each series to zero mean and unit variance, so compositions that span very different natural scales stay comparable — the usual setting for a pretraining pool. Set `standardize=False` to keep the raw GP draws. Because `KernelSynthGenerator` is an ordinary generator, it composes with `SynSet`, `generate_series`, and the pattern-injection options like any other. ```python theme={null} raw = KernelSynthGenerator( engine="polars", min_length=128, max_length=128, freq="h", standardize=False, seed=0, ) raw.generate(n_series=3).group_by("unique_id").agg( pl.col("y").std().round(3).alias("raw_std") ).sort("unique_id") ``` | unique\_id | raw\_std | | ---------- | -------- | | cat | f64 | | "0" | 0.612 | | "1" | 1.887 | | "2" | 0.312 | > **Related generators** > > * [TSI](tsi) and [TCM](tcm) — the other pretraining generators > (component composition and random causal graphs). > * [Gaussian process](../multivariate/gaussian_process) — a single > fixed kernel rather than random compositions. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Temporal causal model Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/pretraining/tcm.html `TCMGenerator` samples a temporal structural causal model (SCM): variables are connected through randomly sampled lagged edges, then the system is rolled forward with nonlinear edge functions and stochastic innovations. $x_i[t] = \sum_{e \in \mathrm{pa}(i)} f_e\big(x_{j_e}[t - \ell_e]\big) + \varepsilon_i[t]$ The temporal-SCM framing follows the overview in Runge et al. (2023), [Causal inference for time series](https://doi.org/10.1038/s43017-023-00431-y). SynForecast’s graph sampler, edge-function mixture, stability rescaling, and rollout guards are original design choices; this is not a reproduction of a generator from that paper. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import polars as pl from synforecast.generators import TCMGenerator ``` ## Independent SCM draws In the default univariate mode, each output series comes from a separately sampled SCM. The observed series is one node; other nodes in that SCM act as latent drivers. ```python theme={null} independent_generator = TCMGenerator( engine="polars", min_length=256, max_length=256, freq="h", n_vars_range=(2, 5), max_lag_range=(1, 12), seed=42, ) independent_df = independent_generator.generate(n_series=3) independent_df.head() ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 1.345195 | | "0" | 2000-01-01 01:00:00 | -0.067311 | | "0" | 2000-01-01 02:00:00 | -0.248761 | | "0" | 2000-01-01 03:00:00 | -0.196262 | | "0" | 2000-01-01 04:00:00 | 0.030754 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in independent_df["unique_id"].unique(maintain_order=True): series = independent_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"], series["y"], label=str(uid), alpha=0.8) ax.set(title="Independent temporal SCM draws", xlabel="Timestamp", ylabel="y") ax.legend(title="Series") plt.tight_layout() plt.show() ``` ## Nodes from one shared SCM Set `multivariate=True` to return several observed nodes from one jointly rolled-out SCM. Because the sampled edges are *lagged*, dependence between nodes appears in lagged cross-correlations rather than in the contemporaneous correlation matrix — and its strength varies pair by pair with the sampled graph: nodes connected by a directed path co-move, nodes without one stay near the noise floor. Below we compare the maximum absolute cross-correlation over lags 0–12 for every node pair against the same statistic on independent SCM draws. ```python theme={null} joint_generator = TCMGenerator( engine="polars", min_length=384, max_length=384, freq="h", multivariate=True, n_vars_range=(4, 6), max_lag_range=(1, 12), edge_probability_range=(0.15, 0.3), seed=1, ) joint_df = joint_generator.generate(n_series=4) # Edges in a temporal SCM are lagged, so dependence between nodes shows up in # lagged cross-correlations rather than the contemporaneous correlation matrix. def max_lagged_xcorr(x: np.ndarray, y: np.ndarray, max_lag: int) -> float: """Largest |corr(x_t, y_{t-k})| over k = -max_lag..max_lag.""" best = abs(np.corrcoef(x, y)[0, 1]) for k in range(1, max_lag + 1): best = max( best, abs(np.corrcoef(x[k:], y[: len(y) - k])[0, 1]), abs(np.corrcoef(y[k:], x[: len(x) - k])[0, 1]), ) return best wide = joint_df.pivot(on="unique_id", index="ds", values="y").sort("ds") nodes = [column for column in wide.columns if column != "ds"] values = {node: wide[node].to_numpy() for node in nodes} # Baseline: the same statistic on independent SCM draws (no shared graph). independent_baseline_df = TCMGenerator( engine="polars", min_length=384, max_length=384, freq="h", seed=101 ).generate(n_series=4) wide_ind = independent_baseline_df.pivot(on="unique_id", index="ds", values="y").sort( "ds" ) ind_nodes = [column for column in wide_ind.columns if column != "ds"] ind_values = {node: wide_ind[node].to_numpy() for node in ind_nodes} print("Max |cross-correlation| over lags 0..12 per node pair:\n") print(" shared SCM (one causal graph):") for i in range(len(nodes)): for j in range(i + 1, len(nodes)): xc = max_lagged_xcorr(values[nodes[i]], values[nodes[j]], max_lag=12) print(f" {nodes[i]} vs {nodes[j]}: {xc:.2f}") print("\n independent draws (baseline noise level):") for i in range(len(ind_nodes)): for j in range(i + 1, len(ind_nodes)): xc = max_lagged_xcorr(ind_values[ind_nodes[i]], ind_values[ind_nodes[j]], 12) print(f" {ind_nodes[i]} vs {ind_nodes[j]}: {xc:.2f}") joint_df.head() ``` ```text theme={null} Max |cross-correlation| over lags 0..12 per node pair: shared SCM (one causal graph): 0 vs 1: 0.32 0 vs 2: 0.83 0 vs 3: 0.49 1 vs 2: 0.18 1 vs 3: 0.75 2 vs 3: 0.33 independent draws (baseline noise level): 0 vs 1: 0.11 0 vs 2: 0.11 0 vs 3: 0.11 1 vs 2: 0.11 1 vs 3: 0.11 2 vs 3: 0.09 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 5.245226 | | "0" | 2000-01-01 01:00:00 | 2.547378 | | "0" | 2000-01-01 02:00:00 | 1.999835 | | "0" | 2000-01-01 03:00:00 | -0.711531 | | "0" | 2000-01-01 04:00:00 | -2.090204 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in joint_df["unique_id"].unique(maintain_order=True): series = joint_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"], series["y"], label=str(uid), alpha=0.8) ax.set(title="Observed nodes from one temporal SCM", xlabel="Timestamp", ylabel="y") ax.legend(title="Node", ncol=4) plt.tight_layout() plt.show() ``` > **Related generators** > > * [TSI](tsi) — trend/seasonal/irregular composition; > [KernelSynth](kernel_synth) — GP kernel compositions. > * [VAR](../multivariate/var) — linear multivariate dynamics without > a random causal graph. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # TSI Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/pretraining/tsi.html `TSIGenerator` creates diverse series by combining randomized trend, seasonality, and irregular components. Each generated series samples a fresh configuration, so one call can produce a varied synthetic pool. $y_t = T(t) + \sum_{h} S_h(t) + I_t \quad \text{(additive)}, \qquad y_t = T(t)\prod_{h} S_h(t) + I_t \quad \text{(multiplicative)}$ The component-based construction is informed by Bahrpeyma et al. (2021), [A Methodology for Validating Diversity in Synthetic Time Series Generation](https://doi.org/10.1016/j.mex.2021.101459). SynForecast’s component families, sampling distributions, and stability guards are its own design choices rather than a reproduction of the paper’s generator. ```python theme={null} import matplotlib.pyplot as plt import polars as pl from synforecast.generators import TSIGenerator ``` ## Generate a diverse pool Lengths are fixed here to make the series easy to compare. The remaining defaults randomize the trend shape, seasonal harmonics, irregular process, magnitude, and additive or multiplicative composition. ```python theme={null} generator = TSIGenerator( engine="polars", min_length=256, max_length=256, freq="h", seed=42, ) tsi_df = generator.generate(n_series=4) summary = tsi_df.group_by("unique_id").agg( pl.len().alias("length"), pl.col("y").mean().alias("mean"), pl.col("y").std().alias("std"), ) summary ``` | unique\_id | length | mean | std | | ---------- | ------ | --------- | -------- | | cat | u32 | f64 | f64 | | "3" | 256 | 9.960003 | 16.37748 | | "0" | 256 | -1.564618 | 0.675566 | | "1" | 256 | -4.0516 | 2.524986 | | "2" | 256 | 2.734903 | 1.325484 | ```python theme={null} fig, axes = plt.subplots(4, 1, figsize=(12, 9), sharex=True) for ax, uid in zip(axes, tsi_df["unique_id"].unique(maintain_order=True), strict=True): series = tsi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"], series["y"], linewidth=1) ax.set_title(str(uid), loc="left") ax.set_ylabel("y") axes[-1].set_xlabel("Timestamp") fig.suptitle("Randomized trend-seasonality-irregularity compositions") plt.tight_layout() plt.show() ``` ## Constrain the component pool Every component family can be narrowed when a dataset needs a more specific inductive bias. This example limits the pool to linear or damped trends, one or two seasonal components, and Gaussian or AR(1) irregularity. ```python theme={null} controlled = TSIGenerator( engine="polars", min_length=168, max_length=168, freq="h", trend_types=["linear", "damped"], n_seasonal_range=(1, 2), seasonal_periods=[12.0, 24.0, 168.0], irregular_types=["gaussian", "ar1"], seed=7, ) controlled.generate(n_series=2).head() ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 11.837247 | | "0" | 2000-01-01 01:00:00 | 17.178323 | | "0" | 2000-01-01 02:00:00 | 12.562518 | | "0" | 2000-01-01 03:00:00 | 17.547732 | | "0" | 2000-01-01 04:00:00 | 15.332925 | > **Related generators** > > * [TCM](tcm) — random causal-graph dynamics; > [KernelSynth](kernel_synth) — Gaussian-process kernel > compositions. > * [Balanced pool](../../capabilities/balanced_pool) — interpretable > single-mechanism generators for benchmarking. > > Full parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # ETS (exponential smoothing) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/statistical/ets.html ETS models describe a series through slowly evolving *states* — level, optional trend, optional seasonality — updated by exponential smoothing. The error/trend/season taxonomy spans 30 variants (additive or multiplicative, damped or not), so a single generator covers a broad family of realistic trend-plus-seasonality shapes. > **The model** > > $y_t = \mu_t + \varepsilon_t, \qquad \mu_t = \ell_{t-1} + \phi\, b_{t-1} + s_{t-m} \quad \text{(ETS(A,A,A))}$ > > Each series is built from a level state plus optional trend and > seasonal states, combined additively or multiplicatively via > `error_type`, `trend_type`, `seasonal_type`, and `damped`. This is the > state-space form behind Holt and Holt-Winters smoothing. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import ETSGenerator ``` ## 1. Taxonomy variants The error/trend/season triple selects the model. All three draws share a seed, a starting level, and a seasonal period, so the shapes differ only by taxonomy: a damped additive trend flattens out, a multiplicative season grows with the level, and no trend or season leaves pure level smoothing. ```python theme={null} damped_df = ETSGenerator( engine="polars", min_length=120, max_length=120, freq="ME", error_type="add", trend_type="add", seasonal_type="add", damped=True, phi=0.9, seasonal_period=12, level=100.0, trend=1.0, noise_std=2.0, seed=42, ).generate(n_series=1) multiplicative_df = ETSGenerator( engine="polars", min_length=120, max_length=120, freq="ME", error_type="mul", trend_type="add", seasonal_type="mul", seasonal_period=12, level=100.0, trend=1.0, noise_std=0.02, seed=42, ).generate(n_series=1) level_only_df = ETSGenerator( engine="polars", min_length=120, max_length=120, freq="ME", error_type="add", trend_type=None, seasonal_type=None, level=100.0, noise_std=2.0, seed=42, ).generate(n_series=1) panels = [ ("ETS(A,Ad,A) damped additive Holt-Winters", damped_df), ("ETS(M,A,M) multiplicative Holt-Winters", multiplicative_df), ("ETS(A,N,N) simple exponential smoothing", level_only_df), ] fig, axes = plt.subplots(3, 1, figsize=(12, 7.5), sharex=True) for ax, (label, df) in zip(axes, panels): ax.plot(df["ds"].to_list(), df["y"].to_list(), alpha=0.85, linewidth=1) ax.set(ylabel="Value", title=label) axes[-1].set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## 2. State export Generate data and extract the underlying level, trend, and seasonal state components. ```python theme={null} params_states = { "min_length": 50, "max_length": 50, "freq": "D", "error_type": "add", "trend_type": "add", "seasonal_type": "add", "seasonal_period": 7, "level": 100.0, "trend": 1.0, "alpha": 0.3, "beta": 0.1, "gamma": 0.1, "noise_std": 1.0, "seed": 42, } generator_states = ETSGenerator(engine="polars", **params_states) obs_df, states_df = generator_states.generate_with_states(n_series=1) ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in obs_df["unique_id"].unique().to_list(): series = obs_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("ETS(A,A,A) with state export") ax.legend() plt.tight_layout() plt.show() ``` ```python theme={null} print("Observations DataFrame:") obs_df.head(10) ``` ```text theme={null} Observations DataFrame: ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 101.852261 | | "0" | 2000-01-02 00:00:00 | 99.674429 | | "0" | 2000-01-03 00:00:00 | 103.997037 | | "0" | 2000-01-04 00:00:00 | 104.742658 | | "0" | 2000-01-05 00:00:00 | 99.842295 | | "0" | 2000-01-06 00:00:00 | 109.225432 | | "0" | 2000-01-07 00:00:00 | 109.215338 | | "0" | 2000-01-08 00:00:00 | 110.156755 | | "0" | 2000-01-09 00:00:00 | 106.862563 | | "0" | 2000-01-10 00:00:00 | 113.073648 | ```python theme={null} print("States DataFrame (level, trend, seasonal components):") states_df.head(10) ``` ```text theme={null} States DataFrame (level, trend, seasonal components): ``` | unique\_id | ds | level | trend | seasonal\_0 | seasonal\_1 | seasonal\_2 | seasonal\_3 | seasonal\_4 | seasonal\_5 | seasonal\_6 | | ---------- | ------------------- | ---------- | -------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | | cat | datetime\[ns] | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | | "0" | 2000-01-01 00:00:00 | 100.0 | 1.0 | 1.168504 | -2.182273 | 2.014922 | 0.402623 | -5.629283 | 3.185167 | 1.04034 | | "0" | 2000-01-02 00:00:00 | 100.905127 | 0.968376 | 1.136879 | -2.182273 | 2.014922 | 0.402623 | -5.629283 | 3.185167 | 1.04034 | | "0" | 2000-01-03 00:00:00 | 101.868463 | 0.966696 | 1.136879 | -2.183953 | 2.014922 | 0.402623 | -5.629283 | 3.185167 | 1.04034 | | "0" | 2000-01-04 00:00:00 | 102.579245 | 0.881391 | 1.136879 | -2.183953 | 1.929618 | 0.402623 | -5.629283 | 3.185167 | 1.04034 | | "0" | 2000-01-05 00:00:00 | 103.724456 | 0.969331 | 1.136879 | -2.183953 | 1.929618 | 0.490563 | -5.629283 | 3.185167 | 1.04034 | | "0" | 2000-01-06 00:00:00 | 104.927124 | 1.04711 | 1.136879 | -2.183953 | 1.929618 | 0.490563 | -5.551504 | 3.185167 | 1.04034 | | "0" | 2000-01-07 00:00:00 | 105.994044 | 1.053713 | 1.136879 | -2.183953 | 1.929618 | 0.490563 | -5.551504 | 3.19177 | 1.04034 | | "0" | 2000-01-08 00:00:00 | 107.385929 | 1.166437 | 1.136879 | -2.183953 | 1.929618 | 0.490563 | -5.551504 | 3.19177 | 1.153064 | | "0" | 2000-01-09 00:00:00 | 108.69262 | 1.213188 | 1.18363 | -2.183953 | 1.929618 | 0.490563 | -5.551504 | 3.19177 | 1.153064 | | "0" | 2000-01-10 00:00:00 | 109.64802 | 1.127259 | 1.18363 | -2.269882 | 1.929618 | 0.490563 | -5.551504 | 3.19177 | 1.153064 | ## 3. Box-Cox transformation Apply a Box-Cox transformation (lambda=0.5, square root) to the generated series. ```python theme={null} params_boxcox = { "min_length": 100, "max_length": 100, "freq": "D", "error_type": "add", "trend_type": "add", "seasonal_type": None, "level": 100.0, "trend": 0.5, "alpha": 0.3, "beta": 0.1, "noise_std": 0.5, "box_cox_lambda": 0.5, "seed": 111, } generator_boxcox = ETSGenerator(engine="polars", **params_boxcox) df_boxcox = generator_boxcox.generate(n_series=1) print(f"Model: {generator_boxcox.get_model_info()['model']}") print(f"Box-Cox lambda: {generator_boxcox.get_model_info()['box_cox_lambda']}") print(f"Mean: {df_boxcox['y'].mean():.2f}") print(f"Std: {df_boxcox['y'].std():.2f}") ``` ```text theme={null} Model: ETS(A,A,N) Box-Cox lambda: 0.5 Mean: 115.15 Std: 5.02 ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_boxcox["unique_id"].unique().to_list(): series = df_boxcox.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("ETS with Box-Cox transformation") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [SARIMA](sarima) — the ARIMA view of the same trend/seasonal > structure. > * [Seasonal](seasonal) — a fixed seasonal wave when you don’t need > evolving states. > > The full error/trend/season taxonomy is in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # INAR (integer counts) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/statistical/inar.html INAR — INteger-valued AutoRegression — generates *count* series: non-negative integers with autocorrelation, for demand, arrivals, and case counts where a Gaussian model makes no sense. It carries dependence forward through binomial thinning rather than a linear lag, so the output stays integer-valued. > **The model** > > $X_t = \alpha_1 \circ X_{t-1} + \dots + \alpha_p \circ X_{t-p} + \varepsilon_t, \qquad \alpha \circ X = \sum_{i=1}^{X} \mathrm{Bernoulli}(\alpha)$ > > Each step keeps a binomially-thinned fraction of the previous count > and adds a fresh integer innovation. The thinning probability sets the > persistence; the innovation distribution (`poisson` or > `negative_binomial`) sets the marginal spread — negative binomial for > overdispersion. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import INARGenerator ``` ## 1. Innovation distribution The innovation distribution sets the marginal spread. With the thinning parameter and innovation mean held fixed, negative binomial innovations produce overdispersion — variance above the mean — while Poisson innovations keep the two equal. ```python theme={null} poisson_df = INARGenerator( engine="polars", min_length=200, max_length=200, freq="D", p=1, alpha=[0.5], innovation_type="poisson", innovation_mean=3.0, seed=42, ).generate(n_series=1) negative_binomial_df = INARGenerator( engine="polars", min_length=200, max_length=200, freq="D", p=1, alpha=[0.5], innovation_type="negative_binomial", innovation_mean=3.0, innovation_dispersion=2.0, seed=42, ).generate(n_series=1) fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True) panels = [("poisson", poisson_df), ("negative binomial", negative_binomial_df)] for ax, (label, df) in zip(axes, panels): counts = df["y"].to_list() mean = sum(counts) / len(counts) variance = sum((count - mean) ** 2 for count in counts) / len(counts) ax.step(df["ds"].to_list(), counts, where="mid", alpha=0.85) ax.set( ylabel="Count", title=f"{label} innovations (mean {mean:.2f}, variance {variance:.2f})", ) axes[-1].set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## 2. Higher-order dependence INAR(2) models have memory over two lags, producing smoother count dynamics. ```python theme={null} inar2_gen = INARGenerator(engine="polars", min_length=200, max_length=200, freq="D", p=2, alpha=[0.3, 0.2], innovation_type="poisson", innovation_mean=2.0, seed=42, ) inar2_df = inar2_gen.generate(n_series=1) fig, ax = plt.subplots(figsize=(12, 4)) ax.step(inar2_df["ds"].to_list(), inar2_df["y"].to_list(), where="mid", alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Count") ax.set_title("INAR(2) with two-lag dependence") plt.tight_layout() plt.show() ``` ## 3. Multiple series ```python theme={null} multi_gen = INARGenerator(engine="polars", min_length=150, max_length=150, freq="D", p=1, alpha=[0.5], innovation_mean=3.0, seed=42, ) multi_df = multi_gen.generate(n_series=3) fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.step(series["ds"].to_list(), series["y"].to_list(), where="mid", label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Count") ax.set_title("Multiple INAR(1) series") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Intermittent demand](../domain/intermittent_demand) — sparse > counts with many zeros. > * [Poisson process](../stochastic/poisson_process) — event arrivals > in continuous time. > > Innovation and thinning parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Random walk Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/statistical/random_walk.html A random walk is the canonical non-stationary series: each value is the previous one plus a random step, optionally nudged by a constant drift. It is the process behind the *naive* forecast (tomorrow ≈ today), a first model of efficient-market prices, and a useful stress test for whether a pipeline handles trends and growing variance rather than assuming a stable mean. > **The model** > > $y_t = y_{t-1} + \mu + \sigma\,\varepsilon_t, \qquad \varepsilon_t \sim \mathcal{N}(0, 1)$ > > * `drift` ($\mu$) — the constant step added each period; a > deterministic trend. > * `volatility` ($\sigma$) — the standard deviation of the random > step. > * `start_value` ($y_0$) — where every series begins. > > Because steps accumulate, the variance grows with time — the series > has no fixed mean to revert to. ## Generate Instantiate the generator and draw a few series. All outputs are long-format (`unique_id`, `ds`, `y`). ```python theme={null} import matplotlib.pyplot as plt import polars as pl from synforecast.generators import RandomWalkGenerator def plot_panel(df, title): """Plot each series in a long-format panel on a shared axis.""" fig, ax = plt.subplots(figsize=(11, 4)) for uid in df["unique_id"].unique(maintain_order=True).to_list(): series = df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"], series["y"], linewidth=1, alpha=0.85, label=str(uid)) ax.set(title=title, xlabel="ds", ylabel="y") ax.legend(fontsize=8) plt.tight_layout() plt.show() generator = RandomWalkGenerator( engine="polars", min_length=200, max_length=200, freq="D", drift=0.1, volatility=1.5, start_value=100.0, seed=42, ) walks = generator.generate(n_series=3) plot_panel(walks, "Random walks (drift=0.1, volatility=1.5)") ``` The three series share the same process but a different noise draw. They all start at 100 and trend gently upward — that shared pull is the drift — while wandering by an amount set by the volatility. Notice how they fan out over time: that spreading is the growing variance a random walk always produces. ## Control the process `drift` sets the trend and `volatility` sets the noise. Holding volatility fixed, larger drift turns a flat wander into a clear trend. ```python theme={null} fig, ax = plt.subplots(figsize=(11, 4)) for drift in (0.0, 0.1, 0.4): series = RandomWalkGenerator( engine="polars", min_length=200, max_length=200, freq="D", drift=drift, volatility=1.0, start_value=100.0, seed=0, ).generate(n_series=1) ax.plot(series["ds"], series["y"], linewidth=1.2, label=f"drift={drift}") ax.set(title="Same noise seed, increasing drift", xlabel="ds", ylabel="y") ax.legend() plt.tight_layout() plt.show() ``` Sharing the seed isolates the effect: the wiggles are identical, but a larger drift lifts the whole path. Raise `volatility` instead and the paths would keep this trend while wandering further from it. ## Statistics by series Summary statistics vary widely between series even from one generator — a direct consequence of the accumulating, unbounded variance. ```python theme={null} walks.group_by("unique_id").agg( pl.len().alias("count"), pl.col("y").min().round(2).alias("min"), pl.col("y").max().round(2).alias("max"), pl.col("y").mean().round(2).alias("mean"), pl.col("y").std().round(2).alias("std"), ).sort("unique_id") ``` | unique\_id | count | min | max | mean | std | | ---------- | ----- | ----- | ------ | ------ | ----- | | cat | u32 | f64 | f64 | f64 | f64 | | "0" | 200 | 96.74 | 139.24 | 120.67 | 10.36 | | "1" | 200 | 90.58 | 130.9 | 115.29 | 10.56 | | "2" | 200 | 90.8 | 110.13 | 99.45 | 3.61 | > **Related generators** > > * [Geometric Brownian > motion](../stochastic/geometric_brownian_motion) — a > *multiplicative* random walk for strictly positive series like > prices. > * [SARIMA](sarima) with `d=1` — a random walk with added > autoregressive/moving-average structure. > * Add trend breaks or outliers with the > [changepoints](../../capabilities/changepoints) and > [anomalies](../../capabilities/anomalies) options. > > Every parameter is documented in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # SARIMA Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/statistical/sarima.html SARIMA — Seasonal AutoRegressive Integrated Moving Average — is the workhorse linear model for series with autocorrelation, trend, and seasonality. Generating from a *known* `(p, d, q)(P, D, Q)` specification lets you confirm a model recovers the order you put in, or build panels with a precise, well-understood dependence structure. > **The model** > > $\phi(B)\,\Phi(B^s)\,(1-B)^d (1-B^s)^D\, y_t = \theta(B)\,\Theta(B^s)\, \varepsilon_t$ > > An ARIMA(p, d, q) process combines `p` autoregressive lags, `d` orders > of differencing (for trend / unit roots), and `q` moving-average lags; > the seasonal part `(P, D, Q)` repeats that structure at the seasonal > period. Set the orders and coefficients to dial in anything from white > noise to a strongly seasonal, integrated series. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import SARIMAGenerator ``` ## 1. Basic SARIMA(2,1,1)(1,1,1)\_7 A SARIMA model with weekly seasonality, suitable for daily data. ```python theme={null} params = { "min_length": 200, "max_length": 200, "freq": "D", "p": 2, "d": 1, "q": 1, "P": 1, "D": 1, "Q": 1, "seasonal_period": 7, "noise_std": 2.0, "drift": 0.5, "seed": 42, } generator = SARIMAGenerator(engine="polars", **params) ``` ### Model information Inspect the auto-generated model parameters and polynomial structure. ```python theme={null} model_info = generator.get_model_info() print(f"Model: {model_info['model']}") print(f"AR parameters: {model_info['ar_params']}") print(f"MA parameters: {model_info['ma_params']}") print(f"Seasonal AR parameters: {model_info['seasonal_ar_params']}") print(f"Seasonal MA parameters: {model_info['seasonal_ma_params']}") print(f"Drift: {model_info['drift']}") print(f"Burn-in: {model_info['burn_in']}") print(f"\nExpanded AR polynomial lags: {model_info['full_ar_polynomial_lags']}") print(f"Expanded AR polynomial coeffs: {model_info['full_ar_polynomial_coeffs']}") ``` ```text theme={null} Model: SARIMA(2,1,1)(1,1,1)[7] AR parameters: [0.2298813963661166, -0.04889724819835817] MA parameters: [0.35859791991138246] Seasonal AR parameters: [0.15789442324749114] Seasonal MA parameters: [-0.40582265211235047] Drift: 0.5 Burn-in: 100 Expanded AR polynomial lags: [1, 2, 3, 4, 5, 6, 7, 8, 9] Expanded AR polynomial coeffs: [0.2298813963661166, -0.04889724819835817, -0.0, -0.0, -0.0, -0.0, 0.15789442324749114, -0.036296990494555884, 0.007720602802669188] ``` ### Generate and inspect data ```python theme={null} df = generator.generate(n_series=3) print(f"Generated {df['unique_id'].n_unique()} time series") print(f"Total observations: {len(df)}") df.head(10) ``` ```text theme={null} Generated 3 time series Total observations: 600 ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 840.568595 | | "0" | 2000-01-02 00:00:00 | 849.409081 | | "0" | 2000-01-03 00:00:00 | 852.256521 | | "0" | 2000-01-04 00:00:00 | 864.365821 | | "0" | 2000-01-05 00:00:00 | 890.874551 | | "0" | 2000-01-06 00:00:00 | 904.417479 | | "0" | 2000-01-07 00:00:00 | 912.95809 | | "0" | 2000-01-08 00:00:00 | 927.189939 | | "0" | 2000-01-09 00:00:00 | 941.521277 | | "0" | 2000-01-10 00:00:00 | 946.445261 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df["unique_id"].unique().to_list(): series = df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("SARIMA(2,1,1)(1,1,1)_7 series") ax.legend() plt.tight_layout() plt.show() ``` ### Statistics by series ```python theme={null} df.group_by("unique_id").agg( [ pl.col("y").count().alias("count"), pl.col("y").min().alias("min_value"), pl.col("y").max().alias("max_value"), pl.col("y").mean().alias("mean_value"), pl.col("y").std().alias("std_value"), ] ).sort("unique_id") ``` | unique\_id | count | min\_value | max\_value | mean\_value | std\_value | | ---------- | ----- | ---------- | ----------- | ----------- | ----------- | | cat | u32 | f64 | f64 | f64 | f64 | | "0" | 200 | 840.568595 | 4716.719654 | 2557.619461 | 1132.631603 | | "1" | 200 | 628.393 | 4135.85017 | 2221.617457 | 1020.787936 | | "2" | 200 | 452.878919 | 3437.538082 | 1778.004889 | 857.699315 | ## 2. Stationary ARMA(1,1) with custom parameters A stationary model with no differencing and explicit AR/MA coefficients. ```python theme={null} params_arma = { "min_length": 200, "max_length": 200, "freq": "D", "p": 1, "d": 0, "q": 1, "P": 0, "D": 0, "Q": 0, "ar_params": [0.7], "ma_params": [0.3], "mean": 50.0, "noise_std": 1.0, "seed": 123, } generator_arma = SARIMAGenerator(engine="polars", **params_arma) df_arma = generator_arma.generate(n_series=1) print(f"Model: {generator_arma.get_model_info()['model']}") print(f"Mean: {generator_arma.get_model_info()['mean']}") print(f"Series mean: {df_arma['y'].mean():.2f}") print(f"Series std: {df_arma['y'].std():.2f}") ``` ```text theme={null} Model: ARIMA(1,0,1) Mean: 50.0 Series mean: 50.35 Series std: 1.61 ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_arma["unique_id"].unique().to_list(): series = df_arma.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Stationary ARMA(1,1) series") ax.legend() plt.tight_layout() plt.show() ``` ## 3. Pure seasonal ARIMA(0,0,0)(1,1,1)\_12 A purely seasonal model with monthly frequency and 12-month seasonality. ```python theme={null} params_seasonal = { "min_length": 100, "max_length": 100, "freq": "MS", "p": 0, "d": 0, "q": 0, "P": 1, "D": 1, "Q": 1, "seasonal_period": 12, "seasonal_ar_params": [0.5], "seasonal_ma_params": [0.3], "noise_std": 1.0, "seed": 456, } generator_seasonal = SARIMAGenerator(engine="polars", **params_seasonal) df_seasonal = generator_seasonal.generate(n_series=1) print(f"Model: {generator_seasonal.get_model_info()['model']}") print("\nFirst 24 months:") df_seasonal.head(24) ``` ```text theme={null} Model: SARIMA(0,0,0)(1,1,1)[12] First 24 months: ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -0.846697 | | "0" | 2000-02-01 00:00:00 | 9.129227 | | "0" | 2000-03-01 00:00:00 | 7.281973 | | "0" | 2000-04-01 00:00:00 | -1.550024 | | "0" | 2000-05-01 00:00:00 | 4.399888 | | … | … | … | | "0" | 2001-08-01 00:00:00 | -4.021058 | | "0" | 2001-09-01 00:00:00 | 0.429695 | | "0" | 2001-10-01 00:00:00 | -3.538732 | | "0" | 2001-11-01 00:00:00 | -5.016018 | | "0" | 2001-12-01 00:00:00 | 9.350066 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df_seasonal["unique_id"].unique().to_list(): series = df_seasonal.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Pure seasonal ARIMA(0,0,0)(1,1,1)_12 series") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [ETS](ets) — the exponential-smoothing counterpart for trend and > seasonality. > * [Seasonal](seasonal) — a simpler fixed seasonal wave without ARMA > dynamics. > * [Random walk](random_walk) — the special case ARIMA(0, 1, 0). > > All orders and coefficients are documented in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Seasonal Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/statistical/seasonal.html A seasonal series is a repeating cycle riding on a trend — daily traffic, weekly sales, yearly demand. `SeasonalGenerator` builds one by adding a periodic wave, a linear trend, and observation noise, which makes it the natural test of whether a model captures periodicity and extrapolates the level. > **The model** > > $y_t = \text{base} + \text{trend}\cdot t + \text{amplitude}\cdot s\!\left(2\pi t / \text{period}\right) + \varepsilon_t$ > > * `seasonality_period` — length of one cycle in time steps (24 gives > a daily cycle on hourly data). > * `seasonality_amplitude` — height of the seasonal swing. > * `trend` — linear drift per step; `base_level` — the starting > level. > * `noise_level` — standard deviation of the additive noise. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import SeasonalGenerator ``` ## Define generator parameters Configure a seasonal generator with daily seasonality (24-hour period), a slight upward trend, and a base level of 50. ```python theme={null} params = { "min_length": 168, "max_length": 336, "freq": "h", "seasonality_period": 24, "seasonality_amplitude": 15.0, "trend": 0.05, "noise_level": 2.0, "base_level": 50.0, "seed": 123, } generator = SeasonalGenerator(engine="polars", **params) df = generator.generate(n_series=3) ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df["unique_id"].unique().to_list(): series = df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Seasonal series") ax.legend() plt.tight_layout() plt.show() ``` Every series repeats on a 24-step cycle around a gently rising level. The series have different lengths (`min_length` ≠ `max_length`) but share the same daily shape and trend — the seasonal structure a forecaster should learn. ## Inspect the generated data ```python theme={null} print(f"Generated {df['unique_id'].n_unique()} time series") print(f"Total observations: {len(df)}") df.head(10) ``` ```text theme={null} Generated 3 time series Total observations: 721 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 48.634355 | | "0" | 2000-01-01 01:00:00 | 51.658968 | | "0" | 2000-01-01 02:00:00 | 56.909722 | | "0" | 2000-01-01 03:00:00 | 59.2974 | | "0" | 2000-01-01 04:00:00 | 62.751023 | | "0" | 2000-01-01 05:00:00 | 65.499531 | | "0" | 2000-01-01 06:00:00 | 67.897628 | | "0" | 2000-01-01 07:00:00 | 63.845217 | | "0" | 2000-01-01 08:00:00 | 65.0017 | | "0" | 2000-01-01 09:00:00 | 62.627031 | ## Statistics by series ```python theme={null} df.group_by("unique_id").agg( [ pl.col("y").count().alias("count"), pl.col("y").min().alias("min_value"), pl.col("y").max().alias("max_value"), pl.col("y").mean().alias("mean_value"), pl.col("y").std().alias("std_value"), ] ) ``` | unique\_id | count | min\_value | max\_value | mean\_value | std\_value | | ---------- | ----- | ---------- | ---------- | ----------- | ---------- | | cat | u32 | f64 | f64 | f64 | f64 | | "2" | 268 | 32.718865 | 78.747915 | 56.740124 | 11.285512 | | "0" | 170 | 35.600282 | 75.837123 | 54.621329 | 10.619455 | | "1" | 283 | 34.431384 | 79.936749 | 57.388507 | 11.255673 | ## Sample of one series View the first 24 hours of a single series to see the seasonal pattern. ```python theme={null} df.filter(pl.col("unique_id") == "0").head(24) ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 48.634355 | | "0" | 2000-01-01 01:00:00 | 51.658968 | | "0" | 2000-01-01 02:00:00 | 56.909722 | | "0" | 2000-01-01 03:00:00 | 59.2974 | | "0" | 2000-01-01 04:00:00 | 62.751023 | | … | … | … | | "0" | 2000-01-01 19:00:00 | 42.030839 | | "0" | 2000-01-01 20:00:00 | 38.652835 | | "0" | 2000-01-01 21:00:00 | 43.811501 | | "0" | 2000-01-01 22:00:00 | 45.388067 | | "0" | 2000-01-01 23:00:00 | 47.831552 | > **Related generators** > > * [SARIMA](sarima) — seasonality with autoregressive/moving-average > dynamics instead of a fixed wave. > * [ETS](ets) — seasonality through exponential-smoothing state-space > models. > * Layer on [anomalies](../../capabilities/anomalies) or > [changepoints](../../capabilities/changepoints) to stress-test a > seasonal model. # Bounded process (proportions and rates) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/bounded_process.html Many series live inside fixed bounds — proportions, utilization rates, market shares that must stay in \[0, 1]. A bounded process keeps values inside the interval while still allowing autocorrelation and mean reversion, unlike an unbounded random walk that would eventually escape. > **The model** > > $\mu_t = \omega + \phi\, x_{t-1}, \qquad x_t \sim \mathrm{Beta}\!\big(\mu_t \kappa,\, (1-\mu_t)\kappa\big)$ > > Pick `model="beta_ar"` (a Beta-distributed autoregression that > mean-reverts inside the interval) or `model="logit_normal"` (a random > walk in logit space mapped back to (0, 1)). Both guarantee valid > bounded output with tunable persistence. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import BoundedProcessGenerator ``` ## 1. Model choice Both models stay inside the interval, by different routes: `beta_ar` mean-reverts through a conditional Beta draw, while `logit_normal` runs an AR(1) in logit space and maps back. The dashed lines mark the bounds neither can cross. ```python theme={null} beta_df = BoundedProcessGenerator( engine="polars", min_length=200, max_length=200, freq="D", model="beta_ar", phi=0.8, omega=0.1, kappa=20.0, seed=42, ).generate(n_series=1) logit_df = BoundedProcessGenerator( engine="polars", min_length=200, max_length=200, freq="D", model="logit_normal", phi=0.9, sigma=0.5, seed=42, ).generate(n_series=1) fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True) panels = [("beta_ar", beta_df), ("logit_normal", logit_df)] for ax, (label, df) in zip(axes, panels): ax.plot(df["ds"].to_list(), df["y"].to_list(), alpha=0.85) ax.axhline(0, color="gray", linestyle="--", alpha=0.4) ax.axhline(1, color="gray", linestyle="--", alpha=0.4) ax.set(ylim=(-0.05, 1.05), ylabel="Value", title=f'model="{label}"') axes[-1].set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## 2. Custom bounds Scale the output to any `[lower, upper]` interval, e.g., temperature in \[15, 35]. ```python theme={null} temp_gen = BoundedProcessGenerator(engine="polars", min_length=200, max_length=200, freq="D", model="beta_ar", phi=0.85, omega=0.1, kappa=30.0, lower=15.0, upper=35.0, seed=42, ) temp_df = temp_gen.generate(n_series=1) print(f"Min={temp_df['y'].min():.2f}, Max={temp_df['y'].max():.2f}") fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(temp_df["ds"].to_list(), temp_df["y"].to_list(), alpha=0.8) ax.axhline(y=15, color="blue", linestyle="--", alpha=0.3, label="lower bound") ax.axhline(y=35, color="red", linestyle="--", alpha=0.3, label="upper bound") ax.set_xlabel("Timestamp") ax.set_ylabel("Temperature") ax.set_title("Bounded process with custom bounds [15, 35]") ax.legend() plt.tight_layout() plt.show() ``` ```text theme={null} Min=21.01, Max=34.23 ``` ## 3. Multiple series ```python theme={null} multi_gen = BoundedProcessGenerator(engine="polars", min_length=150, max_length=150, freq="D", model="beta_ar", phi=0.7, seed=42, ) multi_df = multi_gen.generate(n_series=3) fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple bounded process series") ax.set_ylim(-0.05, 1.05) ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Ornstein-Uhlenbeck](ornstein_uhlenbeck) — unbounded mean > reversion. > * [INAR](../statistical/inar) — bounded-below integer counts. > > Model choices and parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Chaotic systems Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/chaotic_system.html Chaotic systems are *deterministic* yet unpredictable: fully specified equations whose sensitivity to initial conditions produces series that look random but have rich hidden structure. They test whether a model captures nonlinear dynamics rather than just fitting noise. > **The model** > > $x_{n+1} = r\, x_n (1 - x_n) \quad \text{(logistic map; \texttt{lorenz} and \texttt{mackey\_glass} differ)}$ > > Choose a classic system via `system` — `lorenz` (a strange attractor), > `logistic` (the period-doubling map), or `mackey_glass` (a > delay-differential equation). Each is generated by iterating its exact > dynamics, so the same seed and parameters reproduce the trajectory > precisely. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import ChaoticSystemGenerator ``` ## 1. The three systems Each system is fully deterministic given its initial condition, yet none looks periodic. `lorenz` is a continuous attractor sampled once per time unit, `logistic` is a period-doubling map, and `mackey_glass` is a delay-differential equation. ```python theme={null} lorenz_df = ChaoticSystemGenerator( engine="polars", min_length=500, max_length=500, freq="D", system="lorenz", observation_noise=0.1, seed=42, ).generate(n_series=1) logistic_df = ChaoticSystemGenerator( engine="polars", min_length=500, max_length=500, freq="D", system="logistic", logistic_r=3.9, observation_noise=0.01, seed=42, ).generate(n_series=1) mackey_glass_df = ChaoticSystemGenerator( engine="polars", min_length=500, max_length=500, freq="D", system="mackey_glass", mg_tau=17, observation_noise=0.01, seed=42, ).generate(n_series=1) panels = [ ("lorenz (x-component)", lorenz_df), ("logistic map (r=3.9)", logistic_df), ("mackey_glass (tau=17)", mackey_glass_df), ] fig, axes = plt.subplots(3, 1, figsize=(12, 7.5), sharex=False) for ax, (label, df) in zip(axes, panels): ax.plot(df["ds"].to_list(), df["y"].to_list(), alpha=0.85, linewidth=1) ax.set(ylabel="Value", title=label) axes[-1].set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## 2. Sensitivity to initial conditions Multiple series from the same chaotic system diverge due to tiny differences in initial perturbations. ```python theme={null} multi_gen = ChaoticSystemGenerator(engine="polars", min_length=200, max_length=200, freq="D", system="lorenz", seed=42, ) multi_df = multi_gen.generate(n_series=3) fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8, linewidth=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple Lorenz series (sensitive to initial conditions)") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Cyclic](cyclic) — irregular but stochastic oscillation. > * [State space](../domain/state_space) — custom > deterministic-plus-noise dynamics. > > Available systems and parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Cyclic (irregular cycles) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/cyclic.html Cyclic series oscillate, but — unlike a clean seasonal wave — with *irregular* periods and amplitudes: business cycles, ecological booms and busts, quasi-periodic signals. The generator overlays one or more noisy cycles so the period drifts rather than repeating exactly. > **The model** > > $y_t = \text{base} + \text{trend}\cdot t + \sum_{i=1}^{n} A_i \sin\!\big(\psi_i(t)\big) + \varepsilon_t$ > > Each series superimposes `num_cycles` oscillations whose periods vary > from cycle to cycle (`cycle_period_std` controls how irregular). Small > variance approaches a fixed seasonal wave; large variance produces > genuinely aperiodic cycling. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import CyclicGenerator ``` ## 1. Cycle length `cycle_period_mean` sets the average cycle length in steps. The period still drifts within each series, which is what separates a cycle from a fixed seasonal wave. ```python theme={null} base = { "min_length": 400, "max_length": 400, "freq": "D", "base_level": 100.0, "trend": 0.0, "cycle_amplitude_mean": 20.0, "num_cycles": 1, "noise_std": 2.0, "seed": 42, } fig, ax = plt.subplots(figsize=(12, 4)) for period in (30.0, 90.0, 180.0): df = CyclicGenerator( engine="polars", cycle_period_mean=period, **base ).generate(n_series=1) ax.plot(df["ds"].to_list(), df["y"].to_list(), label=f"period={period:.0f}", alpha=0.85) ax.set(xlabel="Timestamp", ylabel="Value", title="Average cycle length in steps") ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## 2. Cycle irregularity `cycle_period_std` controls how much the period wanders. At zero the series is close to a fixed seasonal wave; large values give genuinely aperiodic cycling. ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for period_std in (0.0, 10.0, 40.0): df = CyclicGenerator( engine="polars", min_length=400, max_length=400, freq="D", base_level=100.0, trend=0.0, cycle_period_mean=90.0, cycle_period_std=period_std, cycle_amplitude_mean=20.0, num_cycles=1, noise_std=2.0, seed=42, ).generate(n_series=1) ax.plot( df["ds"].to_list(), df["y"].to_list(), label=f"period_std={period_std:.0f}", alpha=0.85 ) ax.set(xlabel="Timestamp", ylabel="Value", title="Larger period_std makes cycles aperiodic") ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## 3. Negative trend A downward trend combined with cyclical fluctuations. ```python theme={null} declining_params = { "min_length": 300, "max_length": 300, "freq": "D", "base_level": 150.0, "trend": -0.05, "cycle_period_mean": 50.0, "cycle_amplitude_mean": 20.0, "num_cycles": 3, "seed": 42, } declining_gen = CyclicGenerator(engine="polars", **declining_params) declining_df = declining_gen.generate(n_series=1) print(f"Generated {len(declining_df)} observations with declining trend") print(f"Statistics: Mean={declining_df['y'].mean():.4f}, Std={declining_df['y'].std():.4f}") declining_df.head(10) ``` ```text theme={null} Generated 300 observations with declining trend Statistics: Mean=140.8307, Std=27.3867 ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 143.101924 | | "0" | 2000-01-02 00:00:00 | 141.627292 | | "0" | 2000-01-03 00:00:00 | 136.638082 | | "0" | 2000-01-04 00:00:00 | 134.529648 | | "0" | 2000-01-05 00:00:00 | 133.656044 | | "0" | 2000-01-06 00:00:00 | 129.039617 | | "0" | 2000-01-07 00:00:00 | 128.071289 | | "0" | 2000-01-08 00:00:00 | 128.080343 | | "0" | 2000-01-09 00:00:00 | 129.365766 | | "0" | 2000-01-10 00:00:00 | 128.117344 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in declining_df["unique_id"].unique().to_list(): series = declining_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Negative trend with cycles (declining market)") ax.legend() plt.tight_layout() plt.show() ``` ## 4. Overlapping cycles Five overlapping cycle components create a complex, realistic pattern. ```python theme={null} complex_params = { "min_length": 500, "max_length": 500, "freq": "D", "base_level": 100.0, "trend": 0.02, "cycle_period_mean": 60.0, "cycle_amplitude_mean": 15.0, "num_cycles": 5, "seed": 42, } complex_gen = CyclicGenerator(engine="polars", **complex_params) complex_df = complex_gen.generate(n_series=1) print(f"Generated {len(complex_df)} observations with multiple overlapping cycles") print(f"Statistics: Mean={complex_df['y'].mean():.4f}, Std={complex_df['y'].std():.4f}") complex_df.head(10) ``` ```text theme={null} Generated 500 observations with multiple overlapping cycles Statistics: Mean=105.8392, Std=43.9463 ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 126.751732 | | "0" | 2000-01-02 00:00:00 | 127.885824 | | "0" | 2000-01-03 00:00:00 | 129.749177 | | "0" | 2000-01-04 00:00:00 | 128.563635 | | "0" | 2000-01-05 00:00:00 | 130.119181 | | "0" | 2000-01-06 00:00:00 | 127.31363 | | "0" | 2000-01-07 00:00:00 | 126.418862 | | "0" | 2000-01-08 00:00:00 | 125.266044 | | "0" | 2000-01-09 00:00:00 | 124.954641 | | "0" | 2000-01-10 00:00:00 | 125.315938 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in complex_df["unique_id"].unique().to_list(): series = complex_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple overlapping cycles (complex pattern)") ax.legend() plt.tight_layout() plt.show() ``` ## 5. Multiple series Generate multiple independent cyclic series in one call. ```python theme={null} multi_params = { "min_length": 200, "max_length": 200, "freq": "D", "base_level": 100.0, "trend": 0.03, "cycle_period_mean": 60.0, "cycle_amplitude_mean": 20.0, "num_cycles": 3, "seed": 42, } multi_gen = CyclicGenerator(engine="polars", **multi_params) multi_df = multi_gen.generate(n_series=3) print(f"Generated 3 series with {len(multi_df)} total observations") print(f"Overall Statistics: Mean={multi_df['y'].mean():.4f}, Std={multi_df['y'].std():.4f}") multi_df.filter(pl.col("unique_id") == "0").head(10) ``` ```text theme={null} Generated 3 series with 600 total observations Overall Statistics: Mean=103.3095, Std=24.7829 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 93.663885 | | "0" | 2000-01-02 00:00:00 | 92.67384 | | "0" | 2000-01-03 00:00:00 | 88.006548 | | "0" | 2000-01-04 00:00:00 | 86.059544 | | "0" | 2000-01-05 00:00:00 | 85.195164 | | "0" | 2000-01-06 00:00:00 | 80.448824 | | "0" | 2000-01-07 00:00:00 | 79.228091 | | "0" | 2000-01-08 00:00:00 | 78.881414 | | "0" | 2000-01-09 00:00:00 | 79.728552 | | "0" | 2000-01-10 00:00:00 | 77.980928 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple cyclic series") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Seasonal](../statistical/seasonal) — a fixed, exactly repeating > period. > * [Gaussian process](../multivariate/gaussian_process) — periodic > kernels for smooth quasi-periodicity. > > Cycle-count and irregularity parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Fractional Brownian motion (long memory) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/fractional_brownian_motion.html Fractional Brownian motion generalizes the random walk with a tunable *memory*: increments can be persistent (trending) or anti-persistent (mean-reverting), governed by the Hurst exponent. It models long-range dependence in network traffic, hydrology, and finance. > **The model** > > $\gamma(k) = \tfrac{\sigma^2}{2}\Big(|k+1|^{2H} - 2|k|^{2H} + |k-1|^{2H}\Big), \qquad \mathrm{Var}\,B_H(t) = \sigma^2 t^{2H}$ > > The Hurst exponent `hurst` ∈ (0, 1) sets the correlation of > increments: `H = 0.5` is ordinary Brownian motion, `H > 0.5` is > persistent (long-range positive dependence, smooth trends), and > `H < 0.5` is anti-persistent (rough, mean-reverting). ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import FractionalBrownianMotionGenerator ``` ## Comparing Hurst exponents Generate series with different Hurst exponents and estimate the Hurst parameter back from the data using the R/S method. ```python theme={null} hurst_values = [0.2, 0.5, 0.8] behaviors = [ "Anti-persistent (mean-reverting)", "Standard BM", "Persistent (trending)", ] for hurst, behavior in zip(hurst_values, behaviors): params = { "min_length": 200, "max_length": 200, "freq": "D", "hurst": hurst, "sigma": 1.0, "method": "cholesky", "seed": 42, } generator = FractionalBrownianMotionGenerator(engine="polars", **params) df = generator.generate(n_series=1) values = df["y"].to_numpy() estimated_h = generator.estimate_hurst(values, method="rs") print(f"Hurst = {hurst}: {behavior}") print(f" True Hurst: {hurst}") print(f" Estimated Hurst (R/S): {estimated_h:.3f}") stats = df.group_by("unique_id").agg( [ pl.col("y").min().alias("min"), pl.col("y").max().alias("max"), pl.col("y").mean().alias("mean"), pl.col("y").std().alias("std"), ] ) print(f" Statistics: {stats.to_dicts()[0]}\n") ``` ```text theme={null} Hurst = 0.2: Anti-persistent (mean-reverting) True Hurst: 0.2 Estimated Hurst (R/S): 0.980 Statistics: {'unique_id': '0', 'min': -3.2475141838303783, 'max': 5.725301335783486, 'mean': 0.7914070582630771, 'std': 1.7315551152633275} Hurst = 0.5: Standard BM True Hurst: 0.5 Estimated Hurst (R/S): 0.990 Statistics: {'unique_id': '0', 'min': -3.821319993238327, 'max': 18.890232027057795, 'mean': 7.077249598478712, 'std': 5.526131141406723} Hurst = 0.8: Persistent (trending) True Hurst: 0.8 Estimated Hurst (R/S): 0.990 Statistics: {'unique_id': '0', 'min': -6.585931552089723, 'max': 46.593447636080874, 'mean': 21.059286673082482, 'std': 15.403023148107025} ``` ```python theme={null} fig, axes = plt.subplots(1, 3, figsize=(16, 4)) for ax, (hurst, behavior) in zip(axes, zip(hurst_values, behaviors)): params = { "min_length": 200, "max_length": 200, "freq": "D", "hurst": hurst, "sigma": 1.0, "method": "cholesky", "seed": 42, } generator = FractionalBrownianMotionGenerator(engine="polars", **params) df = generator.generate(n_series=1) ax.plot(df["ds"].to_list(), df["y"].to_list(), alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title(f"H={hurst} ({behavior})") plt.suptitle("Fractional Brownian motion — Hurst exponent comparison") plt.tight_layout() plt.show() ``` ## Comparing fBm vs fGn (Increments) Fractional Brownian motion (fBm) is the cumulative process, while fractional Gaussian noise (fGn) represents its increments. ```python theme={null} fbm_gen = FractionalBrownianMotionGenerator(engine="polars", **{ "min_length": 200, "max_length": 200, "freq": "D", "hurst": 0.7, "return_increments": False, "seed": 42, } ) fbm_df = fbm_gen.generate(n_series=1) fgn_gen = FractionalBrownianMotionGenerator(engine="polars", **{ "min_length": 200, "max_length": 200, "freq": "D", "hurst": 0.7, "return_increments": True, "seed": 42, } ) fgn_df = fgn_gen.generate(n_series=1) print(f"fBm mean: {fbm_df['y'].mean():.3f}, std: {fbm_df['y'].std():.3f}") print(f"fGn mean: {fgn_df['y'].mean():.3f}, std: {fgn_df['y'].std():.3f}") ``` ```text theme={null} fBm mean: -16.055, std: 11.956 fGn mean: -0.231, std: 0.959 ``` ```python theme={null} fig, axes = plt.subplots(1, 2, figsize=(14, 4)) axes[0].plot(fbm_df["ds"].to_list(), fbm_df["y"].to_list(), alpha=0.8) axes[0].set_xlabel("Timestamp") axes[0].set_ylabel("Value") axes[0].set_title("Fractional Brownian motion (cumulative)") axes[1].plot(fgn_df["ds"].to_list(), fgn_df["y"].to_list(), alpha=0.8, color="tab:orange") axes[1].set_xlabel("Timestamp") axes[1].set_ylabel("Value") axes[1].set_title("Fractional Gaussian noise (increments)") plt.suptitle("fBm vs fGn (H=0.7)") plt.tight_layout() plt.show() ``` ## Model information Inspect the model parameters and metadata. ```python theme={null} info = fbm_gen.get_model_info() for key, value in info.items(): print(f"{key}: {value}") ``` ```text theme={null} hurst_exponent: 0.7 sigma: 1.0 behavior: persistent (trending) long_range_dependence_exponent: 0.3999999999999999 method: fft return_increments: False ``` > **Related generators** > > * [Random walk](../statistical/random_walk) — the `H = 0.5` special > case. > * [Ornstein-Uhlenbeck](ornstein_uhlenbeck) — a different route to > mean reversion. > > The Hurst parameter is documented in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # GARCH (volatility clustering) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/garch.html GARCH is the standard model for *volatility clustering* — calm stretches and turbulent stretches that group together, as in financial returns. The series itself is roughly uncorrelated, but its variance is autocorrelated, which is exactly what fools models that assume constant noise. > **The model** > > The conditional variance follows > $\sigma_t^2 = \omega + \sum\alpha_i\,\varepsilon_{t-i}^2 + \sum\beta_j\,\sigma_{t-j}^2$: > today’s variance depends on recent squared shocks (`alpha`) and recent > variance (`beta`). Higher `alpha + beta` means more persistent > volatility bursts. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import GARCHGenerator ``` ## 1. Basic GARCH(1,1) model A standard GARCH(1,1) with default alpha/beta and moderate base volatility. ```python theme={null} basic_params = { "min_length": 200, "max_length": 200, "freq": "D", "p": 1, "q": 1, "omega": 0.1, "seed": 42, } basic_gen = GARCHGenerator(engine="polars", **basic_params) basic_df = basic_gen.generate(n_series=1) print(f"Generated {len(basic_df)} observations with GARCH(1,1)") print(f"Statistics: Mean={basic_df['y'].mean():.4f}, Std={basic_df['y'].std():.4f}") basic_df.head(10) ``` ```text theme={null} Generated 200 observations with GARCH(1,1) Statistics: Mean=-0.0104, Std=0.4302 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -0.222789 | | "0" | 2000-01-02 00:00:00 | -0.4579 | | "0" | 2000-01-03 00:00:00 | -0.040794 | | "0" | 2000-01-04 00:00:00 | -0.036292 | | "0" | 2000-01-05 00:00:00 | 0.476476 | | "0" | 2000-01-06 00:00:00 | 0.109476 | | "0" | 2000-01-07 00:00:00 | -0.37548 | | "0" | 2000-01-08 00:00:00 | -0.082258 | | "0" | 2000-01-09 00:00:00 | 0.677303 | | "0" | 2000-01-10 00:00:00 | -1.093879 | ```python theme={null} fig, (ax_returns, ax_vol) = plt.subplots(2, 1, figsize=(12, 6), sharex=True) for uid in basic_df["unique_id"].unique().to_list(): series = basic_df.filter(pl.col("unique_id") == uid) ax_returns.plot( series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8, linewidth=0.9 ) rolling = series.with_columns(pl.col("y").rolling_std(window_size=20).alias("sigma")) ax_vol.plot(rolling["ds"].to_list(), rolling["sigma"].to_list(), alpha=0.8) ax_returns.set(ylabel="Return", title="Basic GARCH(1,1) series") ax_returns.legend(fontsize=8) ax_vol.set( xlabel="Timestamp", ylabel="Rolling std (20 steps)", title="Volatility clustering: calm and turbulent stretches group together", ) plt.tight_layout() plt.show() ``` ## 2. High volatility GARCH Increase the base volatility parameter `omega` to produce larger fluctuations. ```python theme={null} high_vol_params = { "min_length": 200, "max_length": 200, "freq": "D", "p": 1, "q": 1, "omega": 0.5, "seed": 42, } high_vol_gen = GARCHGenerator(engine="polars", **high_vol_params) high_vol_df = high_vol_gen.generate(n_series=1) print(f"Generated {len(high_vol_df)} observations with high base volatility") print(f"Statistics: Mean={high_vol_df['y'].mean():.4f}, Std={high_vol_df['y'].std():.4f}") high_vol_df.head(10) ``` ```text theme={null} Generated 200 observations with high base volatility Statistics: Mean=-0.0233, Std=0.9620 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -0.498172 | | "0" | 2000-01-02 00:00:00 | -1.023896 | | "0" | 2000-01-03 00:00:00 | -0.091218 | | "0" | 2000-01-04 00:00:00 | -0.081152 | | "0" | 2000-01-05 00:00:00 | 1.065434 | | "0" | 2000-01-06 00:00:00 | 0.244796 | | "0" | 2000-01-07 00:00:00 | -0.839599 | | "0" | 2000-01-08 00:00:00 | -0.183934 | | "0" | 2000-01-09 00:00:00 | 1.514496 | | "0" | 2000-01-10 00:00:00 | -2.445987 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in high_vol_df["unique_id"].unique().to_list(): series = high_vol_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("High volatility GARCH series") ax.legend() plt.tight_layout() plt.show() ``` ## 3. Multiple GARCH series Generate multiple independent GARCH series in one call. ```python theme={null} multi_params = { "min_length": 150, "max_length": 150, "freq": "D", "p": 1, "q": 1, "omega": 0.1, "seed": 42, } multi_gen = GARCHGenerator(engine="polars", **multi_params) multi_df = multi_gen.generate(n_series=3) print(f"Generated 3 series with {len(multi_df)} total observations") print(f"Overall Statistics: Mean={multi_df['y'].mean():.4f}, Std={multi_df['y'].std():.4f}") multi_df.filter(pl.col("unique_id") == "0").head(10) ``` ```text theme={null} Generated 3 series with 450 total observations Overall Statistics: Mean=0.0004, Std=0.4641 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -0.222789 | | "0" | 2000-01-02 00:00:00 | -0.4579 | | "0" | 2000-01-03 00:00:00 | -0.040794 | | "0" | 2000-01-04 00:00:00 | -0.036292 | | "0" | 2000-01-05 00:00:00 | 0.476476 | | "0" | 2000-01-06 00:00:00 | 0.109476 | | "0" | 2000-01-07 00:00:00 | -0.37548 | | "0" | 2000-01-08 00:00:00 | -0.082258 | | "0" | 2000-01-09 00:00:00 | 0.677303 | | "0" | 2000-01-10 00:00:00 | -1.093879 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple GARCH series") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Stochastic volatility](stochastic_volatility) — latent-process > volatility (Heston / SABR) rather than GARCH recursion. > * [Levy process](levy_process) — heavy tails without the clustering. > > Coefficient parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Geometric Brownian motion Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/geometric_brownian_motion.html Geometric Brownian motion (GBM) is the standard model for strictly positive, compounding quantities — asset prices, populations, anything that grows *multiplicatively*. Its log grows as a drifting random walk, so the level is log-normal and never goes negative. > **The model** > > $dS_t = \mu\,S_t\,dt + \sigma\,S_t\,dW_t$ > > Returns are proportional to the current level: `drift` (`mu`) sets the > exponential growth rate and `volatility` (`sigma`) the multiplicative > noise. This is the process behind Black-Scholes option pricing. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import GeometricBrownianMotionGenerator ``` ## 1. Drift `drift` is the exponential growth rate. The seed is shared, so the three paths have identical noise and differ only in trend direction. ```python theme={null} base = { "min_length": 200, "max_length": 200, "freq": "D", "sigma": 0.2, "initial_value": 100.0, "seed": 42, } fig, ax = plt.subplots(figsize=(12, 4)) for mu in (-0.05, 0.05, 0.15): df = GeometricBrownianMotionGenerator( engine="polars", mu=mu, **base ).generate(n_series=1) ax.plot(df["ds"].to_list(), df["y"].to_list(), label=f"drift={mu}", alpha=0.85) ax.set( xlabel="Timestamp", ylabel="Price", title="Drift sets the growth rate; the paths never cross zero", ) ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## 2. Volatility `volatility` scales the multiplicative noise. Because the process is multiplicative, the spread grows with the level rather than staying constant. ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for sigma in (0.1, 0.2, 0.5): df = GeometricBrownianMotionGenerator( engine="polars", min_length=200, max_length=200, freq="D", mu=0.05, sigma=sigma, initial_value=100.0, seed=42, ).generate(n_series=1) ax.plot(df["ds"].to_list(), df["y"].to_list(), label=f"volatility={sigma}", alpha=0.85) ax.set( xlabel="Timestamp", ylabel="Price", title="Volatility scales the multiplicative noise", ) ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## 3. Multiple paths Generate 5 independent GBM paths in one call. ```python theme={null} multi_params = { "min_length": 150, "max_length": 150, "freq": "D", "mu": 0.05, "sigma": 0.2, "initial_value": 100.0, "seed": 42, } multi_gen = GeometricBrownianMotionGenerator(engine="polars", **multi_params) multi_df = multi_gen.generate(n_series=5) print(f"Generated 5 stock price simulations with {len(multi_df)} total observations") print(f"Overall Statistics: Mean={multi_df['y'].mean():.4f}, Min={multi_df['y'].min():.4f}, Max={multi_df['y'].max():.4f}") multi_df.filter(pl.col("unique_id") == "0").head(10) ``` ```text theme={null} Generated 5 stock price simulations with 750 total observations Overall Statistics: Mean=33529.5984, Min=39.7278, Max=1355600.3980 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 100.0 | | "0" | 2000-01-02 00:00:00 | 77.855257 | | "0" | 2000-01-03 00:00:00 | 66.931005 | | "0" | 2000-01-04 00:00:00 | 83.485543 | | "0" | 2000-01-05 00:00:00 | 92.09352 | | "0" | 2000-01-06 00:00:00 | 85.875252 | | "0" | 2000-01-07 00:00:00 | 77.473016 | | "0" | 2000-01-08 00:00:00 | 90.038957 | | "0" | 2000-01-09 00:00:00 | 86.666449 | | "0" | 2000-01-10 00:00:00 | 92.367284 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Price") ax.set_title("GBM: multiple stock price simulations") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Random walk](../statistical/random_walk) — the *additive* > counterpart. > * [Jump diffusion](jump_diffusion) — GBM plus sudden jumps. > > Drift and volatility parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Hawkes process (self-excitation) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/hawkes_process.html A Hawkes process is a *self-exciting* arrival process: each event temporarily raises the probability of further events, producing the clustered bursts seen in trades, earthquakes, and social-media cascades. It is the natural counterpoint to the memoryless Poisson process. > **The model** > > $\lambda(t) = \mu + \sum_{t_i \le t} g(t - t_i), \qquad g(t) = \alpha e^{-\beta t}$ > > The arrival intensity is a baseline rate plus a decaying kick after > every event, so activity begets activity. The excitation strength and > decay set how tightly events cluster and how long a burst lasts; below > a stability threshold the process stays finite. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import HawkesProcessGenerator ``` ## 1. Event counts (default output) Generate event count time series with a baseline intensity and self-excitation. ```python theme={null} params = { "min_length": 200, "max_length": 200, "freq": "h", "baseline_intensity": 1.0, "excitation_amplitude": 0.5, "decay_rate": 2.0, "output_type": "counts", "seed": 42, } generator = HawkesProcessGenerator(engine="polars", **params) df = generator.generate(n_series=3) print(f"Generated {df['unique_id'].n_unique()} time series") print(f"Total observations: {len(df)}") print(f"Total events: {df['y'].sum()}") stats = df.group_by("unique_id").agg( [ pl.col("y").sum().alias("total_events"), pl.col("y").mean().alias("mean_per_hour"), pl.col("y").max().alias("max_in_hour"), ] ) stats ``` ```text theme={null} Generated 3 time series Total observations: 600 Total events: 836.0 ``` | unique\_id | total\_events | mean\_per\_hour | max\_in\_hour | | ---------- | ------------- | --------------- | ------------- | | cat | f64 | f64 | f64 | | "0" | 286.0 | 1.43 | 6.0 | | "2" | 245.0 | 1.225 | 7.0 | | "1" | 305.0 | 1.525 | 7.0 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df["unique_id"].unique().to_list(): series = df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Event Count") ax.set_title("Hawkes process — event counts") ax.legend() plt.tight_layout() plt.show() ``` ## 2. Intensity process Output the underlying intensity function instead of event counts. ```python theme={null} intensity_gen = HawkesProcessGenerator(engine="polars", **{ "min_length": 100, "max_length": 100, "freq": "h", "baseline_intensity": 0.5, "excitation_amplitude": 0.3, "decay_rate": 1.0, "output_type": "intensity", "seed": 42, } ) intensity_df = intensity_gen.generate(n_series=1) print(f"Intensity range: [{intensity_df['y'].min():.3f}, {intensity_df['y'].max():.3f}]") print(f"Mean intensity: {intensity_df['y'].mean():.3f}") ``` ```text theme={null} Intensity range: [0.501, 1.510] Mean intensity: 0.723 ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in intensity_df["unique_id"].unique().to_list(): series = intensity_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Intensity") ax.set_title("Hawkes process — intensity function") ax.legend() plt.tight_layout() plt.show() ``` ## 3. Model information and stability Inspect model parameters including the branching ratio which determines stability. ```python theme={null} info = generator.get_model_info() print(f"Baseline intensity (mu): {info['baseline_intensity']}") print(f"Excitation amplitude (alpha): {info['excitation_amplitude']}") print(f"Decay rate (beta): {info['decay_rate']}") print(f"Branching ratio (alpha/beta): {info['branching_ratio']:.3f}") print(f"Expected cluster size: {info['expected_cluster_size']:.3f}") print(f"Process is stable: {info['is_stable']}") ``` ```text theme={null} Baseline intensity (mu): 1.0 Excitation amplitude (alpha): 0.5 Decay rate (beta): 2.0 Branching ratio (alpha/beta): 0.250 Expected cluster size: 1.333 Process is stable: True ``` ## 4. Raw event simulation Directly simulate event arrival times and their associated intensities. ```python theme={null} event_times, intensities = generator.simulate_with_events(time_horizon=50.0) print(f"Simulated {len(event_times)} events over 50 time units") print(f"Event rate: {len(event_times) / 50:.2f} events/unit time") print(f"First 10 event times: {event_times[:10].round(3)}") ``` ```text theme={null} Simulated 64 events over 50 time units Event rate: 1.28 events/unit time First 10 event times: [1.714 2.527 2.584 3.022 3.662 5.29 5.583 5.682 6.25 6.494] ``` ```python theme={null} fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True) axes[0].plot(event_times, intensities, alpha=0.8) axes[0].set_ylabel("Intensity") axes[0].set_title("Hawkes process — event arrivals and intensity") axes[1].eventplot([event_times], lineoffsets=0.5, linelengths=0.8, colors="tab:red") axes[1].set_xlabel("Time") axes[1].set_ylabel("Events") axes[1].set_yticks([]) plt.tight_layout() plt.show() ``` ## 5. Power-law kernel Use a power-law decay kernel instead of the default exponential kernel for longer memory effects. ```python theme={null} power_law_gen = HawkesProcessGenerator(engine="polars", **{ "min_length": 100, "max_length": 100, "freq": "h", "kernel": "power_law", "power_law_exponent": 1.5, "baseline_intensity": 0.5, "excitation_amplitude": 0.2, "seed": 42, } ) power_law_df = power_law_gen.generate(n_series=1) print(f"Power-law kernel total events: {power_law_df['y'].sum()}") ``` ```text theme={null} Power-law kernel total events: 76.0 ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in power_law_df["unique_id"].unique().to_list(): series = power_law_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Event Count") ax.set_title("Hawkes process — power-law kernel") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Poisson process](poisson_process) — the no-excitation baseline. > * [Jump diffusion](jump_diffusion) — continuous dynamics with > discrete shocks. > > Excitation and decay parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Jump diffusion Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/jump_diffusion.html A jump-diffusion process is a smooth diffusion (like GBM) punctuated by sudden *jumps* — the model for prices that mostly drift but occasionally gap on news. It is a realistic test for methods that must distinguish ordinary volatility from discrete shocks. > **The model** > > $dS_t = \mu S_t\, dt + \sigma S_t\, dW_t + S_{t^-}\, dJ_t, \qquad N_t \sim \mathrm{Poisson}(\lambda\, dt)$ > > A continuous diffusion accumulates small changes while a Poisson jump > process adds occasional discrete moves. The jump intensity sets how > often jumps occur; the jump-size distribution sets how large they are. > Between jumps the series behaves like its underlying diffusion. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import JumpDiffusionGenerator ``` ## 1. Jump intensity `lambda_jump` is the expected number of jumps per unit time. Between jumps the path is an ordinary diffusion, so raising it adds discontinuities without changing the underlying drift. ```python theme={null} base = { "min_length": 300, "max_length": 300, "freq": "D", "mu": 0.05, "sigma": 0.15, "jump_mean": 0.0, "jump_std": 0.05, "initial_value": 100.0, "seed": 42, } fig, ax = plt.subplots(figsize=(12, 4)) for lambda_jump in (0.0, 5.0, 30.0): df = JumpDiffusionGenerator( engine="polars", lambda_jump=lambda_jump, **base ).generate(n_series=1) ax.plot( df["ds"].to_list(), df["y"].to_list(), label=f"lambda_jump={lambda_jump}", alpha=0.85 ) ax.set( xlabel="Timestamp", ylabel="Price", title="Jump intensity: lambda_jump=0 is pure diffusion", ) ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## 2. Jump direction `jump_mean` is the average log jump size, so its sign decides whether shocks push the price up or down. The drift is not compensated for jumps, which is why a negative mean drags the whole path down. ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for jump_mean in (-0.05, 0.0, 0.05): df = JumpDiffusionGenerator( engine="polars", min_length=300, max_length=300, freq="D", mu=0.05, sigma=0.15, lambda_jump=15.0, jump_mean=jump_mean, jump_std=0.02, initial_value=100.0, seed=42, ).generate(n_series=1) ax.plot( df["ds"].to_list(), df["y"].to_list(), label=f"jump_mean={jump_mean}", alpha=0.85 ) ax.set(xlabel="Timestamp", ylabel="Price", title="Jump direction follows the sign of jump_mean") ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## 3. Multiple series Generate multiple independent jump diffusion paths. ```python theme={null} multi_params = { "min_length": 150, "max_length": 150, "freq": "D", "mu": 0.05, "sigma": 0.15, "lambda_jump": 0.2, "jump_mean": 0.0, "jump_std": 0.1, "initial_value": 100.0, "seed": 42, } multi_gen = JumpDiffusionGenerator(engine="polars", **multi_params) multi_df = multi_gen.generate(n_series=3) print(f"Generated 3 series with {len(multi_df)} total observations") print(f"Overall Statistics: Mean={multi_df['y'].mean():.4f}, Std={multi_df['y'].std():.4f}") multi_df.filter(pl.col("unique_id") == "0").head(10) ``` ```text theme={null} Generated 3 series with 450 total observations Overall Statistics: Mean=6662.1054, Std=16489.8470 ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 100.0 | | "0" | 2000-01-02 00:00:00 | 84.240947 | | "0" | 2000-01-03 00:00:00 | 94.557346 | | "0" | 2000-01-04 00:00:00 | 107.575297 | | "0" | 2000-01-05 00:00:00 | 114.688591 | | "0" | 2000-01-06 00:00:00 | 159.7268 | | "0" | 2000-01-07 00:00:00 | 146.174789 | | "0" | 2000-01-08 00:00:00 | 171.596255 | | "0" | 2000-01-09 00:00:00 | 174.553944 | | "0" | 2000-01-10 00:00:00 | 168.409026 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple jump diffusion series") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Geometric Brownian motion](geometric_brownian_motion) — the > jump-free diffusion. > * [Hawkes process](hawkes_process) — when the jumps cluster and > self-excite. > > Jump intensity and size parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Levy process (heavy tails) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/levy_process.html A Levy (alpha-stable) process generalizes Brownian motion to allow *heavy tails* and jumps: increments are independent but drawn from a stable distribution whose tail weight is tunable. It models series with occasional extreme moves that a Gaussian model badly underestimates. > **The model** > > $y_t = y_{t-1} + \text{scale}\cdot X_t + \text{location}, \qquad X_t \sim S(\alpha, \beta; 1)$ > > The stability index `alpha` ∈ (0, 2] sets the tail heaviness: > `alpha = 2` recovers Gaussian increments, while smaller values give > progressively heavier tails and larger jumps (`alpha = 1` is > Cauchy-like, with undefined variance). ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import LevyProcessGenerator ``` ## 1. Tail heaviness Lower alpha = heavier tails = more extreme jumps. ```python theme={null} fig, axes = plt.subplots(3, 1, figsize=(12, 9), sharex=True) for ax, alpha, label in zip( axes, [2.0, 1.5, 1.0], ["alpha=2.0 (Gaussian)", "alpha=1.5 (heavy-tailed)", "alpha=1.0 (Cauchy-like)"], ): gen = LevyProcessGenerator(engine="polars", min_length=500, max_length=500, freq="D", alpha=alpha, cumulative=False, seed=42, ) df = gen.generate(n_series=1) ax.plot(df["ds"].to_list(), df["y"].to_list(), alpha=0.8, linewidth=0.8) ax.set_ylabel("Increment") ax.set_title(label) axes[-1].set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## 2. Multiple series ```python theme={null} multi_gen = LevyProcessGenerator(engine="polars", min_length=200, max_length=200, freq="D", alpha=1.8, cumulative=True, seed=42, ) multi_df = multi_gen.generate(n_series=3) fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Multiple Levy process series (alpha=1.8)") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Jump diffusion](jump_diffusion) — a smooth diffusion with > separate discrete jumps. > * [GARCH](garch) — heavy-tailed *conditional* behavior via > volatility clustering. > > The stability parameter is documented in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Ornstein-Uhlenbeck (mean reversion) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/ornstein_uhlenbeck.html The Ornstein-Uhlenbeck process is the canonical *mean-reverting* series: it wanders like a random walk but is continually pulled back toward a long-run level. It models interest-rate spreads, temperatures, and any quantity with an equilibrium it drifts around rather than away from. > **The model** > > $dx_t = \theta\,(\mu - x_t)\,dt + \sigma\,dW_t$ > > `theta` is the reversion speed (how hard it is pulled back), `mu` the > long-run mean, and `sigma` the volatility. Large `theta` gives a tight > band around `mu`; small `theta` approaches a random walk. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import OrnsteinUhlenbeckGenerator ``` ## 1. Reversion speed `theta` sets how hard the process is pulled back to `mu`. Everything else, including the seed, is held fixed, so the three paths differ only in reversion speed. ```python theme={null} base = { "min_length": 200, "max_length": 200, "freq": "D", "mu": 100.0, "sigma": 5.0, "initial_value": 60.0, "seed": 42, } fig, ax = plt.subplots(figsize=(12, 4)) for theta in (0.05, 0.3, 1.5): df = OrnsteinUhlenbeckGenerator(engine="polars", theta=theta, **base).generate( n_series=1 ) ax.plot(df["ds"].to_list(), df["y"].to_list(), label=f"theta={theta}", alpha=0.85) ax.axhline(base["mu"], color="black", linestyle="--", linewidth=1, label="mu") ax.set( xlabel="Timestamp", ylabel="Value", title="Reversion speed: large theta snaps back to mu, small theta wanders", ) ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## 2. Volatility `sigma` sets the size of the random shocks. With `theta` fixed, a larger `sigma` widens the band the process occupies around `mu` without changing how fast it returns. ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for sigma in (2.0, 5.0, 15.0): df = OrnsteinUhlenbeckGenerator( engine="polars", min_length=200, max_length=200, freq="D", theta=0.3, mu=100.0, sigma=sigma, initial_value=100.0, seed=42, ).generate(n_series=1) ax.plot(df["ds"].to_list(), df["y"].to_list(), label=f"sigma={sigma}", alpha=0.85) ax.axhline(100.0, color="black", linestyle="--", linewidth=1, label="mu") ax.set(xlabel="Timestamp", ylabel="Value", title="Volatility widens the band around mu") ax.legend(fontsize=8) plt.tight_layout() plt.show() ``` ## 3. Multiple series Generate multiple independent OU processes in one call. ```python theme={null} multi_params = { "min_length": 150, "max_length": 150, "freq": "D", "theta": 0.5, "mu": 100.0, "sigma": 5.0, "initial_value": 80.0, "seed": 42, } multi_gen = OrnsteinUhlenbeckGenerator(engine="polars", **multi_params) multi_df = multi_gen.generate(n_series=3) print(f"Generated 3 series with {len(multi_df)} total observations") print(f"Overall Statistics: Mean={multi_df['y'].mean():.4f}, Std={multi_df['y'].std():.4f}") multi_df.filter(pl.col("unique_id") == "0").head(10) ``` ```text theme={null} Generated 3 series with 450 total observations Overall Statistics: Mean=99.8925, Std=5.8501 ``` | unique\_id | ds | y | | ---------- | ------------------- | ---------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 80.0 | | "0" | 2000-01-02 00:00:00 | 82.992031 | | "0" | 2000-01-03 00:00:00 | 86.966288 | | "0" | 2000-01-04 00:00:00 | 98.258423 | | "0" | 2000-01-05 00:00:00 | 100.832489 | | "0" | 2000-01-06 00:00:00 | 97.918522 | | "0" | 2000-01-07 00:00:00 | 95.635111 | | "0" | 2000-01-08 00:00:00 | 100.825374 | | "0" | 2000-01-09 00:00:00 | 98.708297 | | "0" | 2000-01-10 00:00:00 | 100.196799 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.axhline(y=100.0, color="gray", linestyle="--", alpha=0.5, label="Long-term mean") ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Ornstein-Uhlenbeck: multiple mean-reverting series") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Random walk](../statistical/random_walk) — the no-reversion limit > (`theta` → 0). > * [Bounded process](bounded_process) — mean reversion constrained to > an interval. > > Reversion, mean, and volatility parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Poisson process (event arrivals) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/poisson_process.html A Poisson process models *event counts over time* — arrivals at a queue, clicks, failures — where events occur independently at a constant average rate. It is the baseline against which bursty or self-exciting arrival patterns are compared. > **The model** > > $y_t \sim \mathrm{Poisson}(\lambda), \qquad \mathbb{E}[y_t] = \mathrm{Var}(y_t) = \lambda$ > > Events arrive independently at rate `lambda` per unit time, so counts > in disjoint windows are independent and Poisson-distributed. The > result is the memoryless benchmark: no clustering, no correlation > between successive intervals. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import PoissonProcessGenerator ``` ## 1. Arrival rate `lambda_rate` is the expected number of events per time step, and it sets both the mean and the variance. Each panel shares an axis so the change in level and in spread is directly comparable. ```python theme={null} fig, axes = plt.subplots(3, 1, figsize=(12, 7), sharex=True) for ax, lambda_rate in zip(axes, (0.5, 3.0, 10.0)): df = PoissonProcessGenerator( engine="polars", min_length=100, max_length=100, freq="h", lambda_rate=lambda_rate, cumulative=False, seed=42, ).generate(n_series=1) counts = df["y"].to_list() ax.step(df["ds"].to_list(), counts, where="mid", alpha=0.85) ax.axhline(lambda_rate, color="crimson", linestyle="--", linewidth=1) ax.set( ylabel="Events", title=f"lambda_rate={lambda_rate} (observed mean {sum(counts) / len(counts):.2f})", ) axes[-1].set_xlabel("Timestamp") plt.tight_layout() plt.show() ``` ## 2. Cumulative counts The same process but with cumulative counting — useful for modeling total arrivals over time. ```python theme={null} cumulative_params = { "min_length": 50, "max_length": 50, "freq": "h", "lambda_rate": 3.0, "cumulative": True, "seed": 42, } cumulative_gen = PoissonProcessGenerator(engine="polars", **cumulative_params) cumulative_df = cumulative_gen.generate(n_series=1) print(f"Generated {len(cumulative_df)} hourly cumulative counts") print(f"Final cumulative count: {cumulative_df['y'].tail(1).item():.0f}") cumulative_df.head(10) ``` ```text theme={null} Generated 50 hourly cumulative counts Final cumulative count: 146 ``` | unique\_id | ds | y | | ---------- | ------------------- | ---- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 1.0 | | "0" | 2000-01-01 01:00:00 | 4.0 | | "0" | 2000-01-01 02:00:00 | 9.0 | | "0" | 2000-01-01 03:00:00 | 11.0 | | "0" | 2000-01-01 04:00:00 | 12.0 | | "0" | 2000-01-01 05:00:00 | 17.0 | | "0" | 2000-01-01 06:00:00 | 23.0 | | "0" | 2000-01-01 07:00:00 | 29.0 | | "0" | 2000-01-01 08:00:00 | 30.0 | | "0" | 2000-01-01 09:00:00 | 32.0 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in cumulative_df["unique_id"].unique().to_list(): series = cumulative_df.filter(pl.col("unique_id") == uid) ax.step( series["ds"].to_list(), series["y"].to_list(), where="mid", label=uid, alpha=0.8 ) ax.set_xlabel("Timestamp") ax.set_ylabel("Cumulative Count") ax.set_title("Cumulative event counts") ax.legend() plt.tight_layout() plt.show() ``` ## 3. Daily event counts Change the frequency to daily with 5 events per day on average. ```python theme={null} daily_params = { "min_length": 100, "max_length": 100, "freq": "D", "lambda_rate": 5.0, "cumulative": False, "seed": 42, } daily_gen = PoissonProcessGenerator(engine="polars", **daily_params) daily_df = daily_gen.generate(n_series=1) print(f"Generated {len(daily_df)} daily event counts") print(f"Statistics: Mean={daily_df['y'].mean():.4f}, Total Events={daily_df['y'].sum():.0f}") daily_df.head(10) ``` ```text theme={null} Generated 100 daily event counts Statistics: Mean=4.9400, Total Events=494 ``` | unique\_id | ds | y | | ---------- | ------------------- | --- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 2.0 | | "0" | 2000-01-02 00:00:00 | 5.0 | | "0" | 2000-01-03 00:00:00 | 5.0 | | "0" | 2000-01-04 00:00:00 | 6.0 | | "0" | 2000-01-05 00:00:00 | 9.0 | | "0" | 2000-01-06 00:00:00 | 6.0 | | "0" | 2000-01-07 00:00:00 | 4.0 | | "0" | 2000-01-08 00:00:00 | 6.0 | | "0" | 2000-01-09 00:00:00 | 3.0 | | "0" | 2000-01-10 00:00:00 | 2.0 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in daily_df["unique_id"].unique().to_list(): series = daily_df.filter(pl.col("unique_id") == uid) ax.step( series["ds"].to_list(), series["y"].to_list(), where="mid", label=uid, alpha=0.8 ) ax.set_xlabel("Timestamp") ax.set_ylabel("Event Count") ax.set_title("Daily event counts") ax.legend() plt.tight_layout() plt.show() ``` ## 4. Multiple processes Generate multiple independent processes in one call. ```python theme={null} multi_params = { "min_length": 50, "max_length": 50, "freq": "h", "lambda_rate": 3.0, "cumulative": False, "seed": 42, } multi_gen = PoissonProcessGenerator(engine="polars", **multi_params) multi_df = multi_gen.generate(n_series=4) print(f"Generated 4 different Poisson processes with {len(multi_df)} total observations") print(f"Overall Statistics: Mean={multi_df['y'].mean():.4f}, Total Events={multi_df['y'].sum():.0f}") multi_df.filter(pl.col("unique_id") == "0").head(10) ``` ```text theme={null} Generated 4 different Poisson processes with 200 total observations Overall Statistics: Mean=2.9900, Total Events=598 ``` | unique\_id | ds | y | | ---------- | ------------------- | --- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 1.0 | | "0" | 2000-01-01 01:00:00 | 3.0 | | "0" | 2000-01-01 02:00:00 | 5.0 | | "0" | 2000-01-01 03:00:00 | 2.0 | | "0" | 2000-01-01 04:00:00 | 1.0 | | "0" | 2000-01-01 05:00:00 | 5.0 | | "0" | 2000-01-01 06:00:00 | 6.0 | | "0" | 2000-01-01 07:00:00 | 6.0 | | "0" | 2000-01-01 08:00:00 | 1.0 | | "0" | 2000-01-01 09:00:00 | 2.0 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in multi_df["unique_id"].unique().to_list(): series = multi_df.filter(pl.col("unique_id") == uid) ax.step( series["ds"].to_list(), series["y"].to_list(), where="mid", label=uid, alpha=0.8 ) ax.set_xlabel("Timestamp") ax.set_ylabel("Event Count") ax.set_title("Multiple Poisson processes") ax.legend() plt.tight_layout() plt.show() ``` > **Related generators** > > * [Hawkes process](hawkes_process) — arrivals that cluster because > each event raises the rate. > * [INAR](../statistical/inar) — autocorrelated integer counts. > > Rate parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Regime switching (Markov switching) Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/regime_switching.html A regime-switching series alternates between distinct dynamic *regimes* — bull/bear markets, expansion/recession — with switches governed by a hidden Markov chain. Each regime has its own mean and volatility, so the series changes character abruptly and persistently. > **The model** > > $y_t = \mu_{s_t} + \phi_{s_t}\,(y_{t-1} - \mu_{s_t}) + \sigma_{s_t}\,\varepsilon_t, \qquad s_t \sim \text{Markov}(P)$ > > A latent Markov chain over `n_regimes` states controls which dynamics > generate each step; a transition matrix sets how sticky each regime > is. The result is piecewise-stationary data with structural breaks > that recur rather than happen once. ```python theme={null} import polars as pl import matplotlib.pyplot as plt from synforecast.generators import RegimeSwitchingGenerator ``` ## Bull/Bear market model Define a two-regime model where the bull regime has positive mean and low variance, while the bear regime has negative mean and high variance. ```python theme={null} params = { "min_length": 500, "max_length": 500, "freq": "D", "n_regimes": 2, "regime_means": [0.05, -0.03], "regime_variances": [1.0, 4.0], "regime_ar_coeffs": [0.1, 0.2], "transition_matrix": [ [0.98, 0.02], [0.10, 0.90], ], "seed": 42, } generator = RegimeSwitchingGenerator(engine="polars", **params) df = generator.generate(n_series=3) print(f"Generated {df['unique_id'].n_unique()} time series") print(f"Total observations: {len(df)}") df.head(10) ``` ```text theme={null} Generated 3 time series Total observations: 1500 ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | -0.855946 | | "0" | 2000-01-02 00:00:00 | 0.300061 | | "0" | 2000-01-03 00:00:00 | -0.589824 | | "0" | 2000-01-04 00:00:00 | -0.35486 | | "0" | 2000-01-05 00:00:00 | 0.190187 | | "0" | 2000-01-06 00:00:00 | -0.131834 | | "0" | 2000-01-07 00:00:00 | 1.305301 | | "0" | 2000-01-08 00:00:00 | -1.289682 | | "0" | 2000-01-09 00:00:00 | 1.136141 | | "0" | 2000-01-10 00:00:00 | 2.261838 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df["unique_id"].unique().to_list(): series = df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Value") ax.set_title("Regime-Switching time series (Bull/Bear)") ax.legend() plt.tight_layout() plt.show() ``` ## Model information Inspect model parameters including the stationary distribution of the Markov chain. ```python theme={null} info = generator.get_model_info() print(f"Number of regimes: {info['n_regimes']}") print(f"Regime means: {info['regime_means']}") print(f"Regime variances: {info['regime_variances']}") print(f"Stationary distribution: {[f'{p:.3f}' for p in info['stationary_distribution']]}") ``` ```text theme={null} Number of regimes: 2 Regime means: [0.05, -0.03] Regime variances: [1.0, 4.0] Stationary distribution: ['0.833', '0.167'] ``` ## Regime labels Generate data with regime labels to see how observations are distributed across regimes. ```python theme={null} values, regimes, ids = generator.generate_with_regimes(n_series=1) regime_counts = {0: (regimes == 0).sum(), 1: (regimes == 1).sum()} print(f"Regime 0 (Bull) observations: {regime_counts[0]}") print(f"Regime 1 (Bear) observations: {regime_counts[1]}") ``` ```text theme={null} Regime 0 (Bull) observations: 398 Regime 1 (Bear) observations: 102 ``` ```python theme={null} fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True) axes[0].plot(values, alpha=0.8) axes[0].set_ylabel("Value") axes[0].set_title("Regime-Switching — values and regime labels") axes[1].fill_between(range(len(regimes)), regimes, alpha=0.5, step="mid", color="tab:orange") axes[1].set_xlabel("Time Step") axes[1].set_ylabel("Regime") axes[1].set_yticks([0, 1]) axes[1].set_yticklabels(["Bull (0)", "Bear (1)"]) plt.tight_layout() plt.show() ``` ## Statistics by series Compare summary statistics across the generated series. ```python theme={null} stats = df.group_by("unique_id").agg( [ pl.col("y").count().alias("count"), pl.col("y").min().alias("min_value"), pl.col("y").max().alias("max_value"), pl.col("y").mean().alias("mean_value"), pl.col("y").std().alias("std_value"), ] ) stats ``` | unique\_id | count | min\_value | max\_value | mean\_value | std\_value | | ---------- | ----- | ---------- | ---------- | ----------- | ---------- | | cat | u32 | f64 | f64 | f64 | f64 | | "0" | 500 | -4.397419 | 4.687607 | 0.085832 | 1.1675 | | "1" | 500 | -6.417219 | 6.649223 | 0.054715 | 1.114054 | | "2" | 500 | -5.390482 | 5.381689 | 0.083175 | 1.140473 | > **Related generators** > > * [Changepoints](../../capabilities/changepoints) — one-off > structural breaks rather than recurring regimes. > * [GARCH](garch) — smoothly varying volatility instead of discrete > states. > > Regime and transition parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Stochastic volatility Source: https://nixtlaverse.nixtla.io/synforecast/docs/generators/stochastic/stochastic_volatility.html Stochastic-volatility models let the variance follow its *own* latent random process, rather than the deterministic recursion of GARCH. This is the Heston/SABR family used to price derivatives and to reproduce the slowly drifting turbulence of real returns. > **The model** > > $dS = \mu S\, dt + \sqrt{V}\, S\, dW_1, \qquad dV = \kappa(\theta - V)\, dt + \sigma_v \sqrt{V}\, dW_2, \qquad \mathrm{Corr}(dW_1, dW_2) = \rho$ > > The observed series is driven by a hidden volatility process that > evolves stochastically over time; the two can be correlated (a > leverage effect). Because volatility is latent and mean-reverting, > bursts arrive and fade more smoothly than under GARCH. ```python theme={null} import numpy as np import polars as pl import matplotlib.pyplot as plt from synforecast.generators import StochasticVolatilityGenerator ``` ## 1. Heston model (1 year daily) The Heston model features mean-reverting stochastic variance with a leverage effect (negative correlation between price and volatility). ```python theme={null} params = { "min_length": 252, "max_length": 252, "freq": "D", "model": "heston", "initial_price": 100.0, "initial_vol": 0.04, "drift": 0.05, "mean_vol": 0.04, "vol_mean_reversion": 2.0, "vol_of_vol": 0.3, "correlation": -0.7, "seed": 42, } generator = StochasticVolatilityGenerator(engine="polars", **params) df = generator.generate(n_series=3) print(f"Generated {df['unique_id'].n_unique()} price paths") stats = df.group_by("unique_id").agg( [ pl.col("y").first().alias("start_price"), pl.col("y").last().alias("end_price"), pl.col("y").min().alias("min_price"), pl.col("y").max().alias("max_price"), ] ) stats ``` ```text theme={null} Generated 3 price paths ``` | unique\_id | start\_price | end\_price | min\_price | max\_price | | ---------- | ------------ | ---------- | ---------- | ---------- | | cat | f64 | f64 | f64 | f64 | | "2" | 100.0 | 102.874148 | 76.299762 | 102.874148 | | "0" | 100.0 | 71.796065 | 64.666513 | 115.173901 | | "1" | 100.0 | 90.226888 | 82.753918 | 103.886735 | ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in df["unique_id"].unique().to_list(): series = df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Price") ax.set_title("Heston model — price paths") ax.legend() plt.tight_layout() plt.show() ``` ## 2. Price and volatility paths Generate both the price and volatility paths to observe their joint dynamics. ```python theme={null} prices, vols, ids = generator.generate_with_volatility(n_series=1) print(f"Price range: [{prices.min():.2f}, {prices.max():.2f}]") print(f"Volatility range: [{vols.min():.3f}, {vols.max():.3f}]") print(f"Mean volatility: {vols.mean():.3f} ({vols.mean() * np.sqrt(252) * 100:.1f}% annualized)") ``` ```text theme={null} Price range: [83.34, 109.86] Volatility range: [0.155, 0.309] Mean volatility: 0.227 (359.7% annualized) ``` ```python theme={null} fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True) axes[0].plot(prices, alpha=0.8) axes[0].set_ylabel("Price") axes[0].set_title("Heston model — price and volatility paths") axes[1].plot(vols, alpha=0.8, color="tab:orange") axes[1].set_xlabel("Time Step") axes[1].set_ylabel("Variance") plt.tight_layout() plt.show() ``` ## 3. Leverage effect (price-vol correlation) Verify that returns and volatility changes are negatively correlated (leverage effect). ```python theme={null} returns = np.diff(np.log(prices)) vol_changes = np.diff(vols) corr = np.corrcoef(returns, vol_changes)[0, 1] print(f"Return-VolChange correlation: {corr:.3f}") print("(Negative = leverage effect: vol rises when prices fall)") ``` ```text theme={null} Return-VolChange correlation: -0.666 (Negative = leverage effect: vol rises when prices fall) ``` ## 4. SABR model The SABR (Stochastic Alpha Beta Rho) model is widely used for interest rate derivatives and allows a CEV exponent (beta) to control the volatility smile shape. ```python theme={null} sabr_gen = StochasticVolatilityGenerator(engine="polars", **{ "min_length": 252, "max_length": 252, "freq": "D", "model": "sabr", "initial_price": 100.0, "beta": 0.5, "correlation": -0.3, "seed": 42, } ) sabr_df = sabr_gen.generate(n_series=1) print(f"SABR price range: [{sabr_df['y'].min():.2f}, {sabr_df['y'].max():.2f}]") ``` ```text theme={null} SABR price range: [95.99, 101.42] ``` ```python theme={null} fig, ax = plt.subplots(figsize=(12, 4)) for uid in sabr_df["unique_id"].unique().to_list(): series = sabr_df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"].to_list(), series["y"].to_list(), label=uid, alpha=0.8) ax.set_xlabel("Timestamp") ax.set_ylabel("Price") ax.set_title("SABR model — price path") ax.legend() plt.tight_layout() plt.show() ``` ## 5. Model information Inspect the full model parameters. ```python theme={null} info = generator.get_model_info() for key, value in info.items(): print(f"{key}: {value}") ``` ```text theme={null} model: heston initial_price: 100.0 initial_vol: 0.04 drift: 0.05 vol_of_vol: 0.3 correlation: -0.7 leverage_effect: True dt: 0.003968253968253968 output_type: price mean_vol: 0.04 vol_mean_reversion: 2.0 feller_condition_satisfied: True feller_value: 0.07 ``` ## 6. Implied volatility smile (SABR) Compute the implied volatility smile from the SABR model for various strike prices. ```python theme={null} strikes = np.array([80, 90, 95, 100, 105, 110, 120]) impl_vols = sabr_gen.implied_volatility_smile(strikes, maturity=1.0) print("Strike | Implied Vol") print("-" * 25) for k, iv in zip(strikes, impl_vols): print(f" {k:3d} | {iv * 100:.2f}%") ``` ```text theme={null} Strike | Implied Vol ------------------------- 80 | 3.98% 90 | 2.89% 95 | 2.39% 100 | 20.00% 105 | 1.95% 110 | 2.15% 120 | 2.72% ``` ```python theme={null} fig, ax = plt.subplots(figsize=(10, 4)) ax.plot(strikes, impl_vols * 100, marker="o", linewidth=2) ax.set_xlabel("Strike Price") ax.set_ylabel("Implied Volatility (%)") ax.set_title("SABR implied volatility smile") ax.grid(True, alpha=0.3) plt.tight_layout() plt.show() ``` ## 7. Output types The generator supports different output types: price, returns, and volatility. ```python theme={null} for output_type in ["price", "returns", "volatility"]: gen = StochasticVolatilityGenerator(engine="polars", **{ "min_length": 100, "max_length": 100, "freq": "D", "output_type": output_type, "seed": 42, } ) out_df = gen.generate(n_series=1) vals = out_df["y"].to_numpy() print(f"{output_type:12s}: mean={vals.mean():.4f}, std={vals.std():.4f}") ``` ```text theme={null} price : mean=104.0363, std=6.2124 returns : mean=-0.0010, std=0.0140 volatility : mean=0.2009, std=0.0333 ``` > **Related generators** > > * [GARCH](garch) — deterministic-recursion volatility clustering. > * [Ornstein-Uhlenbeck](ornstein_uhlenbeck) — the mean-reverting > process often used for the latent variance. > > Model parameters are in the [generator > reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). # Installation Source: https://nixtlaverse.nixtla.io/synforecast/docs/getting-started/installation.html Install SynForecast, verify the build, and add optional dataframe engines. ## From PyPI ```bash theme={null} pip install synforecast ``` Or with uv: ```bash theme={null} uv add synforecast ``` Prebuilt wheels bundle the compiled Rust extension, so no toolchain is needed on the supported platforms: | Platform | Architectures | | ----------------- | --------------------------------- | | Linux (manylinux) | x86-64, aarch64 | | macOS | Intel x86-64, Apple Silicon arm64 | | Windows | x86-64 | Wheels are published for CPython 3.10 through 3.14. ## Verify the install ```bash theme={null} python -c "import synforecast; from synforecast import _lib; print(synforecast.__version__)" ``` The `_lib` import is the meaningful part: it confirms the native extension loaded, since generation runs in Rust with no Python fallback. ## Runtime dependencies `pip install synforecast` pulls in `narwhals`, `numpy`, `pandas`, `polars`, and `pydantic`. There are no optional extras — both pandas and polars are required, because output is materialized through narwhals rather than one fixed backend. ## Optional dataframe engines `engine="pandas"` (the default) and `engine="polars"` work out of the box. Three further engines are accepted but not installed by default: ```bash theme={null} pip install pyarrow # engine="pyarrow" pip install "modin[dask]" # engine="modin" ``` `engine="cudf"` requires a RAPIDS cuDF build and a CUDA GPU; install it from the RAPIDS channel for your CUDA version. ## Building from source Installing from the source distribution compiles the Rust extension, which needs a stable Rust toolchain (install via [rustup](https://rustup.rs)): ```bash theme={null} pip install synforecast --no-binary synforecast ``` ## From a git checkout For development, clone the repository and use the `devenv` target, which runs `uv sync --dev` and installs the pre-commit hooks: ```bash theme={null} git clone https://github.com/Nixtla/synforecast.git cd synforecast make devenv ``` See [CONTRIBUTING](https://github.com/Nixtla/synforecast/blob/main/CONTRIBUTING.md) for the test and lint workflow. > **Alpha versioning** > > SynForecast is in alpha. APIs and seed-identical outputs may change > between releases, so pin a version if you depend on exact generated > values. ## Next * [Quick start](quickstart) — generate a panel and control the generators. * [Balanced pool](../capabilities/balanced_pool) — what `generate_series` draws from by default. # Quick start Source: https://nixtlaverse.nixtla.io/synforecast/docs/getting-started/quickstart.html SynForecast generates synthetic time-series panels — validated, reproducible, and in the same long format as the rest of the Nixtlaverse. This guide goes from a one-line panel to explicitly controlled generators, injected real-world patterns, and mixed datasets. > **Common uses** > > * **Testing** a forecasting pipeline on data whose true process you > know, before trusting it on real series. > * **Augmenting** a small panel so a global model has more to learn > from — see [SynAugment](../capabilities/augmentation). > * **Pretraining** foundation models on a diverse corpus no single > real dataset provides. > * **Sharing** a reproducible example without exposing proprietary > data. ## 1. Generate a panel in one line `generate_series` draws from a balanced pool of generators spanning trends, seasonality, volatility clustering, counts, and more, then returns a panel you can hand straight to any Nixtla forecaster. ```python theme={null} from synforecast import generate_series panel = generate_series( n_series=6, freq="D", min_length=200, max_length=200, engine="polars", seed=1, ) panel.head() ``` | unique\_id | ds | y | | ---------- | ------------------- | --------- | | cat | datetime\[ns] | f64 | | "0" | 2000-01-01 00:00:00 | 0.705375 | | "0" | 2000-01-02 00:00:00 | 0.838695 | | "0" | 2000-01-03 00:00:00 | 0.303755 | | "0" | 2000-01-04 00:00:00 | -0.502002 | | "0" | 2000-01-05 00:00:00 | 0.060002 | > **The Nixtla long format** > > Every SynForecast output uses three columns — `unique_id` (series id), > `ds` (timestamp), and `y` (value) — the schema `statsforecast`, > `mlforecast`, and `neuralforecast` all expect, so no adapter is > needed. The default `engine="pandas"` returns a pandas frame; pass > `engine="polars"` (as here) for Polars. ```python theme={null} import matplotlib.pyplot as plt import polars as pl from utilsforecast.plotting import plot_series def plot_panel(df, title, max_series=8): """Overlay a panel on one axis, for series that share a data-generating process.""" fig, ax = plt.subplots(figsize=(11, 4)) for uid in df["unique_id"].unique(maintain_order=True).to_list()[:max_series]: series = df.filter(pl.col("unique_id") == uid) ax.plot(series["ds"], series["y"], linewidth=1, alpha=0.8, label=str(uid)) ax.set(title=title, xlabel="ds", ylabel="y") ax.legend(fontsize=8, ncol=4) plt.tight_layout() plt.show() # These six series come from six different generators, so their scales differ by # an order of magnitude. One panel per series keeps the small-amplitude # processes readable; `plot_panel` above is for series that share a process. plot_series(panel, max_ids=6, plot_random=False) ``` Each line is a different data-generating process. Together they cover ARIMA dynamics, exponential smoothing, long memory, regime switching, volatility clustering, and irregular cycles. That diversity is what turns the panel into a stress test for a forecaster rather than one shape repeated six times. Don’t take that on trust — pass `with_generator_col=True` to record which generator produced each series: ```python theme={null} provenance = generate_series( n_series=6, freq="D", min_length=200, max_length=200, engine="polars", seed=1, with_generator_col=True, ) per_series = provenance.group_by("unique_id", "generator").len().sort("unique_id") print(per_series) assert per_series["generator"].n_unique() == 6, "panel should span 6 generators" ``` ```text theme={null} shape: (6, 3) ┌───────────┬─────────────────────────────────┬─────┐ │ unique_id ┆ generator ┆ len │ │ --- ┆ --- ┆ --- │ │ cat ┆ str ┆ u32 │ ╞═══════════╪═════════════════════════════════╪═════╡ │ 0 ┆ SARIMAGenerator ┆ 200 │ │ 1 ┆ ETSGenerator ┆ 200 │ │ 2 ┆ FractionalBrownianMotionGenera… ┆ 200 │ │ 3 ┆ RegimeSwitchingGenerator ┆ 200 │ │ 4 ┆ GARCHGenerator ┆ 200 │ │ 5 ┆ CyclicGenerator ┆ 200 │ └───────────┴─────────────────────────────────┴─────┘ ``` ## 2. Choose a generator when you need control `generate_series` is the fast default. When you need a known process — for example, to confirm your model recovers an upward trend — instantiate a generator and set its parameters explicitly. Here, a random walk with positive drift and moderate volatility. ```python theme={null} from synforecast.generators import RandomWalkGenerator rw = RandomWalkGenerator( engine="polars", min_length=200, max_length=200, freq="D", drift=0.1, volatility=1.5, start_value=100.0, seed=42, ) walks = rw.generate(n_series=4) plot_panel(walks, "Random walks with positive drift (drift=0.1, volatility=1.5)") ``` All four series share the same process but a different noise draw: the common upward pull is the `drift`, the jaggedness is the `volatility`. Change `seed` for fresh draws, or the parameters to reshape the process. Every generator’s full parameter set is listed in the [generator reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md). ## 3. Inject real-world patterns Real series are rarely clean. Any generator can add anomalies, level shifts, and missing values, so you can measure how a model copes with them — and because everything is seeded, the messy series is reproducible. ```python theme={null} messy = RandomWalkGenerator( engine="polars", min_length=200, max_length=200, freq="D", start_value=100.0, seed=42, # spikes and dips anomalies=True, anomaly_fraction=0.03, anomaly_types=["spike", "dip"], # abrupt level shifts changepoints=True, num_changepoints=2, changepoint_type="level", # gaps missing_data=True, missing_rate=0.02, ) plot_panel( messy.generate(n_series=1), "One random walk with spikes/dips, level shifts, and gaps", ) ``` > **Each pattern has its own guide** > > The knobs above are the quick version. Fine-grained control lives in > the capability guides: [anomalies](../capabilities/anomalies), > [changepoints](../capabilities/changepoints), and > [missingness](../capabilities/missingness). To attach exogenous > regressors, see [exogenous](../capabilities/exogenous). ## 4. Combine generators into one dataset A realistic panel mixes behaviors. `SynSet` composes several generators into a single long-format dataset, with each generator contributing a batch of series under its own ids. ```python theme={null} from synforecast import SynSet from synforecast.generators import SeasonalGenerator dataset = SynSet( [ RandomWalkGenerator( engine="polars", min_length=200, max_length=200, freq="D", seed=1 ), SeasonalGenerator( engine="polars", min_length=200, max_length=200, freq="D", seasonality_period=7, seasonality_amplitude=8.0, seed=2, ), ] ) mixed = dataset.generate(n_series_per_generator=3) plot_panel(mixed, "SynSet: random-walk + weekly-seasonal series in one panel") ``` > **Reproducibility and the alpha API** > > Generation is fully seeded: the same inputs produce the same panel > across runs and regardless of `n_jobs`. SynForecast is in alpha, so > seed-identical outputs may change between releases — pin a version if > you depend on exact values. ## Where to go next * **Generators** — 31 generator classes from SARIMA, ETS, and GARCH to Gaussian processes and the [KernelSynth](../generators/pretraining/kernel_synth) pretraining recipe. Start with [random walk](../generators/statistical/random_walk) or [seasonal](../generators/statistical/seasonal). * **[Augmentation](../capabilities/augmentation)** — expand a small real panel with `SynAugment` or TSMixup. * **[Balanced pool](../capabilities/balanced_pool)** — what `generate_series` draws from by default, and how to tailor it. * **Integrations** — end-to-end workflows with [statsforecast](../integrations/statsforecast), [mlforecast](../integrations/mlforecast), and [neuralforecast](../integrations/neuralforecast). # MLForecast with synthetic data Source: https://nixtlaverse.nixtla.io/synforecast/docs/integrations/mlforecast.html This guide compares an observed-data baseline, global training with observed and augmented series, and models trained only on independent synthetic series and transferred zero-shot to the observed series. MLForecast trains a single global model over lagged and derived features pooled across every series in the panel. Because that fitted estimator is shared, two things follow. Adding synthetic series changes the model the observed series is scored by — unlike a local model fitted independently per series, where extra series leave the target fit untouched. And a model trained only on synthetic series can be applied to a previously unseen panel through `predict(new_df=...)`, which is zero-shot transfer rather than a per-series refit. Both workflows below rely on that shared-weight behavior. > **Scale the target before pooling series** > > A global model sees every series through one set of coefficients, so > series on different levels have to be made comparable first. The > forecaster below applies `Differences([12])` to remove the annual > seasonal trend and `LocalStandardScaler()` to put each series on its > own scale. > > Without those transforms every synthetic workflow on this page > degrades by a factor of four to six: the estimator fits whatever level > the synthetic panel happens to occupy and predicts a nearly flat line > for the airline series. That failure is a symptom of the model > configuration, not of the synthetic data. We use the classic [Box–Jenkins airline passenger series](https://search.r-project.org/R/refmans/datasets/html/AirPassengers.html), holding out its final 12 months before generating any augmented data. ```python theme={null} from pathlib import Path import matplotlib.pyplot as plt import numpy as np import pandas as pd from mlforecast import MLForecast from mlforecast.target_transforms import Differences, LocalStandardScaler from sklearn.linear_model import Ridge from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae from synforecast import SynAugment, SynSet, generate_series from synforecast.generators import ETSGenerator, SARIMAGenerator, SeasonalGenerator ``` ```python theme={null} HORIZON = 12 TARGET_ID = "AirPassengers" data_path = Path("nbs/data/air_passengers.csv") if not data_path.exists(): data_path = Path("../../data/air_passengers.csv") observed_df = pd.read_csv(data_path, parse_dates=["ds"]) train_df = observed_df.iloc[:-HORIZON].copy() test_df = observed_df.iloc[-HORIZON:].copy() train_df.tail() ``` | | unique\_id | ds | y | | --- | ------------- | ---------- | ----- | | 127 | AirPassengers | 1959-08-31 | 559.0 | | 128 | AirPassengers | 1959-09-30 | 463.0 | | 129 | AirPassengers | 1959-10-31 | 407.0 | | 130 | AirPassengers | 1959-11-30 | 362.0 | | 131 | AirPassengers | 1959-12-31 | 405.0 | The next cell builds the four training sets. The observed baseline is the single airline series. The augmented panel adds eight counterparts of that series, drawn with `SynAugment` under a SARIMA override so the generated histories match its seasonal structure; the 12-month holdout is removed first, so no future value reaches the augmenter. The two pretraining panels are both independent of the airline data, and differ only in how they were composed: * **matched monthly pool** — SARIMA, ETS, and seasonal generators configured for monthly data with a 12-step seasonal period, an upward trend, and a comparable level. * **generic balanced pool** — `generate_series` with its defaults, which spans random walks, volatility clustering, chaos, and counts at whatever scale each process produces. Non-finite synthetic series are dropped from both so the estimator trains on clean histories. ```python theme={null} augmented_train_df = SynAugment(seed=42).augment( train_df, n_augment=8, generator_override={TARGET_ID: "SARIMAGenerator"}, ) n_obs = len(train_df) monthly_generators = [ SARIMAGenerator( min_length=n_obs, max_length=n_obs, freq="ME", engine="polars", seasonal_period=12, d=1, D=1, noise_std=2.0, seed=seed, ) for seed in range(4) ] + [ ETSGenerator( min_length=n_obs, max_length=n_obs, freq="ME", engine="polars", seasonal_period=12, trend_type="add", seasonal_type="add", level=300.0, trend=1.5, seed=10 + seed, ) for seed in range(4) ] + [ SeasonalGenerator( min_length=n_obs, max_length=n_obs, freq="ME", engine="polars", seasonality_period=12, base_level=300.0, trend=1.5, seasonality_amplitude=50.0, noise_level=10.0, seed=20 + seed, ) for seed in range(4) ] def drop_non_finite(df: pd.DataFrame) -> pd.DataFrame: """Keep only series whose values are all finite.""" finite = df.groupby("unique_id", observed=True)["y"].apply( lambda values: np.isfinite(values).all() ) return df.loc[df["unique_id"].isin(finite[finite].index)].copy() # The generators emit Polars; the rest of this guide stays on pandas. matched_pretrain_df = drop_non_finite( SynSet(monthly_generators).generate(n_series_per_generator=3).to_pandas() ) generic_pretrain_df = drop_non_finite( generate_series( n_series=32, freq="ME", min_length=n_obs, max_length=n_obs, seed=42 ) ) pd.DataFrame( { "training set": [ "observed only", "observed + augmented", "matched monthly pool", "generic balanced pool", ], "series": [ 1, augmented_train_df["unique_id"].nunique(), matched_pretrain_df["unique_id"].nunique(), generic_pretrain_df["unique_id"].nunique(), ], } ) ``` | | training set | series | | - | --------------------- | ------ | | 0 | observed only | 1 | | 1 | observed + augmented | 9 | | 2 | matched monthly pool | 36 | | 3 | generic balanced pool | 32 | ```python theme={null} def make_forecaster() -> MLForecast: return MLForecast( models=Ridge(alpha=1.0), freq="ME", lags=[1, 2, 3, 6, 12], target_transforms=[Differences([12]), LocalStandardScaler()], ) def target_forecast(forecast_df: pd.DataFrame) -> pd.DataFrame: return forecast_df.loc[ forecast_df["unique_id"].astype(str) == TARGET_ID, ["unique_id", "ds", "Ridge"], ].copy() ``` ```python theme={null} real_only = make_forecaster() real_only.fit(train_df) real_only_forecast = target_forecast(real_only.predict(h=HORIZON)) ``` ```python theme={null} with_augmentation = make_forecaster() with_augmentation.fit(augmented_train_df) augmented_forecast = target_forecast(with_augmentation.predict(h=HORIZON)) ``` MLForecast can apply a fitted global estimator to unseen series through `predict(new_df=...)`. The estimators below are fitted only on independent SynForecast series; the observed history is supplied only when producing its recursive lag features and forecasts. This is zero-shot transfer, not fine-tuning. Fitting both pools separately is what makes the comparison useful: the pretraining corpus is a modeling choice, and choosing it badly costs more accuracy here than skipping pretraining altogether. ```python theme={null} matched_pretrained = make_forecaster() matched_pretrained.fit(matched_pretrain_df) matched_zero_shot_forecast = target_forecast( matched_pretrained.predict(h=HORIZON, new_df=train_df) ) generic_pretrained = make_forecaster() generic_pretrained.fit(generic_pretrain_df) generic_zero_shot_forecast = target_forecast( generic_pretrained.predict(h=HORIZON, new_df=train_df) ) ``` We score each workflow by mean absolute error over the 12-month holdout for the airline series. ```python theme={null} forecast_sets = { "Observed only": real_only_forecast, "Observed + augmented": augmented_forecast, "Zero-shot, matched pool": matched_zero_shot_forecast, "Zero-shot, generic pool": generic_zero_shot_forecast, } comparison = test_df[["unique_id", "ds", "y"]].copy() for label, forecast_df in forecast_sets.items(): comparison = comparison.merge( forecast_df.rename(columns={"Ridge": label}), on=["unique_id", "ds"], how="left", ) scores = evaluate(comparison, metrics=[mae], models=list(forecast_sets)) metrics = ( scores.melt(id_vars=["unique_id", "metric"], var_name="workflow", value_name="MAE") .loc[:, ["workflow", "MAE"]] .sort_values("MAE", ignore_index=True) ) metrics ``` | | workflow | MAE | | - | ----------------------- | --------- | | 0 | Zero-shot, matched pool | 12.507531 | | 1 | Observed + augmented | 14.613678 | | 2 | Observed only | 16.302282 | | 3 | Zero-shot, generic pool | 22.675395 | ```python theme={null} fig, ax = plt.subplots(figsize=(11, 5)) history = train_df.tail(48) ax.plot(history["ds"], history["y"], color="black", label="Training history") ax.plot(test_df["ds"], test_df["y"], color="black", linestyle="--", label="Holdout") for label in forecast_sets: ax.plot(comparison["ds"], comparison[label], marker="o", markersize=4, label=label) ax.set(title="Holdout forecasts by training set", ylabel="Passengers (thousands)") ax.legend(fontsize=8) fig.tight_layout() ``` Two things stand out, and both match the paired benchmarks in [When does synthetic data help?](../capabilities/when_synthetic_helps). **Augmentation is roughly neutral.** Across ten augmentation seeds the augmented panel beat the observed-only baseline six times out of ten (median MAE 15.8 against 16.3). Treat the single row in the table as a workflow demonstration, not as evidence that augmentation improves accuracy. **A matched pretraining pool wins here; a generic one loses.** The matched-pool zero-shot model beat observed-only training in all ten pool seeds (median MAE 12.2), while the generic pool lost in all ten (median 22.2). One 132-point series is very little data for a global model, which is the data-scarce regime where synthetic pretraining pays off — but only when the corpus resembles the target domain in frequency, seasonal period, and scale. For a production decision, evaluate multiple temporal folds, generator configurations, estimators, and random seeds. Choose augmentation ratios and pool composition on validation data, never on the final holdout. # NeuralForecast with synthetic data Source: https://nixtlaverse.nixtla.io/synforecast/docs/integrations/neuralforecast.html This guide compares four leakage-safe workflows on the same M4 Monthly holdout: training on observed data, training on observed plus augmented data, zero-shot forecasting after pretraining only on independent synthetic series, and synthetic pretraining followed by fine-tuning on observed data. We use a fixed, stratified sample of 24 series from the [M4 competition dataset](https://github.com/Mcompetitions/M4-methods), with four series from each domain category. Every selected training series has between 96 and 180 monthly observations. Evaluation uses the official 18-month M4 test horizon. The sample was drawn once with seed 42 from eligible M4 Monthly series. The data is downloaded from the official repository when this notebook is executed and is not redistributed with SynForecast. See Makridakis, Spiliotis, and Assimakopoulos, [*The M4 Competition: 100,000 time series and 61 forecasting methods*](https://doi.org/10.1016/j.ijforecast.2019.04.014). ```python theme={null} import csv import tempfile from functools import partial from pathlib import Path from urllib.request import urlretrieve import matplotlib.pyplot as plt import numpy as np import pandas as pd import torch from neuralforecast import NeuralForecast from neuralforecast.models import NHITS from utilsforecast.evaluation import evaluate from utilsforecast.losses import mase, smape from synforecast import SynAugment, generate_series from synforecast.generators import ETSGenerator, SARIMAGenerator, SeasonalGenerator ``` ```python theme={null} HORIZON = 18 SEASON_LENGTH = 12 TARGET_IDS = [ "M21522", "M21345", "M23456", "M22325", # Demographic "M41442", "M40386", "M39368", "M39192", # Finance "M35069", "M27603", "M27586", "M29021", # Industry "M4433", "M4489", "M7650", "M8464", # Macro "M10215", "M14927", "M11649", "M15420", # Micro "M47985", "M47988", "M47930", "M47841", # Other ] M4_BASE_URL = "https://raw.githubusercontent.com/Mcompetitions/M4-methods/master/Dataset" CACHE_DIR = Path(tempfile.gettempdir()) / "synforecast-m4" CACHE_DIR.mkdir(parents=True, exist_ok=True) def download_m4_split(split: str) -> Path: path = CACHE_DIR / f"Monthly-{split.lower()}.csv" if not path.exists(): urlretrieve(f"{M4_BASE_URL}/{split}/Monthly-{split.lower()}.csv", path) return path def load_selected_series( path: Path, start_at: dict[str, int] | None = None ) -> pd.DataFrame: records = [] with path.open(newline="") as file: reader = csv.reader(file) next(reader) for row in reader: unique_id = row[0] if unique_id not in TARGET_IDS: continue values = [float(value) for value in row[1:] if value] start = 1 if start_at is None else start_at[unique_id] records.extend( (unique_id, start + offset, value) for offset, value in enumerate(values) ) return pd.DataFrame(records, columns=["unique_id", "ds", "y"]) train_df = load_selected_series(download_m4_split("Train")) test_starts = ( train_df.groupby("unique_id", observed=True)["ds"].max().add(1).to_dict() ) test_df = load_selected_series(download_m4_split("Test"), test_starts) pd.DataFrame( { "series": [train_df["unique_id"].nunique()], "minimum training length": [train_df.groupby("unique_id").size().min()], "maximum training length": [train_df.groupby("unique_id").size().max()], "holdout length": [test_df.groupby("unique_id").size().min()], } ) ``` | | series | minimum training length | maximum training length | holdout length | | - | ------ | ----------------------- | ----------------------- | -------------- | | 0 | 24 | 102 | 180 | 18 | For the augmentation workflow, `SynAugment` fits each training series independently and creates candidate counterparts. We retain one counterpart from each of the six M4 domain categories, so synthetic series make up only 20% of the combined panel instead of half of it. This category-balanced ratio is fixed before evaluation, and the official holdout is never passed to the augmenter. For synthetic pretraining, we deliberately avoid the generic balanced pool. The custom pool below contains monthly SARIMA, ETS, and seasonal processes with 12-step seasonality and training lengths comparable to the selected M4 histories. These synthetic series are generated independently of both the M4 training observations and the holdout. ```python theme={null} augmented_train_df = SynAugment(seed=42).augment(train_df, n_augment=1) selected_source_ids = TARGET_IDS[::4] # One from each M4 domain category selected_augmentation_ids = { f"{unique_id}_aug_0" for unique_id in selected_source_ids } selected_augmentations = augmented_train_df.loc[ augmented_train_df["unique_id"].astype(str).isin(selected_augmentation_ids) ].copy() augmented_train_df = pd.concat( [train_df, selected_augmentations], ignore_index=True ) monthly_generators = [ SARIMAGenerator( min_length=96, max_length=180, freq=1, engine="polars", seasonal_period=12, d=1, D=1, noise_std=2.0, seed=1, ), SARIMAGenerator( min_length=96, max_length=180, freq=1, engine="polars", seasonal_period=12, p=2, q=0, P=0, Q=1, noise_std=1.5, seed=2, ), ETSGenerator( min_length=96, max_length=180, freq=1, engine="polars", seasonal_period=12, level=100.0, trend=0.2, noise_std=2.0, seed=3, ), ETSGenerator( min_length=96, max_length=180, freq=1, engine="polars", seasonal_period=12, level=50.0, trend=-0.05, noise_std=1.0, damped=True, seed=4, ), SeasonalGenerator( min_length=96, max_length=180, freq=1, engine="polars", seasonality_period=12, seasonality_amplitude=10.0, trend=0.15, noise_level=2.0, seed=5, ), SeasonalGenerator( min_length=96, max_length=180, freq=1, engine="polars", seasonality_period=12, seasonality_amplitude=5.0, trend=-0.05, noise_level=1.0, seed=6, ), ] synthetic_pretrain_df = pd.DataFrame( generate_series(n_series=96, generators=monthly_generators).to_dicts() ) valid_ids = synthetic_pretrain_df.groupby("unique_id", observed=True)["y"].apply( lambda values: np.isfinite(values).all() ) synthetic_pretrain_df = synthetic_pretrain_df.loc[ synthetic_pretrain_df["unique_id"].isin(valid_ids[valid_ids].index) ].copy() pd.DataFrame( { "training set": [ "observed only", "observed + augmented", "independent synthetic only", ], "series": [ train_df["unique_id"].nunique(), augmented_train_df["unique_id"].nunique(), synthetic_pretrain_df["unique_id"].nunique(), ], "observations": [ len(train_df), len(augmented_train_df), len(synthetic_pretrain_df), ], } ) ``` | | training set | series | observations | | - | -------------------------- | ------ | ------------ | | 0 | observed only | 24 | 3533 | | 1 | observed + augmented | 30 | 4498 | | 2 | independent synthetic only | 96 | 13512 | ```python theme={null} def make_forecaster() -> NeuralForecast: torch.set_float32_matmul_precision("medium") model = NHITS( h=HORIZON, input_size=36, scaler_type="standard", val_check_steps=25, random_seed=42, max_steps=100, logger=False, enable_checkpointing=False, enable_model_summary=False, enable_progress_bar=False, ) return NeuralForecast(models=[model], freq=1) def target_forecast(forecast_df: pd.DataFrame) -> pd.DataFrame: return forecast_df.loc[ forecast_df["unique_id"].astype(str).isin(TARGET_IDS), ["unique_id", "ds", "NHITS"], ].copy() ``` ```python theme={null} real_only = make_forecaster() real_only.fit(df=train_df) real_only_forecast = target_forecast(real_only.predict()) ``` ```python theme={null} with_augmentation = make_forecaster() with_augmentation.fit(df=augmented_train_df) augmented_forecast = target_forecast(with_augmentation.predict()) ``` For genuine pretraining, the next model sees only independently generated monthly series. `predict(df=train_df)` then applies those learned weights to the previously unseen M4 panel. Calling `fit` again with `use_init_models=False` retains the pretrained weights and fine-tunes them on the observed training split. ```python theme={null} pretrained = make_forecaster() pretrained.fit(df=synthetic_pretrain_df) zero_shot_forecast = target_forecast(pretrained.predict(df=train_df)) pretrained.fit(df=train_df, use_init_models=False) finetuned_forecast = target_forecast(pretrained.predict()) ``` We report the mean sMAPE and seasonal MASE across series, giving each M4 series equal weight. MASE uses each series’ in-sample 12-month seasonal-naive error as its scale. ```python theme={null} forecast_sets = { "Observed only": real_only_forecast, "Observed + synthetic": augmented_forecast, "Synthetic pretraining (zero-shot)": zero_shot_forecast, "Synthetic pretraining + fine-tuning": finetuned_forecast, } comparison = test_df[["unique_id", "ds", "y"]].copy() for label, forecast_df in forecast_sets.items(): comparison = comparison.merge( forecast_df.rename(columns={"NHITS": label}), on=["unique_id", "ds"], how="left", validate="one_to_one", ) metrics = evaluate( df=comparison, metrics=[smape, partial(mase, seasonality=SEASON_LENGTH)], models=list(forecast_sets), train_df=train_df, ) metrics = ( metrics.groupby("metric", observed=True)[list(forecast_sets)] .mean() .T.rename_axis("workflow") .reset_index() .rename(columns={"smape": "sMAPE", "mase": "MASE"}) .sort_values("MASE") ) metrics = metrics[["workflow", "sMAPE", "MASE"]] metrics ``` | metric | workflow | sMAPE | MASE | | ------ | ----------------------------------- | -------- | -------- | | 0 | Observed only | 0.064493 | 0.906638 | | 3 | Synthetic pretraining + fine-tuning | 0.063916 | 0.907407 | | 1 | Observed + synthetic | 0.064189 | 0.915034 | | 2 | Synthetic pretraining (zero-shot) | 0.092384 | 1.347643 | ```python theme={null} plot_id = TARGET_IDS[0] history = train_df.loc[train_df["unique_id"] == plot_id].tail(36) holdout = comparison.loc[comparison["unique_id"] == plot_id] fig, ax = plt.subplots(figsize=(11, 5)) ax.plot(history["ds"], history["y"], color="black", label="Training history") ax.plot( holdout["ds"], holdout["y"], color="black", linestyle="--", label="Official M4 holdout", ) for label in forecast_sets: ax.plot(holdout["ds"], holdout[label], marker="o", label=label) ax.set(title=f"NeuralForecast workflows on {plot_id}", xlabel="Month", ylabel="Value") ax.legend(fontsize=8, ncol=2) fig.tight_layout() ``` The conservative, category-balanced augmentation adds diversity without allowing generated histories to dominate the observed panel. Synthetic-only zero-shot forecasting remains the hardest setting, while fine-tuning adapts the pretrained model to the target domain. These scores illustrate the workflow, not an M4 benchmark: the sample and neural model are intentionally small, each fit uses only 100 optimization steps, and results can vary with the selected panel. For a model comparison, evaluate all M4 Monthly series over multiple seeds and report uncertainty. Choose augmentation ratios using validation data, and never fit `SynAugment` on the official holdout. [When does synthetic data help?](../capabilities/when_synthetic_helps) reports the multi-seed augmentation and pretraining results, including the history lengths where the pretraining edge reverses. # Works with the Nixtlaverse Source: https://nixtlaverse.nixtla.io/synforecast/docs/integrations/nixtlaverse.html Compare observed, augmented, and synthetic-only training workflows. SynForecast produces the standard Nixtla long format: `unique_id` identifies a series, `ds` identifies time, and `y` contains the target. Materialized SynForecast data can therefore be passed directly to NeuralForecast, MLForecast, or StatsForecast. The executable guides compare three training regimes against the same temporal holdout: * **Observed only:** the baseline that synthetic workflows must beat. * **Observed + synthetic:** `SynAugment` is fitted to the training split and adds statistically matched histories. * **Synthetic only:** the forecasting model is fitted without observed target values. The exact transfer mechanism depends on the model family. ## Integration guides * [NeuralForecast](neuralforecast.ipynb): zero-shot synthetic pretraining and synthetic pretraining followed by fine-tuning. * [MLForecast](mlforecast.ipynb): global training with augmentation and zero-shot transfer through `predict(new_df=...)`. * [StatsForecast](statsforecast.ipynb): local-model behavior and a synthetic-history ensemble instead of transferable pretraining. All three use the classic Box–Jenkins airline passenger series, reserve its last 12 months before generation, report holdout MAE, and save their forecast plots. They are workflow demonstrations rather than evidence that synthetic data always improves accuracy. ## Materialize synthetic data ```python theme={null} from synforecast import SynAugment, generate_series augmented_train_df = SynAugment(seed=42).augment( train_df, n_augment=2, ) synthetic_pretrain_df = generate_series( n_series=1_000, freq="D", min_length=100, max_length=100, seed=42, ) ``` Fit augmentation parameters on training observations only. Using validation or test values would leak future information into the generated training set. Synthetic data also inherits its generators’ assumptions, so retain an observed-only baseline and evaluate multiple temporal folds and random seeds. These examples materialize data before model fitting. On-the-fly training-data generation would require a streaming data-source interface in the forecasting library and is not part of the current integration. # StatsForecast with synthetic data Source: https://nixtlaverse.nixtla.io/synforecast/docs/integrations/statsforecast.html StatsForecast fits each series independently, so synthetic data plays a different role here than it does for global models. This guide establishes three things in order: augmenting a panel cannot change a local fit, there is no local-model equivalent of pretraining, and a generated series with a known process makes a good pipeline check. `AutoETS` and its siblings share no parameters across series. Everything below follows from that. We use the classic [Box–Jenkins airline passenger series](https://search.r-project.org/R/refmans/datasets/html/AirPassengers.html). Its final 12 months are held out before `SynAugment` analyzes the training history. ```python theme={null} from pathlib import Path import matplotlib.pyplot as plt import numpy as np import pandas as pd from statsforecast import StatsForecast from statsforecast.models import AutoETS, SeasonalNaive from utilsforecast.evaluation import evaluate from utilsforecast.losses import mae from synforecast import SynAugment from synforecast.generators import ETSGenerator ``` ```python theme={null} HORIZON = 12 TARGET_ID = "AirPassengers" data_path = Path("nbs/data/air_passengers.csv") if not data_path.exists(): data_path = Path("../../data/air_passengers.csv") observed_df = pd.read_csv(data_path, parse_dates=["ds"]) train_df = observed_df.iloc[:-HORIZON].copy() test_df = observed_df.iloc[-HORIZON:].copy() train_df.tail() ``` | | unique\_id | ds | y | | --- | ------------- | ---------- | ----- | | 127 | AirPassengers | 1959-08-31 | 559.0 | | 128 | AirPassengers | 1959-09-30 | 463.0 | | 129 | AirPassengers | 1959-10-31 | 407.0 | | 130 | AirPassengers | 1959-11-30 | 362.0 | | 131 | AirPassengers | 1959-12-31 | 405.0 | ## 1. Augmentation cannot change a local fit Adding synthetic series gives `AutoETS` more series to fit, but no pooled parameter for them to influence, so the model fitted to the airline series is untouched. Sixteen `SynAugment` counterparts are added under a SARIMA override; the 12-month holdout is removed first, so no future value reaches the augmenter. ```python theme={null} augmented_train_df = SynAugment(seed=42).augment( train_df, n_augment=16, generator_override={TARGET_ID: "SARIMAGenerator"}, ) pd.DataFrame( { "training set": ["observed only", "observed + augmented"], "series": [1, augmented_train_df["unique_id"].nunique()], } ) ``` | | training set | series | | - | -------------------- | ------ | | 0 | observed only | 1 | | 1 | observed + augmented | 17 | ```python theme={null} def make_forecaster() -> StatsForecast: return StatsForecast( models=[AutoETS(season_length=12)], freq="ME", n_jobs=1, ) def target_forecast(forecast_df: pd.DataFrame) -> pd.DataFrame: return forecast_df.loc[ forecast_df["unique_id"].astype(str) == TARGET_ID, ["unique_id", "ds", "AutoETS"], ].copy() ``` ```python theme={null} real_only = make_forecaster() real_only.fit(train_df) real_only_forecast = target_forecast(real_only.predict(h=HORIZON)) ``` ```python theme={null} with_augmentation = make_forecaster() with_augmentation.fit(augmented_train_df) augmented_forecast = target_forecast(with_augmentation.predict(h=HORIZON)) identical = np.allclose( real_only_forecast["AutoETS"], augmented_forecast["AutoETS"] ) print(f"the two forecasts are identical: {identical}") ``` ```text theme={null} the two forecasts are identical: True ``` ```python theme={null} forecast_sets = { "Observed only": real_only_forecast, "Observed + synthetic panel": augmented_forecast, } comparison = test_df[["unique_id", "ds", "y"]].copy() for label, forecast_df in forecast_sets.items(): comparison = comparison.merge( forecast_df.rename(columns={"AutoETS": label}), on=["unique_id", "ds"], how="left", ) scores = evaluate(comparison, metrics=[mae], models=list(forecast_sets)) metrics = scores.melt( id_vars=["unique_id", "metric"], var_name="workflow", value_name="MAE" ).loc[:, ["workflow", "MAE"]] metrics ``` | | workflow | MAE | | - | -------------------------- | --------- | | 0 | Observed only | 35.612471 | | 1 | Observed + synthetic panel | 35.612471 | Both workflows score the same because they *are* the same fit. In the chart the augmented forecast is drawn as a thick translucent band with the observed-only line on top of it — one visible line means the two coincide exactly. ```python theme={null} fig, ax = plt.subplots(figsize=(11, 5)) history = train_df.tail(48) ax.plot(history["ds"], history["y"], color="black", linewidth=1, label="Training history") ax.plot(test_df["ds"], test_df["y"], color="black", linestyle="--", label="Holdout") # The two forecasts are identical, so the augmented one is drawn wide and # translucent with the observed-only line on top of it. ax.plot(comparison["ds"], comparison["Observed + synthetic panel"], color="crimson", linewidth=7, alpha=0.3, solid_capstyle="round", label="Observed + synthetic panel") ax.plot(comparison["ds"], comparison["Observed only"], color="steelblue", linewidth=1.5, marker="o", markersize=4, label="Observed only (identical)") ax.set(title="Augmenting the panel leaves the local fit unchanged", ylabel="Passengers (thousands)") ax.legend(fontsize=8) fig.tight_layout() ``` ## 2. There is no local-model pretraining Because nothing is shared, fitting `AutoETS` to synthetic histories produces forecasts *for those histories*, not for the airline series. Averaging them is not a shortcut either, and the table below shows why: `SynAugment` matches each draw to the **mean of the training window**, which leaves the final level free. On a strongly trending series the two are far apart, so the draws end scattered around the observed endpoint. ```python theme={null} synthetic_history_df = augmented_train_df.loc[ augmented_train_df["unique_id"].astype(str) != TARGET_ID ].copy() levels = synthetic_history_df.groupby("unique_id", observed=True)["y"].agg( window_mean="mean", final_year_mean=lambda values: values.tail(12).mean(), ) pd.DataFrame( { "series": ["observed", "synthetic (16 draws)"], "window mean": [ round(train_df["y"].mean(), 1), f"{levels['window_mean'].min():.0f} - {levels['window_mean'].max():.0f}", ], "final-year mean": [ round(train_df["y"].tail(12).mean(), 1), f"{levels['final_year_mean'].min():.0f} - {levels['final_year_mean'].max():.0f}", ], } ) ``` | | series | window mean | final-year mean | | - | -------------------- | ----------- | --------------- | | 0 | observed | 262.5 | 428.3 | | 1 | synthetic (16 draws) | 262 - 263 | 191 - 506 | ## 3. Validate the pipeline on a known process The two sections above used the airline series, whose true process nobody knows. A generated series is different: you chose the process, the seasonal period, and the noise scale, so you know in advance what a correctly configured pipeline should be able to do. That makes it a check with a known answer. The twelve series below come from an additive ETS process with a 12-period season, and the last 12 points of each are held out. `AutoETS` should beat `SeasonalNaive` by a wide margin, because the data really does contain the smooth trend-plus-season structure `AutoETS` fits and `SeasonalNaive` ignores. If it does not, the pipeline is misconfigured — and you have learned that without touching real data. ```python theme={null} NOISE_STD = 6.0 known_process = ETSGenerator( min_length=132, max_length=132, freq="ME", engine="polars", seasonal_period=12, error_type="add", trend_type="add", seasonal_type="add", level=300.0, trend=1.5, noise_std=NOISE_STD, seed=7, ) known_panel = known_process.generate(n_series=12).to_pandas() is_holdout = ( known_panel.groupby("unique_id", observed=True).cumcount() >= 132 - HORIZON ) known_train_df = known_panel.loc[~is_holdout] known_test_df = known_panel.loc[is_holdout] known_models = StatsForecast( models=[AutoETS(season_length=12), SeasonalNaive(season_length=12)], freq="ME", n_jobs=1, ) known_models.fit(known_train_df) known_forecast = known_models.predict(h=HORIZON).merge( known_test_df[["unique_id", "ds", "y"]], on=["unique_id", "ds"] ) known_scores = evaluate( known_forecast, metrics=[mae], models=["AutoETS", "SeasonalNaive"], agg_fn="mean", ) known_scores.melt(id_vars="metric", var_name="model", value_name="MAE").loc[ :, ["model", "MAE"] ].round(2) ``` | | model | MAE | | - | ------------- | ----- | | 0 | AutoETS | 11.52 | | 1 | SeasonalNaive | 49.54 | `AutoETS` comes in around four times more accurate than `SeasonalNaive`, which is the outcome the generated structure predicts. That is the whole check: a known-answer test that fails loudly when the frequency, the season length, or the model family is set up wrongly. For a sense of how much of the remaining error is irreducible, the noise is Gaussian with standard deviation `NOISE_STD`, so the best any forecaster could do one step ahead is a mean absolute error of `NOISE_STD * sqrt(2 / pi)`. Forecasting twelve steps ahead must do worse than that, since an additive-error ETS accumulates innovations into its state as the horizon grows — so treat the number below as a lower bound to sit above, not a target to reach. ```python theme={null} best_one_step_mae = NOISE_STD * np.sqrt(2 / np.pi) print(f"generator noise standard deviation: {NOISE_STD:.2f}") print(f"best possible one-step MAE: {best_one_step_mae:.2f}") ``` ```text theme={null} generator noise standard deviation: 6.00 best possible one-step MAE: 4.79 ``` ```python theme={null} example_id = known_panel["unique_id"].iloc[0] example_history = known_train_df.loc[known_train_df["unique_id"] == example_id].tail(36) example_holdout = known_test_df.loc[known_test_df["unique_id"] == example_id] example_forecast = known_forecast.loc[known_forecast["unique_id"] == example_id] fig, ax = plt.subplots(figsize=(11, 4.5)) ax.plot(example_history["ds"], example_history["y"], color="black", linewidth=1, label="Known-process history") ax.plot(example_holdout["ds"], example_holdout["y"], color="black", linestyle="--", label="Known holdout") ax.plot(example_forecast["ds"], example_forecast["AutoETS"], color="steelblue", marker="o", markersize=4, label="AutoETS") ax.plot(example_forecast["ds"], example_forecast["SeasonalNaive"], color="goldenrod", marker="o", markersize=4, label="SeasonalNaive") ax.set( title="AutoETS follows the generated trend and season; SeasonalNaive repeats last year", ylabel="Value", ) ax.legend(fontsize=8) fig.tight_layout() ``` For local models, synthetic data is a test instrument rather than extra training signal. Augmentation is inert, aggregating synthetic fits is not a substitute for pretraining, and generating from a process you configured yourself gives you a check whose answer you already know. Panel augmentation does help model families that pool parameters across series — see the [MLForecast](mlforecast) and [NeuralForecast](neuralforecast) guides. Synthetic panels are also useful here for stress testing: inject [anomalies](../capabilities/anomalies) or [changepoints](../capabilities/changepoints) with known positions and measure how far the fitted model moves. # Exogenous Variables Source: https://nixtlaverse.nixtla.io/synforecast/exogenous.html Exogenous variable configuration and generation ### `ExogenousConfig` Bases: [BaseModel](#pydantic.BaseModel) Configuration for exogenous variable generation. Controls which exogenous columns are added to the output DataFrame. All options are off by default for backward compatibility. **Parameters:** | Name | Type | Description | Default | | ------------------- | ---- | -------------------------------------------------------- | ---------- | | `datetime_features` | | Add calendar features (year, month, day\_of\_week, etc.) | *required* | | `datetime_cyclical` | | Add sin/cos cyclical encodings of datetime features | *required* | | `anomaly_flags` | | Add binary column indicating anomaly positions | *required* | | `changepoint_flags` | | Add binary column indicating changepoint positions | *required* | | `missing_flags` | | Add binary column indicating missing data positions | *required* | | `correlated` | | List of correlated exogenous variables to generate | *required* | #### `ExogenousConfig.anomaly_flags` ```python theme={null} anomaly_flags: bool = Field(default=False, description='Add anomaly indicator column') ``` #### `ExogenousConfig.changepoint_flags` ```python theme={null} changepoint_flags: bool = Field(default=False, description='Add changepoint indicator column') ``` #### `ExogenousConfig.correlated` ```python theme={null} correlated: list[CorrelatedExogConfig] = Field(default_factory=list, description='Correlated exogenous variables to generate') ``` #### `ExogenousConfig.datetime_cyclical` ```python theme={null} datetime_cyclical: bool = Field(default=False, description='Add sin/cos cyclical encodings') ``` #### `ExogenousConfig.datetime_features` ```python theme={null} datetime_features: bool = Field(default=False, description='Add calendar features') ``` #### `ExogenousConfig.missing_flags` ```python theme={null} missing_flags: bool = Field(default=False, description='Add missing data indicator column') ``` #### `ExogenousConfig.model_config` ```python theme={null} model_config = ConfigDict(extra='forbid') ``` #### `ExogenousConfig.validate_unique_names` ```python theme={null} validate_unique_names() ``` Reject duplicate output columns before dataframe construction. ### `CorrelatedExogConfig` Bases: [BaseModel](#pydantic.BaseModel) Configuration for a single correlated exogenous variable. #### `CorrelatedExogConfig.correlation` ```python theme={null} correlation: float = Field(default=0.7, ge=(-1), le=1, description='Target correlation with the series') ``` #### `CorrelatedExogConfig.lag` ```python theme={null} lag: int = Field(default=1, ge=1, description='Lag for lagged_copy method') ``` #### `CorrelatedExogConfig.method` ```python theme={null} method: Literal['correlated_noise', 'lagged_copy', 'trend_following'] = Field(default='correlated_noise', description='Method for generating correlated exogenous') ``` #### `CorrelatedExogConfig.model_config` ```python theme={null} model_config = ConfigDict(extra='forbid') ``` #### `CorrelatedExogConfig.name` ```python theme={null} name: str = Field(..., description='Column name for this exogenous variable') ``` #### `CorrelatedExogConfig.noise_std` ```python theme={null} noise_std: float = Field(default=0.1, ge=0, description='Noise std for lagged_copy method') ``` #### `CorrelatedExogConfig.smoothing_window` ```python theme={null} smoothing_window: int = Field(default=10, ge=1, description='Window size for trend_following method') ``` #### `CorrelatedExogConfig.trend_noise_std` ```python theme={null} trend_noise_std: float = Field(default=0.1, ge=0, description='Noise std for trend_following method') ``` # Domain-Specific Generators Source: https://nixtlaverse.nixtla.io/synforecast/generators_domain.html IntermittentDemand, IoTSensor, EnergyLoad, StateSpace, DailyActiveUsers, VitalSigns, and Clickstream generators ### `IntermittentDemandGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate intermittent demand time series with sparse patterns. Demand is a two-part process: a binary occurrence process decides at which periods demand happens, and a size distribution draws the demand quantity for those periods (all other periods are zero). Common in retail, spare parts, and inventory contexts (Croston-style demand).
Occurrence patterns * 'random': i.i.d. Bernoulli(demand\_probability) per period, so the long-run fraction of non-zero periods equals demand\_probability. * 'clustered': runs of `cluster_size` consecutive demand periods separated by Geometric(demand\_probability) gaps. The overall demand fraction is cluster\_size / (1/demand\_probability + cluster\_size), not demand\_probability. demand\_probability == 0 means infinite gaps, i.e. an all-zero series. * 'seasonal': per-period Bernoulli with probability p(t) = demand\_probability + (seasonal\_peak\_prob - demand\_probability) \* (cos(2*pi*(t mod P)/P) + 1) / 2, which peaks at seasonal\_peak\_prob at the start of each cycle (t mod P == 0) and falls to demand\_probability mid-cycle.
Size distributions are moment-matched to (demand\_mean, demand\_std): * 'poisson': Poisson(demand\_mean); demand\_std is ignored. * 'negative\_binomial': p = mean/var, n = mean\*p/(1-p); falls back to Poisson when demand\_std\*\*2 \<= demand\_mean. * 'lognormal': mu = ln(mean^2 / sqrt(var + mean^2)), sigma^2 = ln(1 + var/mean^2). * 'gamma': shape = (mean/std)^2, scale = var/mean. Sizes are clipped from below at min\_demand. **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series | *required* | | `max_length` | [int](#int) | Maximum length of each series | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data (e.g. 'D', 'h', '5min') or int | *required* | | `demand_probability` | [float](#float) | Probability of non-zero demand per period (occurrence-pattern dependent, see above) (default: 0.2) | *required* | | `demand_distribution` | [str](#str) | Distribution for non-zero demand sizes (default: 'poisson') | *required* | | `demand_mean` | [float](#float) | Mean of demand when non-zero (default: 5.0) | *required* | | `demand_std` | [float](#float) | Std of demand when non-zero (default: 2.0) | *required* | | `intermittent_pattern` | [str](#str) | Occurrence pattern: 'random', 'clustered' or 'seasonal' (default: 'random') | *required* | | `cluster_size` | [int](#int) | Size of demand clusters (default: 3) | *required* | | `seasonal_period` | [int](#int) | Period for seasonal intermittency (default: 12) | *required* | | `seasonal_peak_prob` | [float](#float) | Peak occurrence probability at the start of each seasonal cycle (default: 0.4) | *required* | | `min_demand` | [int](#int) | Minimum non-zero demand value (default: 1) | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None) | *required* | #### `IntermittentDemandGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single intermittent demand time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | ------------------------------------------------------- | | [ndarray](#numpy.ndarray) | Array of values (mostly zeros with intermittent demand) | ### `IoTSensorGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate IoT sensor readings with realistic degradation patterns. The signal is `base_value + trend * t` plus optional sinusoidal seasonality, a calibration offset, a cumulative drift random walk (`drift_rate` per step with `drift_noise` variation), and Gaussian measurement noise. After `battery_life` steps, noise grows and the signal is attenuated at `battery_degradation_rate`. Failures can be injected as NaN gaps ('intermittent'), a permanent NaN tail ('complete'), or frozen readings ('stuck'). With `n_sensors > 1`, each generated "series" is a network of sensors whose measurement noise is spatially correlated (`corr[i, j] = spatial_correlation ** |i - j|`); each sensor becomes a separate output series. **Parameters:** | Name | Type | Description | Default | | -------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data, e.g. 's', 'min', 'h'. | *required* | | `n_sensors` | [int](#int) | Sensors per network (default: 1). | *required* | | `sensor_type` | [str](#str) | 'temperature', 'humidity', 'pressure', 'light', 'motion' or 'generic' (default: 'temperature'). | *required* | | `base_value` | [float](#float) \| None | Base sensor reading (default: typical value for sensor\_type). | *required* | | `trend` | [float](#float) | Linear trend per time step (default: 0.0). | *required* | | `seasonal_period` | [int](#int) | Seasonal cycle length in steps, 0 disables (default: 0). | *required* | | `seasonal_amplitude` | [float](#float) | Amplitude of seasonal variation (default: 0.0). | *required* | | `measurement_noise` | [float](#float) | Std of measurement noise (default: 0.1). | *required* | | `drift_rate` | [float](#float) | Deterministic sensor drift per step (default: 0.0). | *required* | | `drift_noise` | [float](#float) | Std of the random drift component (default: 0.01). | *required* | | `calibration_error` | [float](#float) | Constant calibration offset (default: 0.0). | *required* | | `battery_life` | [int](#int) \| None | Steps until battery degradation starts, None disables (default: None). | *required* | | `battery_degradation_rate` | [float](#float) | Rate of quality loss per step after battery\_life (default: 0.001). | *required* | | `failure_probability` | [float](#float) | Probability of sensor failure (default: 0.0). For 'complete': probability the series fails at all; for 'intermittent'/'stuck': per-step probability of starting a failure episode. | *required* | | `failure_type` | [str](#str) | 'intermittent', 'complete' or 'stuck' (default: 'intermittent'). | *required* | | `failure_duration` | [int](#int) | Length of intermittent/stuck episodes (default: 10). | *required* | | `stuck_value` | [float](#float) \| None | Reading during 'stuck' failures (default: the reading at episode start). | *required* | | `spatial_correlation` | [float](#float) | Noise correlation between adjacent sensors in a network (default: 0.5). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | #### `IoTSensorGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single sensor. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ----------------------------------------------- | | [ndarray](#numpy.ndarray) | Array of sensor readings (NaN during failures). | #### `IoTSensorGenerator.generate` ```python theme={null} generate(n_series, start_id=0, n_jobs=-1) ``` Generate IoT sensor data. With `n_sensors == 1`, generates `n_series` independent sensors. With `n_sensors > 1`, generates `n_series` sensor networks, each contributing `n_sensors` correlated series. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | -------------------------------------------------- | --------------- | | `n_series` | [int](#int) | Number of series/networks to generate. | *required* | | `start_id` | [int](#int) | Starting ID for the series numbering (default: 0). | 0 | | `n_jobs` | [int](#int) | Ignored; generation is sequential. | -1 | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | DataFrame in long format with columns \[id\_col, time\_col, target\_col]. | ### `EnergyLoadGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate electricity demand with nested daily/weekly/yearly cycles. The load is a base level plus: * A daily profile depending on `load_type`: residential has Gaussian morning/evening peaks, commercial a broad midday peak, industrial a near-constant profile with a night dip. * A weekly cycle: weekend reduction for residential/commercial, a sinusoidal pattern for industrial. * A yearly cosine cycle peaking around the series start (winter). * Temperature-driven load: both heating (cold) and cooling (hot) increase demand proportionally to `|temperature - base_temperature|`. * Holiday reductions, random extreme-weather multipliers, and Gaussian noise. The result is clipped at zero. Hour of day, day of week and day of year are derived from the step position relative to the series start using the step size implied by `freq` (an integer `freq` is treated as hourly). **Parameters:** | Name | Type | Description | Default | | ------------------------- | ---------------------------------------- | -------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data, e.g. 'h', '15min', 'D'. | *required* | | `base_load` | [float](#float) | Base load in kW or MW (default: 100.0). | *required* | | `load_type` | [str](#str) | 'residential', 'commercial' or 'industrial' (default: 'residential'). | *required* | | `daily_pattern` | [bool](#bool) | Enable the daily cycle (default: True). | *required* | | `daily_amplitude` | [float](#float) | Amplitude of the daily variation (default: 30.0). | *required* | | `weekly_pattern` | [bool](#bool) | Enable the weekly cycle (default: True). | *required* | | `weekly_amplitude` | [float](#float) | Amplitude of the weekly variation (default: 15.0). | *required* | | `yearly_pattern` | [bool](#bool) | Enable the yearly cycle (default: True). | *required* | | `yearly_amplitude` | [float](#float) | Amplitude of the yearly variation (default: 20.0). | *required* | | `temperature_sensitive` | [bool](#bool) | Enable temperature effects (default: True). | *required* | | `temperature_sensitivity` | [float](#float) | Load change per degree of deviation from base\_temperature (default: 2.0). | *required* | | `base_temperature` | [float](#float) | Reference temperature in Celsius (default: 20.0). | *required* | | `morning_peak_hour` | [int](#int) | Hour of the residential morning peak (default: 8). | *required* | | `evening_peak_hour` | [int](#int) | Hour of the residential evening peak (default: 19). | *required* | | `peak_amplitude` | [float](#float) | Additional load at the residential peaks (default: 40.0). | *required* | | `holiday_effect` | [float](#float) | Fractional load reduction on holidays (default: 0.3). | *required* | | `holiday_days` | [list](#list)\[[int](#int)] | Day-of-year indices (0-364) that are holidays (default: \[]). | *required* | | `extreme_weather_prob` | [float](#float) | Per-step probability of extreme weather (default: 0.0). | *required* | | `extreme_weather_impact` | [float](#float) | Load multiplier during extreme weather (default: 1.5). | *required* | | `noise_std` | [float](#float) | Standard deviation of additive noise (default: 5.0). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | #### `EnergyLoadGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single energy load series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------- | | [ndarray](#numpy.ndarray) | Array of energy load values. | ### `StateSpaceGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series from a (linear-Gaussian or custom) state space model. The linear model is: ``` x[t] = F x[t-1] + w[t], w[t] ~ (0, Q) (state equation) y[t] = H x[t] + v[t], v[t] ~ N(0, R) (observation equation) ``` with `x[0] ~ N(initial_state, initial_state_covariance)`. State noise `w` follows the configured innovation distribution (scaled by the Cholesky/PSD factor of Q); observation noise `v` is Gaussian. The univariate output is the first observation dimension `y[t][0]`, with y\[0] observing the initial state. Custom nonlinear dynamics can be supplied via `transition_fn` / `observation_fn`, each called as `fn(x, t, rng)`. **Parameters:** | Name | Type | Description | Default | | -------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data. A pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer time index step. | *required* | | `state_dim` | [int](#int) | Dimension of the hidden state vector (default: 1). | *required* | | `obs_dim` | [int](#int) | Dimension of the observation vector (default: 1). | *required* | | `transition_matrix` | [list](#list)\[[list](#list)\[[float](#float)]] \| None | State transition matrix F, shape (state\_dim, state\_dim). When None (and no transition\_fn), a random stable matrix is generated. | *required* | | `observation_matrix` | [list](#list)\[[list](#list)\[[float](#float)]] \| None | Observation matrix H, shape (obs\_dim, state\_dim). Default observes the first state. | *required* | | `state_covariance` | [list](#list)\[[list](#list)\[[float](#float)]] \| None | State noise covariance Q, symmetric PSD (default: 0.1 \* I). | *required* | | `obs_covariance` | [list](#list)\[[list](#list)\[[float](#float)]] \| None | Observation noise covariance R, symmetric PSD (default: 0.1 \* I). | *required* | | `transition_fn` | [Callable](#collections.abc.Callable) \| None | Custom state transition function. | *required* | | `observation_fn` | [Callable](#collections.abc.Callable) \| None | Custom observation function. | *required* | | `initial_state` | [list](#list)\[[float](#float)] \| None | Initial state mean (default: zeros). | *required* | | `initial_state_covariance` | [list](#list)\[[list](#list)\[[float](#float)]] \| None | Initial state covariance, symmetric PSD (default: identity). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | | `id_col` | [str](#str) | Name of the ID column (default: 'unique\_id'). | *required* | | `time_col` | [str](#str) | Name of the timestamp column (default: 'ds'). | *required* | | `target_col` | [str](#str) | Name of the value column (default: 'y'). | *required* | | `start_datetime` | [str](#str) | First timestamp (default: '2000-01-01'). | *required* | #### `StateSpaceGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single state space series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------------------------- | | [ndarray](#numpy.ndarray) | Observed values (first observation dimension). | #### `StateSpaceGenerator.generate_with_states` ```python theme={null} generate_with_states(n_series=1, start_id=0) ``` Generate series and return both observations and hidden states. Only missingness is applied to the observations (changepoints and anomalies would desynchronize them from the returned states). **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | ---------------------------------------------- | -------------- | | `n_series` | [int](#int) | Number of series to generate (default: 1). | 1 | | `start_id` | [int](#int) | Starting ID for series numbering (default: 0). | 0 | Returns: (observations DataFrame in long format, states DataFrame with one `state_j` column per state dimension). ### `DailyActiveUsersGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate Daily Active Users time series with event-driven jumps. The DAU level is `base_users * (1 + growth_rate)^day` with a weekend multiplier, plus a decaying boost from random events: with probability `event_probability` per step an event adds `(impact - 1) * base` to a boost that decays geometrically at rate `event_decay_rate`. Proportional Gaussian noise is added and the result is clipped at zero. The day index is derived from the step position relative to the series start using the step size implied by `freq` (an integer `freq` is treated as daily). Weekends are day indices 5 and 6 of each 7-day block. Also outputs an exogenous column marking the steps where events occur, usable as a feature for forecasting models. **Parameters:** | Name | Type | Description | Default | | ------------------- | --------------------------------------- | ------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data, e.g. 'D', 'h'. | *required* | | `base_users` | [float](#float) | Base number of daily active users (default: 10000.0). | *required* | | `growth_rate` | [float](#float) | Daily organic growth rate (default: 0.0005). | *required* | | `growth_rate_std` | [float](#float) | Std dev of a per-series perturbation of growth\_rate (default: 0.0). | *required* | | `app_type` | [str](#str) | 'consumer', 'business' or 'gaming' (default: 'consumer'). | *required* | | `weekly_pattern` | [bool](#bool) | Enable weekly seasonality (default: True). | *required* | | `weekend_factor` | [float](#float) \| None | Multiplier for weekend activity (default: 1.2 for gaming, 0.8 otherwise). | *required* | | `event_probability` | [float](#float) | Per-step probability of an event (default: 0.02). | *required* | | `event_impact_min` | [float](#float) | Minimum event impact multiplier (default: 1.2). | *required* | | `event_impact_max` | [float](#float) | Maximum event impact multiplier (default: 2.0). | *required* | | `event_decay_rate` | [float](#float) | Per-step decay rate of the event boost (default: 0.1). | *required* | | `noise_std` | [float](#float) | Std of noise relative to the current level (default: 0.05). | *required* | | `event_col` | [str](#str) | Name of the event indicator column (default: 'event'). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | #### `DailyActiveUsersGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single DAU time series. Also populates `self._current_events` with event indicators. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | -------------------- | | [ndarray](#numpy.ndarray) | Array of DAU values. | #### `DailyActiveUsersGenerator.generate` ```python theme={null} generate(n_series, start_id=0, n_jobs=-1) ``` Generate synthetic DAU time series data with event indicators. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | -------------------------------------------------- | --------------- | | `n_series` | [int](#int) | Number of time series to generate. | *required* | | `start_id` | [int](#int) | Starting ID for the series numbering (default: 0). | 0 | | `n_jobs` | [int](#int) | Ignored; generation is sequential. | -1 | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | DataFrame in long format with columns \[id\_col, time\_col, target\_col, event\_col], where event\_col is 1 at steps where an event occurred. | ### `VitalSignsGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate realistic vital signs time series for healthcare applications. Simulates one of six vital signs (heart rate, systolic/diastolic blood pressure, respiratory rate, SpO2, temperature) as a per-series baseline plus a slow random-walk drift, a circadian rhythm, heart rate variability (for HR and BP), random physiological events (activity bursts, rest periods, spikes), measurement noise, and cross-vital correlations with heart rate. Values are clipped to physiological bounds that depend on the patient archetype.
Note Circadian and HRV components assume one time step = 1 minute (freq='min'); other frequencies distort those cycle periods.
**Parameters:** | Name | Type | Description | Default | | ------------------- | --------------------------------------- | ------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data; use 'min' for correct circadian/HRV periods. | *required* | | `patient_type` | [str](#str) | 'healthy', 'cardiac', 'sepsis', 'respiratory' or 'hypertensive' (default: 'healthy'). | *required* | | `vital_sign` | [str](#str) | Which vital sign to output (default: 'heart\_rate'). | *required* | | `include_circadian` | [bool](#bool) | Include circadian rhythm effects (default: True). | *required* | | `include_hrv` | [bool](#bool) | Include heart rate variability (default: True). | *required* | | `include_events` | [bool](#bool) | Include random physiological events (default: True). | *required* | | `event_probability` | [float](#float) | Per-step probability of an event (default: 0.01). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* |
Example > > > gen = VitalSignsGenerator( > > > ... min\_length=1440, # 24 hours of per-minute data > > > ... max\_length=1440, > > > ... freq="min", > > > ... patient\_type="healthy", > > > ... vital\_sign="heart\_rate", > > > ... seed=42, > > > ... ) > > > df = gen.generate(n\_series=10)
#### `VitalSignsGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single vital signs time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of vital sign values. | #### `VitalSignsGenerator.generate_all_vitals` ```python theme={null} generate_all_vitals(n_series=1, start_id=0) ``` Generate all six vital signs for complete patient monitoring. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | -------------------------------------- | -------------- | | `n_series` | [int](#int) | Number of patients/series to generate. | 1 | | `start_id` | [int](#int) | Starting ID for the series numbering. | 0 | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | DataFrame with columns \[id\_col, time\_col] plus one column per vital sign, aligned on the same timestamps per patient. | #### `VitalSignsGenerator.get_model_info` ```python theme={null} get_model_info() ``` Get information about the vital signs model. **Returns:** | Type | Description | | -------------------------- | --------------------------------------------- | | [dict](#dict) | Model parameters and patient characteristics. | ### `ClickstreamGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate web clickstream/session time series for analytics applications. Human sessions per time bin are Poisson-distributed around `base_sessions` modulated by hour-of-day/day-of-week seasonality and a slow log-random-walk trend. Bot traffic (flatter profile plus occasional crawl spikes) can be added on top. Bounces, pageviews (geometric page depth for engaged sessions) and conversions are derived per bin from the human sessions, with multipliers depending on `traffic_source`. Output types: * 'sessions': total session counts (human + bot) per time bin * 'pageviews': total pageviews per time bin * 'conversions': conversion counts per time bin * 'bounce\_rate': bounced fraction of total sessions per time bin
Note Seasonality assumes hourly frequency (freq='h'). Other frequencies produce incorrect day/night and weekday patterns.
**Parameters:** | Name | Type | Description | Default | | --------------------- | --------------------------------------- | ---------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data; use 'h' for correct seasonality patterns. | *required* | | `base_sessions` | [float](#float) | Baseline sessions per time bin (default: 100). | *required* | | `traffic_source` | [str](#str) | 'organic', 'paid', 'direct', 'referral' or 'mixed' (default: 'mixed'). | *required* | | `conversion_rate` | [float](#float) | Base conversion rate for engaged sessions (default: 0.03). | *required* | | `bounce_rate` | [float](#float) | Base rate of single-page sessions (default: 0.40). | *required* | | `avg_session_depth` | [float](#float) | Average pages per engaged session (default: 3.5). | *required* | | `include_seasonality` | [bool](#bool) | Include time-of-day and day-of-week patterns (default: True). | *required* | | `include_bots` | [bool](#bool) | Include bot traffic (default: True). | *required* | | `bot_fraction` | [float](#float) | Fraction of traffic from bots, \< 1.0 (default: 0.15). | *required* | | `output_type` | [str](#str) | Metric to output (default: 'sessions'). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* |
Example > > > gen = ClickstreamGenerator( > > > ... min\_length=168, # 1 week of hourly data > > > ... max\_length=168, > > > ... freq="h", > > > ... base\_sessions=500, > > > ... output\_type="sessions", > > > ... seed=42, > > > ... ) > > > df = gen.generate(n\_series=10)
#### `ClickstreamGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single clickstream time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ----------------------- | | [ndarray](#numpy.ndarray) | Array of metric values. | #### `ClickstreamGenerator.generate_full_metrics` ```python theme={null} generate_full_metrics(n_series=1, start_id=0) ``` Generate all clickstream metrics for complete analytics. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | ------------------------------ | -------------- | | `n_series` | [int](#int) | Number of series to generate. | 1 | | `start_id` | [int](#int) | Starting ID for series naming. | 0 | **Returns:** | Type | Description | | ------------------------------------------------------------------- | ------------------------------------------------------------------- | | [dict](#dict)\[[str](#str), [ndarray](#numpy.ndarray)] | All metrics as flat arrays, keyed by metric name plus 'series\_id'. | #### `ClickstreamGenerator.generate_funnel` ```python theme={null} generate_funnel(n_sessions=1000, stages=None) ``` Generate a conversion funnel with stage-by-stage drop-off. Retention between stages rises from \~0.4 to \~0.7 (committed users drop off less), adjusted by the traffic source's conversion multiplier. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------ | --------------------------------------------------------- | ----------------- | | `n_sessions` | [int](#int) | Number of sessions entering the funnel. | 1000 | | `stages` | [list](#list)\[[str](#str)] \| None | Funnel stage names (default: standard e-commerce funnel). | None | **Returns:** | Type | Description | | ----------------------------------------------------- | ------------------------------------------ | | [dict](#dict)\[[str](#str), [int](#int)] | Stage name -> session count at that stage. | #### `ClickstreamGenerator.get_model_info` ```python theme={null} get_model_info() ``` Get information about the clickstream model. **Returns:** | Type | Description | | -------------------------- | --------------------------------------------- | | [dict](#dict) | Model parameters and traffic characteristics. | # Multivariate Generators Source: https://nixtlaverse.nixtla.io/synforecast/generators_multivariate.html Copula, VAR, and Gaussian Process generators for correlated time series ### `CopulaGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate correlated time series using Gaussian or t copulas. Copulas model the dependence structure between variables independently of their marginal distributions. Sampling proceeds in two steps: 1. Draw correlated uniforms from the copula. Gaussian copula: `z ~ N(0, R)`, `u_i = Phi(z_i)`. t copula: `z ~ N(0, R)`, `w ~ chi2(df)`, `u_i = T_df(z_i * sqrt(df / w))` (the chi-square mixing is shared across variables, which creates tail dependence). 2. Map each uniform through the inverse CDF of its marginal: `x_i = F_i^{-1}(u_i)`. For the Gaussian copula the rank correlations satisfy `spearman = (6 / pi) * arcsin(rho / 2)` and for both copulas `kendall_tau = (2 / pi) * arcsin(rho)`. `generate(n_series)` creates `n_series` correlated variables sharing one length; each variable is one `unique_id` in the long-format output. Samples are i.i.d. over time (no serial dependence). **Parameters:** | Name | Type | Description | Default | | ------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data. A pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer time index step. | *required* | | `copula_type` | [str](#str) | 'gaussian' or 't' (default: 'gaussian'). | *required* | | `correlation_matrix` | [list](#list)\[[list](#list)\[[float](#float)]] \| None | Correlation matrix. Must be symmetric positive definite with unit diagonal. When None, a random correlation matrix is generated. When smaller than n\_series, it is padded with an identity block (extra variables are independent); when larger, the leading principal submatrix is used. | *required* | | `df` | [float](#float) | Degrees of freedom for the t copula (default: 5.0). | *required* | | `marginal_distributions` | [list](#list)\[[dict](#dict)] | Marginal specs, cycled over variables. Types: 'normal' (loc, scale), 'lognormal' (mean, sigma), 'exponential' (scale), 'uniform' (low, high), 'gamma' (shape, scale). Default: standard normal. | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | | `id_col` | [str](#str) | Name of the ID column (default: 'unique\_id'). | *required* | | `time_col` | [str](#str) | Name of the timestamp column (default: 'ds'). | *required* | | `target_col` | [str](#str) | Name of the value column (default: 'y'). | *required* | | `start_datetime` | [str](#str) | First timestamp (default: '2000-01-01'). | *required* | #### `CopulaGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single univariate series. Provided for compatibility with BaseGenerator; use generate(n\_series) for multivariate output. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values. | #### `CopulaGenerator.generate` ```python theme={null} generate(n_series, start_id=0, n_jobs=-1) ``` Generate n\_series correlated series with copula dependence. Overrides the base generate() for multivariate output: n\_series is the number of correlated variables, all sharing a single length. Generation is inherently joint, so n\_jobs has no effect. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | ---------------------------------------------- | --------------- | | `n_series` | [int](#int) | Number of correlated series (variables). | *required* | | `start_id` | [int](#int) | Starting ID for series numbering (default: 0). | 0 | | `n_jobs` | [int](#int) | Unused (accepted for API compatibility). | -1 | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | DataFrame in long format with columns \[id\_col, time\_col, target\_col]; each unique\_id is one correlated variable. | ### `VARGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate correlated time series using a Vector Autoregression model. A VAR(p) process models each variable as a linear function of past values of all variables: ``` y[t] = c + A_1 y[t-1] + ... + A_p y[t-p] + e[t], e[t] ~ (0, Sigma) ``` The process is stable (stationary) iff the companion matrix ``` [[A_1 ... A_p], [I 0 ... 0], ..., [0 ... I 0]] ``` has spectral radius \< 1, in which case the stationary mean is `(I - A_1 - ... - A_p)^{-1} c`. Innovations are drawn from the configured innovation distribution and correlated via the Cholesky factor of Sigma. A burn-in of 100 steps is discarded. `generate(n_series)` creates `n_series` correlated variables sharing one length; each variable is one `unique_id` in the long-format output. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data. A pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer time index step. | *required* | | `lag_order` | [int](#int) | VAR lag order p (default: 1). | *required* | | `coef_matrices` | [list](#list)\[[list](#list)\[[list](#list)\[[float](#float)]]] \| None | One square coefficient matrix per lag, all the same size. Must define a stable VAR. When None, random stable coefficients are generated. When sized differently from n\_series, the leading principal submatrices are used (or zero-padded). | *required* | | `intercept` | [list](#list)\[[float](#float)] \| None | Intercept vector c (default: zeros). | *required* | | `innovation_covariance` | [list](#list)\[[list](#list)\[[float](#float)]] \| None | Innovation covariance Sigma; symmetric positive definite (default: identity). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | | `id_col` | [str](#str) | Name of the ID column (default: 'unique\_id'). | *required* | | `time_col` | [str](#str) | Name of the timestamp column (default: 'ds'). | *required* | | `target_col` | [str](#str) | Name of the value column (default: 'y'). | *required* | | `start_datetime` | [str](#str) | First timestamp (default: '2000-01-01'). | *required* | #### `VARGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single (univariate) VAR series. Provided for compatibility with BaseGenerator; use generate(n\_series) for multivariate output. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ----------------------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values (univariate). | #### `VARGenerator.generate` ```python theme={null} generate(n_series, start_id=0, n_jobs=-1) ``` Generate n\_series correlated time series using the VAR model. Overrides the base generate() for multivariate output: n\_series is the number of correlated variables, all sharing a single length. Generation is inherently joint, so n\_jobs has no effect. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | ---------------------------------------------- | --------------- | | `n_series` | [int](#int) | Number of correlated series (variables). | *required* | | `start_id` | [int](#int) | Starting ID for series numbering (default: 0). | 0 | | `n_jobs` | [int](#int) | Unused (accepted for API compatibility). | -1 | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | DataFrame in long format with columns \[id\_col, time\_col, target\_col]; each unique\_id is one correlated variable. | ### `GaussianProcessGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series by sampling from a Gaussian Process. Samples `f ~ GP(mean, k)` on the integer grid t = 0..length-1, so the marginal distribution is `N(mean, amplitude^2 + noise_variance)` and the correlation at lag r is `k(r) / k(0)`. Kernels (r = |t - t'|, l = length\_scale, a = amplitude): * rbf: `a^2 exp(-r^2 / (2 l^2))` — infinitely differentiable, very smooth paths * matern\_0.5: `a^2 exp(-r/l)` — rough, Ornstein-Uhlenbeck-like * matern\_1.5: `a^2 (1+s) exp(-s)`, s = sqrt(3) r / l — once-differentiable * matern\_2.5: `a^2 (1+s+s^2/3) exp(-s)`, s = sqrt(5) r / l — twice-differentiable * periodic: `a^2 exp(-2 sin^2(pi r / period) / l^2)` — exact periodicity **Parameters:** | Name | Type | Description | Default | | ---------------- | ---------------------------- | ----------------------------------------------------------------------------------------------- | ---------- | | `kernel` | [str](#str) | Kernel type (default: 'rbf'). | *required* | | `length_scale` | [float](#float) | Kernel length scale (default: 20.0). | *required* | | `amplitude` | [float](#float) | Signal amplitude / output scale (default: 1.0). | *required* | | `period` | [float](#float) | Period for the periodic kernel (default: 50.0). | *required* | | `mean` | [float](#float) | Mean function value (default: 0.0). | *required* | | `noise_variance` | [float](#float) | Observation noise variance, also acts as jitter for the Cholesky factorization (default: 1e-6). | *required* | #### `GaussianProcessGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate a single GP sample path. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | #### `GaussianProcessGenerator.get_model_info` ```python theme={null} get_model_info() ``` Return information about the GP configuration. # Pretraining Generators Source: https://nixtlaverse.nixtla.io/synforecast/generators_pretraining.html TSI, TCM, and KernelSynth generators for foundation-model pretraining ### `TSIGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate series by composing randomized Trend, Seasonality and Irregularity components. The component-based construction is based on Bahrpeyma et al. (2021), "A Methodology for Validating Diversity in Synthetic Time Series Generation," [https://doi.org/10.1016/j.mex.2021.101459](https://doi.org/10.1016/j.mex.2021.101459). SynForecast's component families, sampling distributions, and stability guards are its own extensions rather than a reproduction of that paper's generator. Every series draws a fresh random configuration: a trend type from `trend_types`, 0-3 seasonal harmonics with periods from `seasonal_periods` (integer and non-integer, so multiple harmonics are incommensurate), and an irregular (noise) process from `irregular_types`. The components are combined additively, or multiplicatively with probability `multiplicative_prob` when the trend base can be kept positive: ``` additive: y_t = T_t + S_t + e_t multiplicative: y_t = T_t · (1 + S_t / c) + e_t, min_t T_t > 0 ``` where c caps the relative seasonal swing so the factor stays positive. Trend shapes are normalized so their total movement over the series is drawn from `trend_slope_range` regardless of length; harmonic amplitudes are log-uniform; the noise scale is a log-uniform fraction of the structural signal's standard deviation, so the pool spans signal-dominated through noise-dominated series. A per-series level and log-uniform scale spread series across magnitudes. Degenerate or exploding draws (non-finite, |y| >= 1e8, or constant) are redrawn a bounded number of times. **Parameters:** | Name | Type | Description | Default | | | | --------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | ---------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | | | `freq` | [str](#str) \| [int](#int) | Pandas offset alias (e.g. 'D', 'h', '5min') or an integer time step. | *required* | | | | `trend_types` | [list](#list)\[[str](#str)] | Trend shapes sampled per series. Options: 'none', 'linear', 'exponential', 'logistic', 'piecewise\_linear', 'damped' (default: all six). | *required* | | | | `trend_slope_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Range of the signed total trend movement over the whole series (default: (-8.0, 8.0)). | *required* | | | | `trend_growth_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Range of the exponential trend's total log-curvature (default: (1.0, 4.0)). | *required* | | | | `n_breakpoints_range` | [tuple](#tuple)\[[int](#int), [int](#int)] | Breakpoint count for piecewise-linear trends (default: (1, 3)). | *required* | | | | `level_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Per-series base level draw (default: (-10.0, 10.0)). | *required* | | | | `n_seasonal_range` | [tuple](#tuple)\[[int](#int), [int](#int)] | Number of seasonal harmonics per series (default: (0, 3)). | *required* | | | | `seasonal_periods` | [list](#list)\[[float](#float)] | Period pool, in time steps; mixes integer and non-integer/co-prime periods (default includes 7, 12, 24, ..., 365.25 and 5.5, 11.3, 19.7, 29.53). | *required* | | | | `seasonal_amplitude_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Log-uniform harmonic amplitude range (default: (0.2, 3.0)). | *required* | | | | `amplitude_modulation_prob` | [float](#float) | Probability a harmonic gets a slowly varying amplitude envelope (default: 0.4). | *required* | | | | `harmonics_prob` | [float](#float) | Probability a harmonic gets phase-locked 2f/3f overtones at decaying amplitude (default: 0.4). | *required* | | | | `irregular_types` | [list](#list)\[[str](#str)] | Noise processes sampled per series. Options: 'gaussian', 'ar1', 'garch\_like', 'student\_t', 'laplace' (default: all five). | *required* | | | | `noise_scale_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Log-uniform noise std as a fraction of the structural signal's std (default: (0.5, 12.0)). | *required* | | | | `ar1_phi_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | AR(1) coefficient range for 'ar1' noise, | phi | \< 1 (default: (0.3, 0.95)). | *required* | | `tail_df_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Student-t degrees of freedom range for 'student\_t' noise, > 2 (default: (2.5, 12.0)). | *required* | | | | `multiplicative_prob` | [float](#float) | Probability of multiplicative trend-season composition (default: 0.3). | *required* | | | | `scale_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Log-uniform overall output scale (default: (0.1, 100.0)). | *required* | | | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | | | #### `TSIGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single TSI-composed time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | ### `TCMGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate series from a random temporal structural causal model (SCM). Each series gets a freshly sampled SCM over `n_vars` latent variables: a sparse dependency graph over the (variable x lag) space is drawn, each edge is assigned a random edge function, and the system is rolled out autoregressively. Node `i` evolves as ``` x_i[t] = sum_{e in pa(i)} f_e(x_{j_e}[t - l_e]) + eps_i[t] ``` with per-edge functions `f_e(x)` in ``` c*x, c*tanh(x), c*relu(x), c*tanh(x)*tanh(x'), c*1[x > tau] ``` The temporal-SCM framing follows the overview in Runge et al. (2023), "Causal inference for time series," [https://doi.org/10.1038/s43017-023-00431-y](https://doi.org/10.1038/s43017-023-00431-y). The particular graph sampler, edge-function mixture, stability rescaling, and guards here are original SynForecast design choices; this is not a reproduction of a named TCM generator from that paper or from Chronos-2. where `x'` is a second randomly-paired parent (product interaction) and `tau` a random threshold. Saturating kinds carry a log-uniform softness scale `s` and contribute `c*s*tanh(x/s)` (slope c near 0, bounded output). The returned univariate series is node 0 (nodes are exchangeable by construction); the remaining nodes act as latent parents, i.e. realistic exogenous-looking drivers. This produces genuine causal temporal structure — autocorrelation at sampled lags, lead-lag effects, nonlinear/regime-like dynamics — that component mixing cannot. Diversity is shaped per series: edge kinds follow a random Dirichlet mixture over `edge_kinds` (some series linear-dominated, others nonlinearity-dominated), coefficient magnitudes decay geometrically with lag (short-lag dominance), and, when 'linear' is in the pool, every node gets a positive linear self lag-1 edge so the observed node carries its own persistence. Stability: the linear-gain part (linear/tanh/relu edges) is assembled into VAR companion form and its coefficients are rescaled toward a per-series spectral-radius target below `stability_margin` — drawn near the margin with probability 0.22 (persistent, spectrally peaked series) and well below it otherwise (noise-like series); bounded-output edges cannot destabilize the core and keep their coefficients. During rollout every state is additionally soft-clamped via `clamp * tanh(x / clamp)` so nonlinear feedback cannot diverge. If a trajectory still fails the finiteness/scale guard, the SCM is redrawn (up to 5 times), then a guaranteed-stable linear AR(1) is used. The counters `_redraw_total` / `_fallback_total` and the last accepted SCM `_last_scm` are exposed for introspection on direct `generate_single_series` calls. Multivariate mode: with `multivariate=True`, `generate(n_series)` samples a single SCM (with at least `n_series` variables — the lower bound of `n_vars_range` is clamped up as needed) and one shared length, rolls the system out once, and returns the first `n_series` nodes as separate series in the long-format output (one `unique_id` per node, following `VARGenerator`). Because the nodes share one causal graph, they are genuinely cross-dependent at the sampled lags. The default `multivariate=False` keeps the univariate behavior: `n_series` independent SCMs, one observed node each. **Parameters:** | Name | Type | Description | Default | | ------------------------ | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data. A pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer time index step. | *required* | | `multivariate` | [bool](#bool) | When True, generate(n\_series) returns n\_series nodes of one shared SCM as correlated series sharing one length (default: False). | *required* | | `n_vars_range` | [tuple](#tuple)\[[int](#int), [int](#int)] | Inclusive range for the number of latent variables per SCM (default: (1, 5)). | *required* | | `max_lag_range` | [tuple](#tuple)\[[int](#int), [int](#int)] | Inclusive range for the maximum lag L of the dependency graph (default: (1, 24)). | *required* | | `edge_probability_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Range for the per-slot edge probability over the (variable x lag) space (default: (0.05, 0.3)). | *required* | | `edge_kinds` | [list](#list)\[[str](#str)] | Pool of edge function kinds, sampled per edge. Subset of \['linear', 'tanh', 'relu', 'product', 'threshold'] (default: all). | *required* | | `coef_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Range for edge coefficient magnitudes before stability rescaling; signs are random (default: (0.1, 0.8)). | *required* | | `stability_margin` | [float](#float) | Upper bound (\< 1) on the spectral radius of the linear-part companion matrix (default: 0.95). | *required* | | `clamp_threshold` | [float](#float) | Soft-clamp scale for states during rollout; generous relative to typical noise scales so it only engages on runaway feedback (default: 1e6). | *required* | | `noise_types` | [list](#list)\[[str](#str)] | Pool of per-node innovation distributions. Subset of \['gaussian', 'student\_t', 'laplace'] (default: all). | *required* | | `noise_scale_range` | [tuple](#tuple)\[[float](#float), [float](#float)] | Range for per-node noise standard deviation (default: (0.5, 2.0)). | *required* | | `heteroscedastic_prob` | [float](#float) | Probability that a node's noise scale follows a slow random sinusoidal envelope (default: 0.2). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | | `id_col` | [str](#str) | Name of the ID column (default: 'unique\_id'). | *required* | | `time_col` | [str](#str) | Name of the timestamp column (default: 'ds'). | *required* | | `target_col` | [str](#str) | Name of the value column (default: 'y'). | *required* | | `start_datetime` | [str](#str) | First timestamp (default: '2000-01-01'). | *required* |
Example > > > gen = TCMGenerator( > > > ... min\_length=256, > > > ... max\_length=512, > > > ... freq="h", > > > ... seed=42, > > > ... ) > > > df = gen.generate(n\_series=10)
#### `TCMGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single TCM series. Samples a fresh random SCM, rolls it out (with burn-in), and returns the target node. Redraws the SCM on guard failure, falling back to a stable linear AR(1) after `_MAX_REDRAWS` redraws. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values. | #### `TCMGenerator.generate` ```python theme={null} generate(n_series, start_id=0, n_jobs=-1) ``` Generate n\_series time series from temporal causal models. With `multivariate=False` (default) this is the base behavior: n\_series independent SCMs, one observed node each. With `multivariate=True` the n\_series series are the first n\_series nodes of one shared SCM, sharing a single length (following VARGenerator); generation is inherently joint, so n\_jobs has no effect in that mode. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | --------------------------------------------------------------------------------------------------- | --------------- | | `n_series` | [int](#int) | Number of series to generate. In multivariate mode, the number of observed nodes of one shared SCM. | *required* | | `start_id` | [int](#int) | Starting ID for series numbering (default: 0). | 0 | | `n_jobs` | [int](#int) | Parallel workers for the univariate path; unused in multivariate mode. | -1 | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | DataFrame in long format with columns \[id\_col, time\_col, target\_col]. | ### `KernelSynthGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate series by sampling from randomly composed Gaussian-process kernels. This adapts the KernelSynth recipe introduced for pretraining the Chronos forecasting models (Ansari et al. 2024, "Chronos: Learning the Language of Time Series", [https://arxiv.org/abs/2403.07815](https://arxiv.org/abs/2403.07815)) and its Apache-2.0-licensed reference implementation ([https://github.com/amazon-science/chronos-forecasting/blob/main/scripts/kernel-synth.py](https://github.com/amazon-science/chronos-forecasting/blob/main/scripts/kernel-synth.py)). For each series the generator draws `1..max_kernels` base kernels (with replacement) from a fixed bank, folds them together with randomly chosen binary operators (`+` or `*`), and samples one path from the resulting GP prior on the normalized grid `x = linspace(0, 1, length)`. Kernel addition mixes behaviors (e.g. trend + seasonality); kernel multiplication modulates them (e.g. locally periodic, amplitude-varying seasonality). SynForecast makes the bank configurable, expresses seasonal periods in time steps on a normalized grid, and adds bounded retries, divergence guards, and optional standardization. Base kernels (r = |x\_i - x\_j|, all on the normalized grid): * rbf: `exp(-r^2 / (2 l^2))` — smooth, length-scale `l` * rational\_quadratic: `(1 + r^2 / (2 a))^(-a)` — scale mixture of RBFs, shape `a` * periodic (ExpSineSquared): `exp(-2 sin^2(pi r / p_norm))` with `p_norm = period / length` so `period` is expressed in time steps * linear (DotProduct): `s^2 + x_i x_j` — trend / drift * white: `w` on the diagonal — independent noise * constant: a constant offset Because a composed kernel can be near-degenerate or produce an exploding scale, non-finite, near-constant, or `|y| >= 1e8` draws are redrawn a bounded number of times before falling back to Gaussian noise. **Parameters:** | Name | Type | Description | Default | | --------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `max_kernels` | [int](#int) | Maximum number of base kernels composed per series; the count is drawn uniformly from `1..max_kernels` (default: 5). | *required* | | `seasonal_periods` | [list](#list)\[[float](#float)] | Periodic-kernel periods, in time steps, forming the periodic entries of the bank (default: a broad set from 4 up to 730 covering common hourly/daily/weekly/quarterly/yearly seasonalities). | *required* | | `rbf_length_scales` | [list](#list)\[[float](#float)] | RBF length scales on the normalized grid (default: \[0.1, 1.0, 10.0]). | *required* | | `rational_quadratic_alphas` | [list](#list)\[[float](#float)] | Rational-quadratic shape parameters (default: \[0.1, 1.0, 10.0]). | *required* | | `linear_sigmas` | [list](#list)\[[float](#float)] | `sigma_0` offsets for the linear (DotProduct) kernel (default: \[0.0, 1.0, 10.0]). | *required* | | `white_noise_levels` | [list](#list)\[[float](#float)] | Diagonal noise levels for the white kernel (default: \[0.1, 1.0]). | *required* | | `include_constant` | [bool](#bool) | Include a constant kernel in the bank (default: True). | *required* | | `jitter` | [float](#float) | Diagonal jitter added before factorization for numerical stability (default: 1e-6). | *required* | | `standardize` | [bool](#bool) | Standardize each sampled series to zero mean and unit variance. Kernel compositions span extreme scales, so standardization keeps the pool comparable for pretraining (default: True). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | #### `KernelSynthGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate one KernelSynth series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | # Statistical Generators Source: https://nixtlaverse.nixtla.io/synforecast/generators_statistical.html RandomWalk, Seasonal, SARIMA, ETS, and INAR generators ### `RandomWalkGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate random walk time series. y\_t = y\_\{t-1} + drift + ε\_t, where ε\_t has standard deviation `volatility` and is drawn from `innovation_distribution`. The first output value already includes one step: y\_1 = start\_value + drift + ε\_1. **Parameters:** | Name | Type | Description | Default | | ---------------- | --------------------------------------- | --------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency, a pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer time step. | *required* | | `drift` | [float](#float) | Mean of the random steps (default: 0.0). | *required* | | `volatility` | [float](#float) | Standard deviation of random steps (default: 1.0). | *required* | | `start_value` | [float](#float) | Initial value for all series (default: 0.0). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | | `id_col` | [str](#str) | Name of the ID column (default: 'unique\_id'). | *required* | | `time_col` | [str](#str) | Name of the timestamp column (default: 'ds'). | *required* | | `target_col` | [str](#str) | Name of the value column (default: 'y'). | *required* | | `start_datetime` | [str](#str) | First timestamp of every series (default: '2000-01-01'). | *required* | #### `RandomWalkGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single random walk time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | ### `SeasonalGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series with seasonal patterns. y\_t = base\_level + amplitude · sin(2π t / period) + trend · t + ε\_t, where ε\_t has standard deviation `noise_level`. **Parameters:** | Name | Type | Description | Default | | ----------------------- | --------------------------------------- | --------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency, a pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer time step. | *required* | | `seasonality_period` | [int](#int) | Period of seasonality in time steps (default: 24). | *required* | | `seasonality_amplitude` | [float](#float) | Amplitude of seasonal component (default: 10.0). | *required* | | `trend` | [float](#float) | Linear trend coefficient per time step (default: 0.0). | *required* | | `noise_level` | [float](#float) | Standard deviation of noise (default: 1.0). | *required* | | `base_level` | [float](#float) | Base level of the series (default: 50.0). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | | `id_col` | [str](#str) | Name of the ID column (default: 'unique\_id'). | *required* | | `time_col` | [str](#str) | Name of the timestamp column (default: 'ds'). | *required* | | `target_col` | [str](#str) | Name of the value column (default: 'y'). | *required* | | `start_datetime` | [str](#str) | First timestamp of every series (default: '2000-01-01'). | *required* | #### `SeasonalGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single seasonal time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | ### `SARIMAGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series based on Seasonal ARIMA (SARIMAX) processes. Creates time series using a Seasonal AutoRegressive Integrated Moving Average model with optional eXogenous regressors. The model is defined by (p,d,q)x(P,D,Q,s). The SARIMA model uses multiplicative seasonal structure: * AR polynomial: φ(B)Φ(B^s) where B is the backshift operator * MA polynomial: θ(B)Θ(B^s) * Differencing: (1-B)^d (1-B^s)^D For SARIMA(1,1,1)(1,1,1)\_12, this creates dependencies at lags: * AR: 1, 12, 13 (from φ₁, Φ₁, φ₁Φ₁) * MA: 1, 12, 13 (from θ₁, Θ₁, θ₁Θ₁) **Parameters:** | Name | Type | Description | Default | | ----------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency, a pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer time step. | *required* | | `p` | [int](#int) | AR order (default: 1). | *required* | | `d` | [int](#int) | Differencing order, 0-2 (default: 0). | *required* | | `q` | [int](#int) | MA order (default: 1). | *required* | | `P` | [int](#int) | Seasonal AR order (default: 1). | *required* | | `D` | [int](#int) | Seasonal differencing order, 0-2 (default: 0). | *required* | | `Q` | [int](#int) | Seasonal MA order (default: 1). | *required* | | `seasonal_period` | [int](#int) | Seasonal period s (default: 12). | *required* | | `ar_params` | [list](#list)\[[float](#float)] \| None | AR coefficients φ₁,...,φ\_p (default: random stable). | *required* | | `ma_params` | [list](#list)\[[float](#float)] \| None | MA coefficients θ₁,...,θ\_q (default: random in (-0.5, 0.5)). | *required* | | `seasonal_ar_params` | [list](#list)\[[float](#float)] \| None | Seasonal AR coefficients Φ₁,...,Φ\_P (default: random stable). | *required* | | `seasonal_ma_params` | [list](#list)\[[float](#float)] \| None | Seasonal MA coefficients Θ₁,...,Θ\_Q (default: random in (-0.5, 0.5)). | *required* | | `mean` | [float](#float) | Process mean for stationary models (d=0, D=0) (default: 0.0). | *required* | | `drift` | [float](#float) | Constant added to the differenced series for integrated models (d>0 or D>0); yields slope `drift` per step when d=1 (default: 0.0). | *required* | | `noise_std` | [float](#float) | Standard deviation of innovation noise (default: 1.0). | *required* | | `burn_in` | [int](#int) \| None | Burn-in period; None computes it from model order and AR persistence (default: None). | *required* | | `validate_stationarity` | [bool](#bool) | Validate AR parameters for stationarity (default: True). | *required* | | `exog_coefficients` | [list](#list)\[[float](#float)] \| None | Coefficients for exogenous regressors (default: None). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | | `id_col` | [str](#str) | Name of the ID column (default: 'unique\_id'). | *required* | | `time_col` | [str](#str) | Name of the timestamp column (default: 'ds'). | *required* | | `target_col` | [str](#str) | Name of the value column (default: 'y'). | *required* | | `start_datetime` | [str](#str) | First timestamp of every series (default: '2000-01-01'). | *required* | #### `SARIMAGenerator.generate_single_series` ```python theme={null} generate_single_series(length, exog=None) ``` Generate values for a single SARIMA time series. The generation process: 1. Generate white noise innovations 2. Apply MA filtering to get MA component 3. Apply AR filtering recursively 4. Apply inverse differencing to get integrated process 5. Add mean/drift and exogenous effects **Parameters:** | Name | Type | Description | Default | | -------- | ---------------------------------------------- | ----------------------------------------------- | ----------------- | | `length` | [int](#int) | The length of the series to generate | *required* | | `exog` | [ndarray](#numpy.ndarray) \| None | Exogenous regressors of shape (length, n\_exog) | None | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | #### `SARIMAGenerator.get_model_info` ```python theme={null} get_model_info() ``` Get information about the SARIMA model configuration. **Returns:** | Type | Description | | ------------------------------------------------------------ | ------------------------------------------------------------------------ | | [dict](#dict)\[[str](#str), [Any](#typing.Any)] | Model information including orders, parameters, and polynomial structure | ### `ETSGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series based on ETS (Error, Trend, Seasonal) models. Creates time series from the innovations state space form of exponential smoothing (Hyndman, Koehler, Ord & Snyder, 2008). Each component is additive (A), multiplicative (M), or absent (N): * y\_t = μ\_t + ε\_t (additive error) or y\_t = μ\_t (1 + ε\_t) (multiplicative) * μ\_t combines level l, trend b (optionally damped by φ), and seasonal s, e.g. ETS(A,A,A): μ\_t = l\_\{t-1} + φ b\_\{t-1} + s\_\{t-m} * States update per the standard taxonomy, e.g. ETS(A,A,A): l\_t = l\_\{t-1} + φ b\_\{t-1} + α ε\_t; b\_t = φ b\_\{t-1} + β ε\_t; s\_t = s\_\{t-m} + γ ε\_t Common models: ETS(A,N,N) simple exponential smoothing, ETS(A,A,N) Holt, ETS(A,A,A) additive Holt-Winters, ETS(M,A,M) multiplicative Holt-Winters, ETS(A,Ad,A) damped Holt-Winters. **Parameters:** | Name | Type | Description | Default | | ----------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency, a pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer time step. | *required* | | `error_type` | [str](#str) | Error component, 'add' or 'mul' (default: 'add'). | *required* | | `trend_type` | [str](#str) \| None | Trend component, 'add', 'mul', or None (default: 'add'). | *required* | | `seasonal_type` | [str](#str) \| None | Seasonal component, 'add', 'mul', or None (default: 'add'). | *required* | | `seasonal_period` | [int](#int) | Seasonal period m (default: 12). | *required* | | `level` | [float](#float) | Initial level l\_0 (default: 100.0). | *required* | | `trend` | [float](#float) | Initial trend b\_0 (default: 0.0; reset to 1.0 for multiplicative trend when \<= 0). | *required* | | `seasonal` | [list](#list)\[[float](#float)] \| None | Initial seasonal states, one per season (default: random, zero-sum for additive / unit-mean for multiplicative). | *required* | | `alpha` | [float](#float) | Level smoothing parameter in \[0, 1] (default: 0.3). | *required* | | `beta` | [float](#float) | Trend smoothing parameter in \[0, 1] (default: 0.1). | *required* | | `gamma` | [float](#float) | Seasonal smoothing parameter in \[0, 1] (default: 0.1). | *required* | | `phi` | [float](#float) | Damping parameter in \[0, 1], used when damped=True (default: 0.98). | *required* | | `damped` | [bool](#bool) | Whether to damp the trend (default: False). | *required* | | `noise_std` | [float](#float) | Standard deviation of the innovations ε (default: 1.0). | *required* | | `box_cox_lambda` | [float](#float) \| None | If set, apply the inverse Box-Cox transform with this λ to the generated series (default: None). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | | `id_col` | [str](#str) | Name of the ID column (default: 'unique\_id'). | *required* | | `time_col` | [str](#str) | Name of the timestamp column (default: 'ds'). | *required* | | `target_col` | [str](#str) | Name of the value column (default: 'y'). | *required* | | `start_datetime` | [str](#str) | First timestamp of every series (default: '2000-01-01'). | *required* | #### `ETSGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single ETS time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | #### `ETSGenerator.generate_with_states` ```python theme={null} generate_with_states(n_series=1, start_id=0) ``` Generate series and return both observations and hidden states. This is useful for analyzing the underlying ETS state evolution. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | ------------------------------------------ | -------------- | | `n_series` | [int](#int) | Number of series to generate (default: 1) | 1 | | `start_id` | [int](#int) | Starting ID for series naming (default: 0) | 0 | **Returns:** | Type | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [tuple](#tuple)\[[IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT), [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT)] | tuple\[DataFrame, DataFrame]: - DataFrame with observations (id\_col, time\_col, target\_col) - DataFrame with states (id\_col, time\_col, level, trend, seasonal\_\*) | #### `ETSGenerator.get_model_info` ```python theme={null} get_model_info() ``` Get information about the ETS model configuration. **Returns:** | Type | Description | | ------------------------------------------------------------ | ------------------------------------------------------- | | [dict](#dict)\[[str](#str), [Any](#typing.Any)] | Model information including type, parameters, and state | ### `INARGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate integer-valued time series with autoregressive structure. INAR(p) models use binomial thinning to maintain integer values while preserving autoregressive dynamics: ``` X_t = alpha_1 o X_{t-1} + ... + alpha_p o X_{t-p} + epsilon_t ``` where 'o' is binomial thinning, alpha o X = sum\_\{i=1}^\{X} Bernoulli(alpha), and epsilon\_t are i.i.d. count innovations (Poisson or negative binomial). Stationarity requires sum(alpha) \< 1, giving unconditional mean E\[X] = E\[epsilon] / (1 - sum(alpha)). The autocorrelation function follows the same Yule-Walker recursions as a Gaussian AR(p); for INAR(1), acf(k) = alpha^k. With Poisson innovations the INAR(1) stationary marginal is Poisson(innovation\_mean / (1 - alpha)). **Parameters:** | Name | Type | Description | Default | | ----------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series | *required* | | `max_length` | [int](#int) | Maximum length of each series | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data (e.g. 'D', 'h', '5min') or int | *required* | | `p` | [int](#int) | Autoregressive order (default: 1) | *required* | | `alpha` | [list](#list)\[[float](#float)] \| None | Thinning probabilities, each in \[0, 1] with sum \< 1 (default: random with sum \< 0.8) | *required* | | `innovation_type` | [str](#str) | 'poisson' or 'negative\_binomial' (default: 'poisson') | *required* | | `innovation_mean` | [float](#float) | Mean of innovations (default: 5.0) | *required* | | `innovation_dispersion` | [float](#float) | Dispersion r for the negative binomial; innovation variance is mean + mean^2 / r (default: 2.0) | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None) | *required* | #### `INARGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate a single INAR time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | ------------------------------------------------ | | [ndarray](#numpy.ndarray) | Array of non-negative integer time series values | #### `INARGenerator.get_model_info` ```python theme={null} get_model_info() ``` Return information about the INAR configuration. # Stochastic Generators Source: https://nixtlaverse.nixtla.io/synforecast/generators_stochastic.html GARCH, Ornstein-Uhlenbeck, GBM, Jump Diffusion, Poisson, Cyclic, fBm, Hawkes, Stochastic Volatility, Regime Switching, Chaotic System, Bounded Process, and Lévy Process generators ### `GARCHGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate return series from a GARCH(p, q) model. The model is `r_t = mu + eps_t` with `eps_t = sigma_t * z_t` and conditional variance ``` sigma2_t = omega + sum_i alpha_i * eps_{t-i}^2 + sum_j beta_j * sigma2_{t-j} ``` Stationarity requires `sum(alpha) + sum(beta) < 1`, giving an unconditional variance of `omega / (1 - sum(alpha) - sum(beta))`. Squared returns are positively autocorrelated (volatility clustering) while the returns themselves are serially uncorrelated. **Parameters:** | Name | Type | Description | Default | | ------------------ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer time step. | *required* | | `p` | [int](#int) | GARCH order (number of variance lags, default: 1). | *required* | | `q` | [int](#int) | ARCH order (number of squared-innovation lags, default: 1). | *required* | | `omega` | [float](#float) | Constant term in the variance equation (default: 0.1). | *required* | | `alpha` | [list](#list)\[[float](#float)] \| None | ARCH coefficients; auto-generated when None. | *required* | | `beta` | [list](#list)\[[float](#float)] \| None | GARCH coefficients; auto-generated when None. | *required* | | `mu` | [float](#float) | Mean of returns (default: 0.0). | *required* | | `initial_variance` | [float](#float) | Variance used to start the recursion (default: 1.0). A 100-step burn-in removes its influence. | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | #### `GARCHGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single GARCH time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | ### `OrnsteinUhlenbeckGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series from an Ornstein-Uhlenbeck (mean-reverting) process. The OU process is commonly used to model interest rates, volatility, and other mean-reverting phenomena: ``` dX_t = theta * (mu - X_t) * dt + sigma * dW_t ``` Simulated with the Euler-Maruyama scheme `X_t = X_{t-1} + theta * (mu - X_{t-1}) * dt + sigma * sqrt(dt) * z_t`, where `z_t` are unit-variance draws from `innovation_distribution`. This is an AR(1) process with coefficient `phi = 1 - theta * dt`, stationary mean `mu`, stationary variance `sigma^2 * dt / (1 - phi^2)` (which approaches the continuous-time `sigma^2 / (2 * theta)` as dt -> 0), and lag-1 autocorrelation `phi`. Stability requires `theta * dt < 2`. `dt` is the model time per observation and is independent of `freq`. **Parameters:** | Name | Type | Description | Default | | --------------- | --------------------------------------- | ------------------------------------------------------------------------------------------ | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer for an integer time index. | *required* | | `theta` | [float](#float) | Speed of mean reversion, must satisfy `theta * dt < 2` (default: 0.5). | *required* | | `mu` | [float](#float) | Long-term mean (default: 0.0). | *required* | | `sigma` | [float](#float) | Volatility (default: 1.0). | *required* | | `initial_value` | [float](#float) | Initial value X\_0 (default: 0.0). | *required* | | `dt` | [float](#float) | Model time step per observation (default: 1.0). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | #### `OrnsteinUhlenbeckGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single Ornstein-Uhlenbeck time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values. | ### `GeometricBrownianMotionGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series from Geometric Brownian Motion. GBM models strictly positive processes such as asset prices: ``` dS_t = mu * S_t * dt + sigma * S_t * dW_t ``` Simulated via the exact solution of the SDE, `S_t = S_{t-1} * exp((mu - sigma^2/2) * dt + sigma * sqrt(dt) * z_t)`, where `z_t` are unit-variance draws from `innovation_distribution` (exact for normal innovations). `dt` is the model time per observation and is independent of `freq`: with annualized `mu`/`sigma`, daily observations correspond to `dt=1/252`. Note that the default `dt=1.0` treats `mu` and `sigma` as per-step rates; long series with a large `mu * dt` grow explosively. **Parameters:** | Name | Type | Description | Default | | --------------- | --------------------------------------- | ------------------------------------------------------------------------------------------ | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer for an integer time index. | *required* | | `mu` | [float](#float) | Drift per unit of model time (default: 0.05). | *required* | | `sigma` | [float](#float) | Volatility per sqrt unit of model time (default: 0.2). | *required* | | `initial_value` | [float](#float) | Initial value S\_0, must be > 0 (default: 100.0). | *required* | | `dt` | [float](#float) | Model time step per observation (default: 1.0). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | #### `GeometricBrownianMotionGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single Geometric Brownian Motion time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values. | ### `JumpDiffusionGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series from a jump diffusion process (Merton model). Combines Geometric Brownian Motion with discontinuous jumps from a compound Poisson process, commonly used for asset prices with rare events: ``` dS_t = mu * S_t * dt + sigma * S_t * dW_t + S_{t-} * dJ_t ``` Each step multiplies the price by `exp((mu - sigma^2/2) * dt + sigma * sqrt(dt) * z_t + sum_k Y_k)` with `N_t ~ Poisson(lambda_jump * dt)` jumps of log-size `Y_k = jump_mean + jump_std * eps_k`. Both `z_t` and `eps_k` are unit-variance draws from `innovation_distribution` (normal by default, giving Merton's log-normal jumps). The drift is not compensated for jumps, so the expected log-return per step is `(mu - sigma^2/2) * dt + lambda_jump * dt * jump_mean`. `dt` is the model time per observation and is independent of `freq`. **Parameters:** | Name | Type | Description | Default | | --------------- | --------------------------------------- | ------------------------------------------------------------------------------------------ | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer for an integer time index. | *required* | | `mu` | [float](#float) | Drift per unit of model time (default: 0.05). | *required* | | `sigma` | [float](#float) | Diffusion volatility (default: 0.2). | *required* | | `lambda_jump` | [float](#float) | Jump intensity, expected jumps per unit of model time (default: 0.1). | *required* | | `jump_mean` | [float](#float) | Mean jump size in log-price (default: 0.0). | *required* | | `jump_std` | [float](#float) | Std of jump size in log-price (default: 0.1). | *required* | | `initial_value` | [float](#float) | Initial value S\_0, must be > 0 (default: 100.0). | *required* | | `dt` | [float](#float) | Model time step per observation (default: 1.0). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | #### `JumpDiffusionGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single jump diffusion time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values. | ### `PoissonProcessGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series based on a homogeneous Poisson process. Each observation is the event count in one time step: y\_t \~ Poisson(lambda\_rate), i.i.d., so mean and variance both equal lambda\_rate. With cumulative=True the running total N(t) = sum y\_s is returned instead (the counting process itself). lambda\_rate is expressed per time step of `freq`. **Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------------- | ---------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series | *required* | | `max_length` | [int](#int) | Maximum length of each series | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data (e.g. 'D', 'h', '5min') or int | *required* | | `lambda_rate` | [float](#float) | Expected events per time step (default: 5.0) | *required* | | `cumulative` | [bool](#bool) | Return cumulative counts (default: False) | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None) | *required* | #### `PoissonProcessGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single Poisson Process time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values (counts per time period) | ### `CyclicGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series with irregular cyclic patterns. Models business cycles and economic indicators: a linear trend plus `num_cycles` superposed sinusoids whose periods and amplitudes are drawn once per series (period \~ |N(period\_mean, period\_std)|, amplitude \~ N(amplitude\_mean, amplitude\_std)), plus additive noise drawn from the configured `innovation_distribution`. Each sinusoid's instantaneous frequency is slowly modulated (+-20% around 2\*pi/period, integrated as a cumulative phase), so cycle lengths vary within a series, unlike regular seasonal patterns. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------- | ---------------------------------------------------- | ---------- | | `base_level` | [float](#float) | Base level of the series (default: 100.0). | *required* | | `trend` | [float](#float) | Linear trend coefficient per step (default: 0.0). | *required* | | `cycle_period_mean` | [float](#float) | Mean cycle period in steps (default: 50.0). | *required* | | `cycle_period_std` | [float](#float) | Std of the per-series period draw (default: 10.0). | *required* | | `cycle_amplitude_mean` | [float](#float) | Mean cycle amplitude (default: 20.0). | *required* | | `cycle_amplitude_std` | [float](#float) | Std of the per-series amplitude draw (default: 5.0). | *required* | | `num_cycles` | [int](#int) | Number of superposed cycle components (default: 3). | *required* | | `noise_std` | [float](#float) | Standard deviation of additive noise (default: 1.0). | *required* | #### `CyclicGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single time series with irregular cycles. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | ### `FractionalBrownianMotionGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series using Fractional Brownian Motion (fBm). fBm extends standard Brownian motion with a Hurst exponent H that controls long-range dependence: * H = 0.5: standard Brownian motion (independent increments) * H > 0.5: persistent/trending (positively correlated increments) * H \< 0.5: anti-persistent/mean-reverting (negatively correlated) The increments (fractional Gaussian noise, fGn) are stationary with autocovariance `gamma(k) = (sigma^2/2) * (|k+1|^{2H} - 2|k|^{2H} + |k-1|^{2H})`, and the path satisfies `Var(B_H(t)) = sigma^2 * t^{2H}`.
Warning The 'cholesky' and 'hosking' methods have O(n^2) memory (an n x n covariance matrix); prefer the default 'fft' (Davies-Harte) method for long series.
**Parameters:** | Name | Type | Description | Default | | ------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------- | ---------- | | `hurst` | [float](#float) | Hurst exponent H in (0, 1) (default: 0.5). | *required* | | `sigma` | [float](#float) | Volatility/scale of the increments (default: 1.0). | *required* | | `method` | [str](#str) | Generation method: 'fft' (O(n log n), default), 'cholesky' or 'hosking' (both exact, O(n^2) memory). | *required* | | `return_increments` | [bool](#bool) | Return fGn increments instead of the cumulative fBm path (default: False). | *required* | | `initial_value` | [float](#float) | Starting value of the fBm path; ignored when return\_increments=True (default: 0.0). | *required* |
Example > > > gen = FractionalBrownianMotionGenerator( > > > ... min\_length=100, > > > ... max\_length=200, > > > ... freq="D", > > > ... hurst=0.8, # H > 0.5: trending > > > ... seed=42, > > > ... ) > > > df = gen.generate(n\_series=10)
#### `FractionalBrownianMotionGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate a single fBm series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------------------------------------------- | | [ndarray](#numpy.ndarray) | fBm path values (or fGn increments if return\_increments=True). | #### `FractionalBrownianMotionGenerator.get_model_info` ```python theme={null} get_model_info() ``` Return model parameters and qualitative behavior. #### `FractionalBrownianMotionGenerator.estimate_hurst` ```python theme={null} estimate_hurst(series, method='rs') ``` Estimate the Hurst exponent from an increment (fGn) series. **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------------------- | -------------------------------------------------------- | ----------------- | | `series` | [ndarray](#numpy.ndarray) | Time series of increments. | *required* | | `method` | [str](#str) | 'rs' (rescaled range) or 'var' (variance of aggregates). | 'rs' | **Returns:** | Type | Description | | ---------------------------- | --------------------------------------------------- | | [float](#float) | Estimated Hurst exponent, clipped to \[0.01, 0.99]. | ### `HawkesProcessGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series using Hawkes (self-exciting) point processes. Hawkes processes model events where past occurrences increase the probability of future events. The conditional intensity at time t is: ``` lambda(t) = mu + sum_{t_i <= t} g(t - t_i) ``` with baseline intensity mu and excitation kernel g. Supported kernels: ``` - exponential: g(t) = alpha * exp(-beta * t), branching ratio n = alpha / beta - power_law: g(t) = alpha / (1 + beta * t)^p with p > 1, branching ratio n = alpha / (beta * (p - 1)) ``` Stability requires n \< 1; the long-run event rate is then mu / (1 - n) events per time step, and each event spawns on average 1 / (1 - n) events (itself included) in its cluster. Time is measured in steps of `freq`, so mu and beta are per-step quantities. Applications: order arrivals in high-frequency trading, earthquake aftershock sequences, viral cascades, clustered fraud events. **Parameters:** | Name | Type | Description | Default | | ---------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series | *required* | | `max_length` | [int](#int) | Maximum length of each series | *required* | | `freq` | [str](#str) \| [int](#int) | Frequency of the data (e.g. 'D', 'h', '5min') or int | *required* | | `baseline_intensity` | [float](#float) | Background event rate mu per time step (default: 1.0) | *required* | | `excitation_amplitude` | [float](#float) | Jump in intensity per event alpha (default: 0.5) | *required* | | `decay_rate` | [float](#float) | Rate of intensity decay beta (default: 1.0) | *required* | | `kernel` | [str](#str) | Excitation kernel, 'exponential' or 'power\_law' (default: 'exponential') | *required* | | `power_law_exponent` | [float](#float) | Exponent p for the power-law kernel, must be > 1 (default: 1.5) | *required* | | `output_type` | [str](#str) | 'counts' (events per bin), 'intensity' (lambda at bin midpoints), or 'events' (0/1 indicator per bin) (default: 'counts') | *required* | | `max_events` | [int](#int) | Maximum events to simulate per series (default: 10000) | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None) | *required* |
Example > > > gen = HawkesProcessGenerator( > > > ... min\_length=100, > > > ... max\_length=200, > > > ... freq="h", > > > ... baseline\_intensity=0.5, > > > ... excitation\_amplitude=0.3, > > > ... decay\_rate=2.0, > > > ... seed=42, > > > ... ) > > > df = gen.generate(n\_series=10)
#### `HawkesProcessGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single Hawkes process series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | ------------------------------------------------------- | | [ndarray](#numpy.ndarray) | Array of values (counts, intensity, or event indicator) | #### `HawkesProcessGenerator.simulate_with_events` ```python theme={null} simulate_with_events(time_horizon) ``` Simulate and return both event times and intensity at those times. **Parameters:** | Name | Type | Description | Default | | -------------- | ---------------------------- | ---------------------- | ---------- | | `time_horizon` | [float](#float) | Total time to simulate | *required* | **Returns:** | Type | Description | | ----------------------------------------------------------------------------------- | ------------------------------------- | | [tuple](#tuple)\[[ndarray](#numpy.ndarray), [ndarray](#numpy.ndarray)] | (event\_times, intensity\_at\_events) | #### `HawkesProcessGenerator.get_model_info` ```python theme={null} get_model_info() ``` Get information about the Hawkes process model. **Returns:** | Type | Description | | -------------------------- | ------------------------------------ | | [dict](#dict) | Model parameters and characteristics | #### `HawkesProcessGenerator.estimate_parameters` ```python theme={null} estimate_parameters(event_times, _method='mle') ``` Estimate Hawkes process parameters from observed event times. Heuristic moment-based estimation: the coefficient of variation of inter-arrival times proxies the branching ratio (CV = 1 for a Poisson process, larger under clustering), and the mean rate identifies mu via rate = mu / (1 - n). **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------------------- | -------------------------------------------------- | ------------------ | | `event_times` | [ndarray](#numpy.ndarray) | Array of observed event times | *required* | | `_method` | [str](#str) | Estimation method (currently only 'mle' supported) | 'mle' | **Returns:** | Type | Description | | -------------------------- | -------------------- | | [dict](#dict) | Estimated parameters | ### `StochasticVolatilityGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series where volatility itself follows a stochastic process. Heston model (variance is mean-reverting square-root/CIR): ``` dS = mu * S dt + sqrt(V) * S dW1 dV = kappa * (theta - V) dt + sigma_v * sqrt(V) dW2 Corr(dW1, dW2) = rho ``` SABR model (for rates/FX): ``` dF = sigma * F^beta dW1 dsigma = alpha * sigma dW2 Corr(dW1, dW2) = rho ``` Both are simulated with Euler-Maruyama; the Heston variance uses a truncation scheme (floored at a small positive value) so the discretized variance stays positive even when the Feller condition `2 * kappa * theta > sigma_v^2` is violated. Negative `rho` produces the leverage effect (volatility rises when prices fall). **Parameters:** | Name | Type | Description | Default | | -------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Pandas offset alias (e.g. 'D', 'h', '5min') or an integer time step. | *required* | | `model` | [str](#str) | 'heston' or 'sabr' (default: 'heston'). | *required* | | `initial_price` | [float](#float) | Starting price S0 (default: 100.0). | *required* | | `initial_vol` | [float](#float) | Starting variance V0; SABR uses sqrt(initial\_vol) as its starting volatility sigma0 (default: 0.04). | *required* | | `drift` | [float](#float) | Price drift mu (default: 0.05). | *required* | | `mean_vol` | [float](#float) | Long-run variance theta (Heston only, default: 0.04). | *required* | | `vol_mean_reversion` | [float](#float) | Variance mean-reversion speed kappa (Heston only, default: 2.0). | *required* | | `vol_of_vol` | [float](#float) | Volatility of volatility sigma\_v (Heston) or alpha (SABR) (default: 0.3). | *required* | | `correlation` | [float](#float) | Price-volatility correlation rho in \[-1, 1] (default: -0.7). | *required* | | `beta` | [float](#float) | CEV exponent in \[0, 1] (SABR only; 0=normal, 1=lognormal, default: 0.5). | *required* | | `dt` | [float](#float) | Time step for discretization (default: 1/252). | *required* | | `output_type` | [str](#str) | 'price', 'returns' (log returns), or 'volatility' (default: 'price'). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* |
Example > > > gen = StochasticVolatilityGenerator( > > > ... min\_length=252, > > > ... max\_length=252, > > > ... freq="D", > > > ... model="heston", > > > ... initial\_price=100.0, > > > ... correlation=-0.7, # Leverage effect > > > ... seed=42, > > > ... ) > > > df = gen.generate(n\_series=10)
#### `StochasticVolatilityGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single stochastic volatility series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | ----------------------------------------------- | | [ndarray](#numpy.ndarray) | Array of values (price, returns, or volatility) | #### `StochasticVolatilityGenerator.generate_with_volatility` ```python theme={null} generate_with_volatility(n_series=1, start_id=0) ``` Generate series and return both prices and volatility paths. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | ------------------------------------------ | -------------- | | `n_series` | [int](#int) | Number of series to generate (default: 1) | 1 | | `start_id` | [int](#int) | Starting ID for series naming (default: 0) | 0 | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | [tuple](#tuple)\[[ndarray](#numpy.ndarray), [ndarray](#numpy.ndarray), [ndarray](#numpy.ndarray)] | (prices, volatilities, series\_ids) arrays | #### `StochasticVolatilityGenerator.get_model_info` ```python theme={null} get_model_info() ``` Get information about the stochastic volatility model. **Returns:** | Type | Description | | -------------------------- | ------------------------------------ | | [dict](#dict) | Model parameters and characteristics | #### `StochasticVolatilityGenerator.implied_volatility_smile` ```python theme={null} implied_volatility_smile(strikes, maturity=1.0) ``` Approximate implied volatility smile for given strikes. Uses the Hagan SABR approximation formula (valid for the SABR model; a rough approximation for Heston). **Parameters:** | Name | Type | Description | Default | | ---------- | -------------------------------------- | ------------------------- | ---------------- | | `strikes` | [ndarray](#numpy.ndarray) | Array of strike prices | *required* | | `maturity` | [float](#float) | Time to maturity in years | 1.0 | **Returns:** | Type | Description | | -------------------------------------- | ----------------------------- | | [ndarray](#numpy.ndarray) | Array of implied volatilities | ### `RegimeSwitchingGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series with Markov regime-switching dynamics. A hidden regime s\_t follows a first-order Markov chain with transition matrix P (rows sum to 1). Conditional on the regime, values follow an AR(1) around the regime mean: ``` y_t = mu_{s_t} + phi_{s_t} * (y_{t-1} - mu_{s_t}) + sigma_{s_t} * eps_t ``` When no initial regime is given, s\_0 is drawn from the stationary distribution pi of P (pi = pi P), so long-run regime occupancy matches pi. **Parameters:** | Name | Type | Description | Default | | | | ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------- | ------------------ | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | | | `freq` | [str](#str) \| [int](#int) | Pandas offset alias (e.g. 'D', 'h', '5min') or an integer time step. | *required* | | | | `n_regimes` | [int](#int) | Number of regimes/states (default: 2). | *required* | | | | `regime_means` | [list](#list)\[[float](#float)] \| None | Mean per regime (default: spread across levels). | *required* | | | | `regime_variances` | [list](#list)\[[float](#float)] \| None | Variance per regime (default: linspace(0.5, 2.0)). | *required* | | | | `regime_ar_coeffs` | [list](#list)\[[float](#float)] \| None | AR(1) coefficient per regime, each | phi | \< 1 (default: 0). | *required* | | `transition_matrix` | [list](#list)\[[list](#list)\[[float](#float)]] \| None | Row-stochastic regime transition matrix (default: 0.95 self-transition probability). | *required* | | | | `initial_regime` | [int](#int) \| None | Starting regime, 0-indexed (default: drawn from the stationary distribution). | *required* | | | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | | |
Example > > > gen = RegimeSwitchingGenerator( > > > ... min\_length=100, > > > ... max\_length=200, > > > ... freq="D", > > > ... n\_regimes=2, > > > ... regime\_means=\[0.0, 5.0], > > > ... regime\_variances=\[1.0, 4.0], > > > ... transition\_matrix=\[\[0.95, 0.05], \[0.10, 0.90]], > > > ... seed=42, > > > ... ) > > > df = gen.generate(n\_series=10)
#### `RegimeSwitchingGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate values for a single regime-switching time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | --------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values | #### `RegimeSwitchingGenerator.generate_with_regimes` ```python theme={null} generate_with_regimes(n_series=1, start_id=0) ``` Generate series and return both values and regime labels. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | ------------------------------------------ | -------------- | | `n_series` | [int](#int) | Number of series to generate (default: 1) | 1 | | `start_id` | [int](#int) | Starting ID for series naming (default: 0) | 0 | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------- | ------------------------------------- | | [tuple](#tuple)\[[ndarray](#numpy.ndarray), [ndarray](#numpy.ndarray), [ndarray](#numpy.ndarray)] | (values, regimes, series\_ids) arrays | #### `RegimeSwitchingGenerator.get_model_info` ```python theme={null} get_model_info() ``` Get information about the regime-switching model. **Returns:** | Type | Description | | -------------------------- | ------------------------------------ | | [dict](#dict) | Model parameters and characteristics | ### `ChaoticSystemGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series from deterministic chaotic dynamical systems. Produces series that look stochastic but are fully deterministic given the initial condition; randomness enters only through a seeded perturbation of the initial condition and optional observation noise.
Systems * lorenz: Lorenz attractor `x' = sigma(y-x), y' = x(rho-z) - y, z' = xy - beta*z`, integrated with RK4 and sampled every 1/dt steps (one time unit per observation); the x-component is returned. * logistic: Logistic map `x_{n+1} = r * x_n * (1 - x_n)` (chaotic for r \~ 3.57..4; for r=4 the invariant density is Beta(1/2, 1/2)). * mackey\_glass: Mackey-Glass delay differential equation `x' = beta * x(t-tau) / (1 + x(t-tau)^n) - gamma * x`, Euler-integrated with unit step (chaotic for tau >= 17 at the default parameters).
**Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------- | ---------- | | `system` | [str](#str) | 'lorenz', 'logistic' or 'mackey\_glass' (default: 'lorenz'). | *required* | | `sigma` | [float](#float) | Lorenz sigma (default: 10.0). | *required* | | `rho` | [float](#float) | Lorenz rho (default: 28.0). | *required* | | `beta_param` | [float](#float) | Lorenz beta, alias 'lorenz\_beta' (default: 2.6667). | *required* | | `dt` | [float](#float) | Lorenz RK4 integration step (default: 0.01). | *required* | | `logistic_r` | [float](#float) | Logistic map parameter r (default: 3.9). | *required* | | `mg_beta` | [float](#float) | Mackey-Glass beta (default: 0.2). | *required* | | `mg_gamma` | [float](#float) | Mackey-Glass gamma (default: 0.1). | *required* | | `mg_n` | [float](#float) | Mackey-Glass exponent n (default: 10.0). | *required* | | `mg_tau` | [int](#int) | Mackey-Glass delay tau (default: 17). | *required* | | `observation_noise` | [float](#float) | Std of additive Gaussian observation noise (default: 0.0). | *required* | | `initial_perturbation` | [float](#float) | Scale of the random initial-condition perturbation; 0 makes the output seed-independent (default: 0.01). | *required* | #### `ChaoticSystemGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate a single chaotic time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values. | #### `ChaoticSystemGenerator.get_model_info` ```python theme={null} get_model_info() ``` Return information about the chaotic system configuration. ### `BoundedProcessGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series constrained to a bounded interval. Values are simulated on the unit interval and affinely mapped to \[lower, upper] (default \[0, 1]). Useful for proportions, market shares, probabilities, and other bounded quantities.
Models * beta\_ar: Beta AR(1) via conditional mean parameterization. mu\_t = omega + phi \* x\_\{t-1}, x\_t \~ Beta(mu\_t \* kappa, (1 - mu\_t) \* kappa), so E\[x\_t | x\_\{t-1}] = mu\_t and the stationary mean is omega / (1 - phi). * logit\_normal: AR(1) on the logit scale, sigmoid-transformed back: z\_t = phi \* z\_\{t-1} + sigma \* eps\_t, x\_t = sigmoid(z\_t).
**Parameters:** | Name | Type | Description | Default | | --------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Pandas offset alias (e.g. 'D', 'h', '5min') or an integer time step. | *required* | | `model` | [str](#str) | 'beta\_ar' or 'logit\_normal' (default: 'beta\_ar'). | *required* | | `phi` | [float](#float) | AR coefficient in \[-1, 1] (default: 0.8). | *required* | | `omega` | [float](#float) | Intercept of the beta\_ar conditional mean (default: 0.1). Must satisfy 0 \< omega + phi \* x \< 1 for x in (0, 1). | *required* | | `kappa` | [float](#float) | Beta precision; larger = less noise (default: 20.0). | *required* | | `sigma` | [float](#float) | Logit-scale innovation std (logit\_normal only, default: 0.3). | *required* | | `initial_value` | [float](#float) | Starting value on the unit scale, in (0, 1) (default: 0.5). | *required* | | `lower` | [float](#float) | Lower bound of the output interval (default: 0.0). | *required* | | `upper` | [float](#float) | Upper bound of the output interval (default: 1.0). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | #### `BoundedProcessGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate a single bounded time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------ | ---------- | | `length` | [int](#int) | The length of the series to generate | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values in \[lower, upper] | #### `BoundedProcessGenerator.get_model_info` ```python theme={null} get_model_info() ``` Return information about the bounded process configuration. ### `LevyProcessGenerator` Bases: [BaseGenerator](#synforecast.base.BaseGenerator) Generate time series with alpha-stable (Levy) increments. Each observation step adds an independent increment `scale * X + location` where `X ~ S(alpha, beta_skew; 1)` is a standard alpha-stable random variable in the S1 parameterization (matching `scipy.stats.levy_stable`), sampled with the Chambers-Mallows-Stuck algorithm. For `alpha < 2` the increments have infinite variance, producing extreme jumps far beyond Gaussian or t-distributed innovations. There is no separate `dt`: `scale` is the per-step scale (a step of duration `dt` in model time corresponds to `scale ~ dt**(1/alpha)` by self-similarity).
Special cases * `alpha=2`: Gaussian with standard deviation `scale * sqrt(2)` * `alpha=1, beta_skew=0`: Cauchy * `alpha=0.5, beta_skew=1`: Levy distribution
**Parameters:** | Name | Type | Description | Default | | --------------- | --------------------------------------- | ------------------------------------------------------------------------------------------ | ---------- | | `min_length` | [int](#int) | Minimum length of each series. | *required* | | `max_length` | [int](#int) | Maximum length of each series. | *required* | | `freq` | [str](#str) \| [int](#int) | Pandas offset alias (e.g. 'D', 'h', '5min', 'MS') or an integer for an integer time index. | *required* | | `alpha` | [float](#float) | Stability index in (0, 2] (default: 1.5). | *required* | | `beta_skew` | [float](#float) | Skewness parameter in \[-1, 1] (default: 0.0). | *required* | | `scale` | [float](#float) | Scale of each increment (default: 1.0). | *required* | | `location` | [float](#float) | Location shift of each increment (default: 0.0). | *required* | | `cumulative` | [bool](#bool) | Return the cumulative sum (Levy flight) instead of raw increments (default: True). | *required* | | `initial_value` | [float](#float) | Starting value for cumulative mode (default: 0.0). | *required* | | `seed` | [int](#int) \| None | Random seed for reproducibility (default: None). | *required* | #### `LevyProcessGenerator.generate_single_series` ```python theme={null} generate_single_series(length) ``` Generate a single Levy process time series. **Parameters:** | Name | Type | Description | Default | | -------- | ------------------------ | ------------------------------------- | ---------- | | `length` | [int](#int) | The length of the series to generate. | *required* | **Returns:** | Type | Description | | -------------------------------------- | ---------------------------- | | [ndarray](#numpy.ndarray) | Array of time series values. | #### `LevyProcessGenerator.get_model_info` ```python theme={null} get_model_info() ``` Return information about the Levy process configuration. # Synthetic 🧬 Forecast Source: https://nixtlaverse.nixtla.io/synforecast/index.html Fast synthetic time series for testing, augmentation, and pretraining **SynForecast** generates synthetic time-series panels with 31 statistical, stochastic, multivariate, domain-specific, and pretraining generators. It follows the Nixtla long format and supports controlled changepoints, anomalies, missing data, exogenous variables, and augmentation of observed series. > **Note** > > SynForecast is in alpha. APIs and seed-identical outputs may change before > the first stable release. ## Installation Install SynForecast from PyPI: ```bash theme={null} pip install synforecast ``` Prebuilt wheels include the required Rust extension on supported platforms. Building from the source distribution requires a Rust toolchain. ## Quick start Generate a long-format panel from a balanced collection of generators: ```python theme={null} from synforecast import generate_series synthetic_df = generate_series( n_series=100, freq="D", min_length=100, max_length=100, seed=42, ) ``` The result is a pandas DataFrame with the standard Nixtla columns `[unique_id, ds, y]`. Pass `engine="polars"` to return a Polars DataFrame. Use a specific generator when you need explicit control over the data-generating process: ```python theme={null} from synforecast.generators import SeasonalGenerator generator = SeasonalGenerator( min_length=365, max_length=365, freq="D", seasonality_period=7, seasonality_amplitude=10.0, seed=42, ) synthetic_df = generator.generate(n_series=20) ``` See the [quick start](https://nixtlaverse.nixtla.io/synforecast/docs/getting-started/quickstart.html) and [generator reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md) for the complete API. ## Augment observed series `SynAugment` analyzes each input series, fits a suitable generator, and adds synthetic series with matching timestamps and statistical characteristics: ```python theme={null} from synforecast import SynAugment augmenter = SynAugment(seed=42) augmented_train_df = augmenter.augment(train_df, n_augment=2) ``` Fit augmentation parameters on the training split only; fitting on validation or test observations would leak information into model training. See the [augmentation guide](https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/augmentation.html) for generator overrides and diagnostics. ## Works with the Nixtlaverse SynForecast materializes the same `[unique_id, ds, y]` schema used throughout the Nixtlaverse. The generated or augmented DataFrame can be passed directly to forecasting libraries without a SynForecast-specific adapter. ### NeuralForecast ```python theme={null} from neuralforecast import NeuralForecast from neuralforecast.models import NHITS nf = NeuralForecast( models=[NHITS(h=14, input_size=28, max_steps=100)], freq="D", ) nf.fit(df=synthetic_df) neural_forecasts = nf.predict() ``` ### MLForecast ```python theme={null} from mlforecast import MLForecast from sklearn.linear_model import LinearRegression mlf = MLForecast( models=LinearRegression(), freq="D", lags=[1, 7, 14], ) mlf.fit(synthetic_df) ml_forecasts = mlf.predict(h=14) ``` ### StatsForecast ```python theme={null} from statsforecast import StatsForecast from statsforecast.models import AutoETS sf = StatsForecast( models=[AutoETS(season_length=7)], freq="D", ) sf.fit(synthetic_df) statistical_forecasts = sf.predict(h=14) ``` Install the forecasting libraries you want to use separately. SynForecast does not add them to its runtime dependencies. See the [Nixtlaverse guide](https://nixtlaverse.nixtla.io/synforecast/docs/integrations/nixtlaverse.html) for materialized pretraining and augmentation workflows. ## Why SynForecast? Synthetic time series are useful when real observations are scarce, sensitive, expensive, or do not cover the conditions a system must handle. SynForecast is designed for: * Testing forecasting and anomaly-detection pipelines against known behavior * Augmenting small training panels without altering validation data * Pretraining global forecasting models on diverse temporal processes * Stress-testing changepoints, missingness, anomalies, and regime changes * Reproducible simulation with explicit, validated generator configurations Synthetic data reflects the assumptions of its generators. Validate those assumptions and downstream performance for your use case. SynForecast is not an anonymization or differential-privacy tool: `SynAugment` is fitted to observed data and its output can resemble that data. ## Evidence and benchmarks SynForecast's native Rust batch path is designed for high-throughput data generation. Reproducible scripts and committed result summaries are available in [`benchmarks/`](https://github.com/Nixtla/synforecast/tree/main/benchmarks); performance depends on generator, series shape, thread count, and hardware. Synthetic data does not improve every model or dataset. The [when synthetic data helps](https://nixtlaverse.nixtla.io/synforecast/docs/capabilities/when_synthetic_helps.html) guide reports positive, neutral, and negative results so that augmentation and pretraining choices can be evaluated against observed-only baselines. ## Features * 31 generators across statistical, stochastic, multivariate, domain-specific, and pretraining categories * Long-format output following Nixtla conventions * pandas, Polars, cuDF, Modin, and PyArrow output through Narwhals * Changepoint, anomaly, missingness, and exogenous-variable injection * Dataset composition with `SynSet` and augmentation with `SynAugment` * Seed-deterministic parallel generation * Native generation through Rust and PyO3 The pandas and Polars engines are covered by the full test suite. cuDF, Modin, and PyArrow support uses Narwhals and is smoke-tested when those optional libraries are installed; install the selected dataframe library separately. ## Documentation * [Getting started](https://nixtlaverse.nixtla.io/synforecast/docs/getting-started/quickstart.html) * [Tutorials and capabilities](https://nixtlaverse.nixtla.io/synforecast/) * [Generator reference](https://github.com/Nixtla/synforecast/blob/main/GENERATORS.md) * [Contributing](https://github.com/Nixtla/synforecast/blob/main/CONTRIBUTING.md) * [Roadmap](https://github.com/Nixtla/synforecast/blob/main/ROADMAP.md) * [Changelog](https://github.com/Nixtla/synforecast/blob/main/CHANGELOG.md) * [Support](https://github.com/Nixtla/synforecast/blob/main/SUPPORT.md) * [Security policy](https://github.com/Nixtla/synforecast/security/policy) * [Citation](https://github.com/Nixtla/synforecast/blob/main/CITATION.cff) ## AI disclaimer Parts of this project were developed with assistance from generative AI tools. All AI-assisted code and documentation are reviewed, tested, and maintained by human contributors, who remain responsible for correctness, security, licensing, and design. ## License SynForecast is licensed under the [Apache License 2.0](https://github.com/Nixtla/synforecast/blob/main/LICENSE). # Data Source: https://nixtlaverse.nixtla.io/utilsforecast/data.html Utilies for generating time series datasets ### `generate_series` ```python theme={null} generate_series(n_series, freq='D', min_length=50, max_length=500, n_static_features=0, equal_ends=False, with_trend=False, static_as_categorical=True, n_models=0, level=None, engine='pandas', seed=0) ``` Generate Synthetic Panel Series. **Parameters:** | Name | Type | Description | Default | | ----------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | | `n_series` | [int](#int) | Number of series for synthetic panel. | *required* | | `freq` | [str](#str) | Frequency of the data (pandas alias). Seasonalities are implemented for hourly, daily and monthly. Defaults to 'D'. | 'D' | | `min_length` | [int](#int) | Minimum length of synthetic panel's series. Defaults to 50. | 50 | | `max_length` | [int](#int) | Maximum length of synthetic panel's series. Defaults to 500. | 500 | | `n_static_features` | [int](#int) | Number of static exogenous variables for synthetic panel's series. Defaults to 0. | 0 | | `equal_ends` | [bool](#bool) | Series should end in the same timestamp. Defaults to False. | False | | `with_trend` | [bool](#bool) | Series should have a (positive) trend. Defaults to False. | False | | `static_as_categorical` | [bool](#bool) | Static features should have a categorical data type. Defaults to True. | True | | `n_models` | [int](#int) | Number of models predictions to simulate. Defaults to 0. | 0 | | `level` | list of float | Confidence level for intervals to simulate for each model. Defaults to None. | None | | `engine` | [str](#str) | Output Dataframe type. Defaults to 'pandas'. | 'pandas' | | `seed` | [int](#int) | Random seed used for generating the data. Defaults to 0. | 0 | **Returns:** | Type | Description | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | [DataFrame](#utilsforecast.compat.DataFrame) | pandas or polars DataFrame: Synthetic panel with columns \[`unique_id`, `ds`, `y`] and exogenous features. | # Multi-Objective Model Selection with Pareto Frontier Source: https://nixtlaverse.nixtla.io/utilsforecast/docs/tutorials/multi_objective_model_selection.html > Learn how to select the best forecasting models when you care about > more than one metric — without manually ranking and comparing every > combination. ## What you’ll learn * Why single-metric model selection can be misleading * What Pareto dominance means and when a model is “dominated” * How to use `evaluate()` correctly as input to `ParetoFrontier` * How to visualize the 2D Pareto frontier across any two metrics * How to handle cross-validation output for multi-objective selection ## The problem: picking one winner across multiple metrics After training several forecasting models, a common question is: **which one should I deploy?** If you optimize for a single metric — say MAE — the answer is straightforward: pick the lowest MAE. But real-world requirements rarely reduce to a single number. You might care about: * **Accuracy** (MAE, RMSE): how close are point forecasts to actuals? * **Relative error** (MAPE, sMAPE): how large is the error relative to the scale of the series? * **Bias** (bias, CFE): does the model systematically over- or under-forecast? When metrics disagree — Model A has the best MAE, Model B has the best MAPE — a simple ranking breaks down. **Pareto analysis** offers a principled solution: instead of collapsing everything to a single score, identify which models are not *dominated*. A model is dominated when another model is at least as good on every metric and strictly better on at least one. Non-dominated models form the **Pareto frontier** — the set of trade-off-optimal choices. ## Install libraries ```python theme={null} %%capture pip install utilsforecast statsforecast -U ``` ```python theme={null} import warnings warnings.filterwarnings('ignore') import pandas as pd from statsforecast import StatsForecast from statsforecast.models import AutoARIMA, MSTL, SeasonalNaive from utilsforecast.data import generate_series from utilsforecast.losses import mae, mape, rmse, smape from utilsforecast.evaluation import evaluate from utilsforecast.model_selection import ParetoFrontier ``` ## Generate synthetic time series We use `generate_series` to create a panel of daily time series with weekly seasonality. Each series contains between 100 and 150 observations, giving models enough history for a meaningful fit. ```python theme={null} series = generate_series( n_series=8, freq='D', min_length=100, max_length=150, seed=42, ) # StatsForecast requires string or integer unique_id, not Categorical series['unique_id'] = series['unique_id'].astype(str) series.head() ``` | | unique\_id | ds | y | | - | ---------- | ---------- | -------- | | 0 | 0 | 2000-01-01 | 0.049987 | | 1 | 0 | 2000-01-02 | 1.229624 | | 2 | 0 | 2000-01-03 | 2.166854 | | 3 | 0 | 2000-01-04 | 3.071433 | | 4 | 0 | 2000-01-05 | 4.325444 | We hold out the last 14 days of each series as the evaluation window and use the rest for training. ```python theme={null} HORIZON = 14 SEASON = 7 test_mask = series.groupby('unique_id').cumcount(ascending=False) < HORIZON train = series[~test_mask].reset_index(drop=True) test = series[test_mask].reset_index(drop=True) print(f'Train: {len(train)} rows | Test: {len(test)} rows') ``` ```text theme={null} Train: 893 rows | Test: 112 rows ``` ## Fit models and generate forecasts We compare three models that cover a range of complexity: * **SeasonalNaive** — repeats the last observed season. Fast, transparent, surprisingly hard to beat. * **AutoARIMA** — fits a SARIMA model selected automatically by AIC. More flexible but slower. * **MSTL** — decomposes the series into trend and seasonal components using STL, then forecasts each part separately. Good at capturing multiple seasonal patterns. ```python theme={null} sf = StatsForecast( models=[ SeasonalNaive(season_length=SEASON), AutoARIMA(season_length=SEASON), MSTL(season_length=SEASON), ], freq='D', n_jobs=1, ) sf.fit(train) preds = sf.predict(h=HORIZON) preds.head() ``` | | unique\_id | ds | SeasonalNaive | AutoARIMA | MSTL | | - | ---------- | ---------- | ------------- | --------- | -------- | | 0 | 0 | 2000-05-04 | 5.453414 | 5.296218 | 5.283464 | | 1 | 0 | 2000-05-05 | 6.136066 | 6.198051 | 6.193499 | | 2 | 0 | 2000-05-06 | 0.323845 | 0.277377 | 0.280454 | | 3 | 0 | 2000-05-07 | 1.000260 | 1.234950 | 1.244538 | | 4 | 0 | 2000-05-08 | 2.176284 | 2.252924 | 2.278188 | Merge predictions with the held-out actuals to get a single DataFrame ready for `evaluate()`. ```python theme={null} eval_df = test.merge(preds, on=['unique_id', 'ds'], how='left') eval_df.head() ``` | | unique\_id | ds | y | SeasonalNaive | AutoARIMA | MSTL | | - | ---------- | ---------- | -------- | ------------- | --------- | -------- | | 0 | 0 | 2000-05-04 | 5.267045 | 5.453414 | 5.296218 | 5.283464 | | 1 | 0 | 2000-05-05 | 6.242415 | 6.136066 | 6.198051 | 6.193499 | | 2 | 0 | 2000-05-06 | 0.346218 | 0.323845 | 0.277377 | 0.280454 | | 3 | 0 | 2000-05-07 | 1.134706 | 1.000260 | 1.234950 | 1.244538 | | 4 | 0 | 2000-05-08 | 2.122063 | 2.176284 | 2.252924 | 2.278188 | ## Evaluate models across multiple metrics `evaluate()` computes any combination of loss functions from `utilsforecast.losses` and returns a tidy DataFrame with one row per `(unique_id, metric)` and one column per model. For Pareto analysis we need **one scalar per metric per model** — a single number that summarises performance across all series. The `agg_fn='mean'` argument collapses the per-series rows into a single mean, giving a `(n_metrics, n_models)` table. ```python theme={null} scores = evaluate( df=eval_df, metrics=[mae, rmse, mape, smape], agg_fn='mean', ) scores ``` | | metric | SeasonalNaive | AutoARIMA | MSTL | | - | ------ | ------------- | --------- | -------- | | 0 | mae | 0.162020 | 0.120070 | 0.119955 | | 1 | rmse | 0.196461 | 0.145129 | 0.144143 | | 2 | mape | 0.354416 | 0.359234 | 0.340032 | | 3 | smape | 0.087060 | 0.065840 | 0.064982 | At a glance, no single model wins on every metric. MSTL tends to have lower absolute errors while SeasonalNaive can be competitive on relative metrics for series with strong weekly patterns. This is exactly the situation where Pareto analysis adds value. ## Find the Pareto frontier `ParetoFrontier.find_non_dominated()` takes the aggregated scores table and returns only the columns corresponding to non-dominated models — dropping any model for which another model is at least as good on every metric and strictly better on at least one. ```python theme={null} pareto = ParetoFrontier.find_non_dominated(scores) pareto ``` | | metric | MSTL | | - | ------ | -------- | | 0 | mae | 0.119955 | | 1 | rmse | 0.144143 | | 2 | mape | 0.340032 | | 3 | smape | 0.064982 | The models that survive are the **Pareto-optimal** set. Dropping the rest is safe: for every eliminated model, there is at least one surviving model that dominates it across every metric simultaneously. ### Focusing on a subset of metrics You can restrict the comparison to only the metrics that matter for your use case by passing a `metrics` list. ```python theme={null} # Only consider MAE and RMSE for dominance — ignore MAPE and sMAPE pareto_subset = ParetoFrontier.find_non_dominated(scores, metrics=['mae', 'rmse']) pareto_subset ``` | | metric | MSTL | | - | ------ | -------- | | 0 | mae | 0.119955 | | 1 | rmse | 0.144143 | ### Maximization metrics By default all metrics are minimized (lower is better). If a metric should be maximized — for example, a custom R² score — pass its name in `maximization`. ```python theme={null} # Hypothetical: minimize MAE but maximize some score column # ParetoFrontier.find_non_dominated(scores, maximization=['score']) # With the current metrics, this is equivalent to the default: pareto_min = ParetoFrontier.find_non_dominated(scores, metrics=['mae', 'rmse', 'mape', 'smape']) pareto_min ``` | | metric | MSTL | | - | ------ | -------- | | 0 | mae | 0.119955 | | 1 | rmse | 0.144143 | | 2 | mape | 0.340032 | | 3 | smape | 0.064982 | ## Visualize the 2D Pareto frontier When comparing two metrics, `ParetoFrontier.plot_pareto_2d()` renders a scatter plot where dominated models appear in grey and Pareto-optimal models appear in red, connected by a dashed frontier line. ```python theme={null} ParetoFrontier.plot_pareto_2d( scores, metric_x='mae', metric_y='mape', title='MAE vs MAPE — Pareto Frontier', ) ``` The plot accepts `maximize_x` and `maximize_y` flags for metrics where larger is better, and `show_dominated=False` to declutter the chart when many models are present. ## Cross-validation: multi-window model selection A single held-out window can be noisy. StatsForecast’s `cross_validation()` produces estimates across multiple windows, giving a more robust picture of model performance. The cross-validation output has a `cutoff` column — `evaluate()` keeps it, so `agg_fn='mean'` aggregates across series *within each cutoff*, not across all windows at once. To collapse everything into a single row per metric for Pareto analysis, apply a second groupby. ```python theme={null} cv = sf.cross_validation(df=series, h=HORIZON, n_windows=3) cv['unique_id'] = cv['unique_id'].astype(str) cv.head() ``` | | unique\_id | ds | cutoff | y | SeasonalNaive | AutoARIMA | MSTL | | - | ---------- | ---------- | ---------- | -------- | ------------- | --------- | -------- | | 0 | 0 | 2000-05-02 | 2000-05-01 | 3.152391 | 3.061044 | 3.200568 | 3.200882 | | 1 | 0 | 2000-05-03 | 2000-05-01 | 4.082328 | 4.178149 | 4.194904 | 4.197371 | | 2 | 0 | 2000-05-04 | 2000-05-01 | 5.267045 | 5.453414 | 5.294926 | 5.284707 | | 3 | 0 | 2000-05-05 | 2000-05-01 | 6.242415 | 6.136066 | 6.198686 | 6.194711 | | 4 | 0 | 2000-05-06 | 2000-05-01 | 0.346218 | 0.323845 | 0.277010 | 0.282018 | ```python theme={null} MODELS = [c for c in cv.columns if c not in {'unique_id', 'ds', 'cutoff', 'y'}] # Step 1: compute per-(series, cutoff) scores cv_scores = evaluate( df=cv, metrics=[mae, rmse, mape, smape], ) # Step 2: average across both series and cutoffs cv_scores_agg = cv_scores.groupby('metric', sort=False)[MODELS].mean().reset_index() cv_scores_agg ``` | | metric | SeasonalNaive | AutoARIMA | MSTL | | - | ------ | ------------- | --------- | -------- | | 0 | mae | 0.163304 | 0.119660 | 0.119076 | | 1 | rmse | 0.196949 | 0.144753 | 0.143648 | | 2 | mape | 0.349638 | 0.326070 | 0.317805 | | 3 | smape | 0.086262 | 0.064798 | 0.063181 | ```python theme={null} pareto_cv = ParetoFrontier.find_non_dominated(cv_scores_agg) pareto_cv ``` | | metric | MSTL | | - | ------ | -------- | | 0 | mae | 0.119076 | | 1 | rmse | 0.143648 | | 2 | mape | 0.317805 | | 3 | smape | 0.063181 | ```python theme={null} ParetoFrontier.plot_pareto_2d( cv_scores_agg, metric_x='mae', metric_y='mape', title='MAE vs MAPE — Cross-Validated Pareto Frontier', ) ``` ## Custom column names If your pipeline uses column names different from the defaults (`unique_id`, `cutoff`), pass `id_col` and `cutoff_col` to both `evaluate()` and `find_non_dominated()` so the Pareto analysis correctly identifies which columns are model predictions. ```python theme={null} # Rename to simulate a custom pipeline eval_df_custom = eval_df.rename(columns={'unique_id': 'series_id'}) scores_custom = evaluate( df=eval_df_custom, metrics=[mae, rmse], id_col='series_id', agg_fn='mean', ) # Pass the same id_col so find_non_dominated() excludes it from model columns ParetoFrontier.find_non_dominated(scores_custom, id_col='series_id') ``` | | metric | MSTL | | - | ------ | -------- | | 0 | mae | 0.119955 | | 1 | rmse | 0.144143 | ## Key takeaways * **Single-metric selection discards information.** When metrics disagree, there is no universally correct answer — only trade-offs worth making explicit. * **Pareto dominance is a lossless filter.** Every eliminated model is objectively outperformed; no information about the surviving frontier is lost. * **Always aggregate before calling `find_non_dominated()`.** Pass `agg_fn='mean'` to `evaluate()` so the input has exactly one row per metric. For cross-validation output, apply a second groupby over the `metric` column to collapse across cutoffs as well. * **`plot_pareto_2d()` makes the trade-off tangible.** Pick any two metrics on the axes to see which models sit on the frontier and which ones are dominated. * **Custom column names are supported.** Pass `id_col` and `cutoff_col` consistently across `evaluate()` and `find_non_dominated()` when your data uses non-default names. # Rectify Strategy for Multi-Step Forecasts Source: https://nixtlaverse.nixtla.io/utilsforecast/docs/tutorials/rectify_strategy.html > Correct horizon-dependent bias in recursive multi-step forecasts by > training a small residual model on top of an existing forecaster. ## What you’ll learn * Why recursive multi-step forecasts develop bias as the horizon grows * How the Rectify strategy combines recursive (low-variance) and direct (low-bias) forecasting * How to compute horizon-indexed residuals from cross-validation output * How to align feature matrices with residuals for training correction models * How to apply fitted correction models to new forecasts using both `per_horizon` and `horizon_aware` modes ## The problem: bias accumulates with horizon Recursive forecasting trains a single one-step-ahead model and feeds its predictions back as inputs for longer horizons. It is fast and produces smooth multi-step trajectories, but for non-linear data-generating processes the bias compounds: each step uses a slightly biased prediction as input, which produces a more biased prediction for the next step. Direct forecasting avoids that problem by training a separate model per horizon. It is unbiased but high-variance and produces no continuity between horizons. The **Rectify** strategy by [Ben Taieb & Hyndman (2012)](https://robjhyndman.com/papers/rectify.pdf) keeps the recursive base forecast and adds a per-horizon correction: $\hat{m}_h(x_t) = \hat{z}_h(x_t) + \hat{r}_h(x_t)$ `utilsforecast` provides three building blocks for this — computing residuals, aligning features, and applying corrections. You bring the base forecaster and the correction regressor. ## Install libraries ```python theme={null} %%capture pip install utilsforecast statsforecast scikit-learn -U ``` ```python theme={null} import warnings warnings.filterwarnings('ignore') import numpy as np import pandas as pd from sklearn.linear_model import LinearRegression from statsforecast import StatsForecast from statsforecast.models import SeasonalNaive from utilsforecast.data import generate_series from utilsforecast.losses import mae from utilsforecast.evaluation import evaluate from utilsforecast.rectify import ( align_rectify_features, compute_rectify_residuals, rectify, ) ``` ## Generate synthetic time series We use `generate_series` to create a panel of 8 daily series with weekly seasonality and a positive trend. The trend is what makes the example interesting: our base forecaster will be `SeasonalNaive`, which repeats the last observed week and has no way to capture trend. Its forecasts will drift further behind the actuals as the horizon grows — a textbook setup for rectify to fix. ```python theme={null} HORIZON = 7 SEASON = 7 series = generate_series( n_series=8, freq='D', min_length=120, max_length=160, with_trend=True, seed=42, ) series['unique_id'] = series['unique_id'].astype(str) test_mask = series.groupby('unique_id').cumcount(ascending=False) < HORIZON train = series[~test_mask].reset_index(drop=True) test = series[test_mask].reset_index(drop=True) print(f'Train: {len(train)} rows | Test: {len(test)} rows') ``` ```text theme={null} Train: 1089 rows | Test: 56 rows ``` ## Generate cross-validation forecasts We need cross-validation output (forecasts plus actuals across multiple cutoffs) to train the correction models. `StatsForecast.cross_validation()` produces exactly that, with a `cutoff` column distinguishing each fold. ```python theme={null} sf = StatsForecast( models=[SeasonalNaive(season_length=SEASON)], freq='D', n_jobs=1, ) cv_df = sf.cross_validation(df=train, h=HORIZON, n_windows=4) cv_df['unique_id'] = cv_df['unique_id'].astype(str) cv_df = cv_df.sort_values(['unique_id', 'cutoff', 'ds']).reset_index(drop=True) cv_df.head() ``` | | unique\_id | ds | cutoff | y | SeasonalNaive | | - | ---------- | ---------- | ---------- | ---------- | ------------- | | 0 | 0 | 2000-05-21 | 2000-05-20 | 119.270219 | 113.641010 | | 1 | 0 | 2000-05-22 | 2000-05-20 | 120.881632 | 115.122690 | | 2 | 0 | 2000-05-23 | 2000-05-20 | 122.832229 | 117.082437 | | 3 | 0 | 2000-05-24 | 2000-05-20 | 124.984052 | 118.821265 | | 4 | 0 | 2000-05-25 | 2000-05-20 | 126.486714 | 120.650260 | Each row is a (series, cutoff, ds) triple with the actual `y` and the SeasonalNaive forecast. With 4 cutoffs and a horizon of 7, every series contributes 28 training rows for the correction model. ## Step 1: compute per-horizon residuals `compute_rectify_residuals` joins actuals and forecasts on `(id_col, time_col, cutoff_col)` and computes `actual - forecast` per row. It also adds a 1-indexed `horizon` column based on the row position within each `(id, cutoff)` group. ```python theme={null} residuals_df = compute_rectify_residuals( df=cv_df, forecasts_df=cv_df, models=['SeasonalNaive'], cutoff_col='cutoff', ) residuals_df.head(10) ``` | | unique\_id | ds | cutoff | horizon | SeasonalNaive | | - | ---------- | ---------- | ---------- | ------- | ------------- | | 0 | 0 | 2000-05-21 | 2000-05-20 | 1 | 5.629209 | | 1 | 0 | 2000-05-22 | 2000-05-20 | 2 | 5.758942 | | 2 | 0 | 2000-05-23 | 2000-05-20 | 3 | 5.749792 | | 3 | 0 | 2000-05-24 | 2000-05-20 | 4 | 6.162787 | | 4 | 0 | 2000-05-25 | 2000-05-20 | 5 | 5.836454 | | 5 | 0 | 2000-05-26 | 2000-05-20 | 6 | 5.913097 | | 6 | 0 | 2000-05-27 | 2000-05-20 | 7 | 5.799038 | | 7 | 0 | 2000-05-22 | 2000-05-21 | 1 | 5.758942 | | 8 | 0 | 2000-05-23 | 2000-05-21 | 2 | 5.749792 | | 9 | 0 | 2000-05-24 | 2000-05-21 | 3 | 6.162787 | We pass `cv_df` for both `df` and `forecasts_df` because the cross-validation output already contains both columns. The output is sorted by `(unique_id, cutoff, ds)` — the same order we sorted `cv_df` in. Keeping the orderings consistent matters for the next step. ## Step 2: build features The correction model needs a feature matrix with the same row count and order as `residuals_df`. Anything that explains residual variation is fair game — calendar effects, lags, or exogenous variables. To keep the tutorial focused, we use two simple features: * `dow`: day of week of the prediction date * `lag_y`: the most recent observed `y` per series, used as a level proxy ```python theme={null} def build_features(df, train_history): """Build a (n_rows, 2) feature matrix row-aligned with df. df must contain unique_id, ds, and a cutoff column (or be a forecast frame where ds itself acts as the cutoff reference). """ dow = df['ds'].dt.dayofweek.to_numpy() last_y_by_uid = ( train_history.sort_values(['unique_id', 'ds']) .groupby('unique_id')['y'] .last() ) lag_y = df['unique_id'].map(last_y_by_uid).to_numpy() return np.column_stack([dow, lag_y]) train_features = build_features(cv_df, train_history=train) train_features.shape ``` ```text theme={null} (224, 2) ``` ## Step 3: align features with residuals `align_rectify_features` partitions the residuals by horizon and slices the feature matrix accordingly. In `per_horizon` mode it returns a dict mapping each horizon `h` to a `(X_h, {model: residuals_h})` tuple — one training set per horizon. ```python theme={null} aligned = align_rectify_features( residuals_df=residuals_df, features=train_features, models=['SeasonalNaive'], ) for h, (X_h, y_dict) in aligned.items(): print(f'h={h}: X shape={X_h.shape}, residuals shape={y_dict["SeasonalNaive"].shape}') ``` ```text theme={null} h=1: X shape=(32, 2), residuals shape=(32,) h=2: X shape=(32, 2), residuals shape=(32,) h=3: X shape=(32, 2), residuals shape=(32,) h=4: X shape=(32, 2), residuals shape=(32,) h=5: X shape=(32, 2), residuals shape=(32,) h=6: X shape=(32, 2), residuals shape=(32,) h=7: X shape=(32, 2), residuals shape=(32,) ``` ## Step 4: train one correction model per horizon Any object with a `.predict(X)` method works. Here we use `LinearRegression`, but `Ridge`, `LightGBM`, or a custom regressor would all be valid. ```python theme={null} correction_models = {} for h, (X_h, y_dict) in aligned.items(): correction_models[h] = {} for model_name, residuals in y_dict.items(): reg = LinearRegression().fit(X_h, residuals) correction_models[h][model_name] = reg print(f'Trained {len(correction_models)} correction models, one per horizon.') ``` ```text theme={null} Trained 7 correction models, one per horizon. ``` ## Step 5: forecast on the held-out window and rectify We refit the base model on the full training set, predict the held-out horizon, then build a matching feature matrix and apply `rectify`. The corrected forecasts add the predicted residual back to each base prediction. ```python theme={null} sf.fit(train) test_forecasts = sf.predict(h=HORIZON) test_forecasts['unique_id'] = test_forecasts['unique_id'].astype(str) test_forecasts = test_forecasts.sort_values(['unique_id', 'ds']).reset_index(drop=True) test_features = build_features(test_forecasts, train_history=train) rectified = rectify( df=test_forecasts, models=['SeasonalNaive'], correction_models=correction_models, features=test_features, ) rectified.head() ``` | | unique\_id | ds | SeasonalNaive | | - | ---------- | ---------- | ------------- | | 0 | 0 | 2000-05-31 | 131.317404 | | 1 | 0 | 2000-06-01 | 132.994530 | | 2 | 0 | 2000-06-02 | 134.817132 | | 3 | 0 | 2000-06-03 | 129.886634 | | 4 | 0 | 2000-06-04 | 131.599479 | ## Step 6: compare base vs rectified MAE Merge the held-out actuals with both forecast frames and use `evaluate()` to compute MAE per series. ```python theme={null} comparison = test.merge( test_forecasts.rename(columns={'SeasonalNaive': 'base'}), on=['unique_id', 'ds'], ).merge( rectified.rename(columns={'SeasonalNaive': 'rectified'}), on=['unique_id', 'ds'], ) evaluate( df=comparison, metrics=[mae], agg_fn='mean', ) ``` | | metric | base | rectified | | - | ------ | -------- | --------- | | 0 | mae | 4.865848 | 0.326756 | SeasonalNaive systematically underestimates trended series, and the corrector learns that pattern from the CV residuals. On this run the rectified MAE drops from \~4.9 to \~0.3 — about a 14x reduction. ## Horizon-aware mode: a single corrector for all horizons `per_horizon` mode trains one correction model per horizon, which is the classic Rectify formulation. When training data is small or you want a single model to share information across horizons, `horizon_aware` mode appends the horizon as an extra feature column and trains one corrector for the full range. ```python theme={null} X_all, y_all = align_rectify_features( residuals_df=residuals_df, features=train_features, models=['SeasonalNaive'], mode='horizon_aware', ) horizon_aware_models = { 'SeasonalNaive': LinearRegression().fit(X_all, y_all['SeasonalNaive']), } rectified_ha = rectify( df=test_forecasts, models=['SeasonalNaive'], correction_models=horizon_aware_models, features=test_features, mode='horizon_aware', ) rectified_ha.head() ``` | | unique\_id | ds | SeasonalNaive | | - | ---------- | ---------- | ------------- | | 0 | 0 | 2000-05-31 | 131.422401 | | 1 | 0 | 2000-06-01 | 132.950212 | | 2 | 0 | 2000-06-02 | 134.765042 | | 3 | 0 | 2000-06-03 | 129.799484 | | 4 | 0 | 2000-06-04 | 131.909747 | A few notes on the API differences between the modes: * `mode='horizon_aware'` is keyword-only, so it must be passed by name * `correction_models` is flat (`{model_name: regressor}`) instead of nested by horizon * The horizon column is appended to the feature matrix before fitting, so the corrector sees horizon as just another feature ## Custom column names All three functions accept `id_col` and `time_col` parameters. `compute_rectify_residuals` additionally accepts `target_col` and `cutoff_col`. Pass them consistently across the pipeline if your data uses non-default names. ```python theme={null} renamed = cv_df.rename(columns={'unique_id': 'series_id', 'ds': 'timestamp'}) residuals_renamed = compute_rectify_residuals( df=renamed, forecasts_df=renamed, models=['SeasonalNaive'], id_col='series_id', time_col='timestamp', target_col='y', cutoff_col='cutoff', ) residuals_renamed.columns.tolist() ``` ```text theme={null} ['series_id', 'timestamp', 'cutoff', 'horizon', 'SeasonalNaive'] ``` ## Key takeaways * **Rectify is post-processing.** Your base forecaster doesn’t need to change; the correction model lives on top of it. * **Use `cutoff_col` when training from cross-validation output.** Without it, repeated `(id, ds)` pairs across folds collide on the join. * **Features must be row-aligned with the dataframe.** Sort `cv_df` and the forecast frame consistently before extracting features and you’re safe. * **`per_horizon` vs `horizon_aware` is a sample-size call.** When you have plenty of folds, separate models per horizon usually win. When data is thin, one shared model with horizon as a feature does better. * **Anything with a `predict(X)` method works.** sklearn, lightgbm, xgboost, your own class. # Evaluation Source: https://nixtlaverse.nixtla.io/utilsforecast/evaluation.html Model performance evaluation ### `evaluate` ```python theme={null} evaluate(df, metrics, models=None, train_df=None, level=None, id_col='unique_id', time_col='ds', target_col='y', cutoff_col='cutoff', agg_fn=None) ``` Evaluate forecast using different metrics. **Parameters:** | Name | Type | Description | Default | | ------------ | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas, polars, dask or spark DataFrame | Forecasts to evaluate. Must have `id_col`, `time_col`, `target_col` and models' predictions. | *required* | | `metrics` | list of callable | Functions with arguments `df`, `models`, `id_col`, `target_col` and optionally `train_df`. | *required* | | `models` | list of str | Names of the models to evaluate. If `None` will use every column in the dataframe after removing id, time and target. Defaults to None. | None | | `train_df` | pandas, polars, dask or spark DataFrame | Training set. Used to evaluate metrics such as `mase`. Defaults to None. | None | | `level` | list of int | Prediction interval levels. Used to compute losses that rely on quantiles. Defaults to None. | None | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | | `agg_fn` | [str](#str) | Statistic to compute on the scores by id to reduce them to a single number. Defaults to None. | None | **Returns:** | Type | Description | | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [AnyDFType](#utilsforecast.compat.AnyDFType) | pandas, polars, dask or spark DataFrame: Metrics with one row per (id, metric) combination and one column per model. If `agg_fn` is not `None`, there is only one row per metric. | # Feature Engineering | UtilsForecast Source: https://nixtlaverse.nixtla.io/utilsforecast/feature_engineering.html Create exogenous regressors for your models ### `fourier` ```python theme={null} fourier(df, freq, season_length, k, h=0, id_col='unique_id', time_col='ds') ``` Compute fourier seasonal terms for training and forecasting **Parameters:** | Name | Type | Description | Default | | --------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | Dataframe with ids, times and values for the exogenous regressors. | *required* | | `freq` | [str](#str) or [int](#int) | Frequency of the data. Must be a valid pandas or polars offset alias, or an integer. | *required* | | `season_length` | [int](#int) | Number of observations per unit of time. Ex: 24 Hourly data. | *required* | | `k` | [int](#int) | Maximum order of the fourier terms | *required* | | `h` | [int](#int) | Forecast horizon. Defaults to 0. | 0 | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[DFType](#utilsforecast.compat.DFType), [DFType](#utilsforecast.compat.DFType)] | tuple\[pandas or polars DataFrame, pandas or polars DataFrame]: A tuple containing the original DataFrame with the computed features and DataFrame with future values. | ### `trend` ```python theme={null} trend(df, freq, h=0, id_col='unique_id', time_col='ds') ``` Add a trend column with consecutive integers for training and forecasting **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | Dataframe with ids, times and values for the exogenous regressors. | *required* | | `freq` | [str](#str) or [int](#int) | Frequency of the data. Must be a valid pandas or polars offset alias, or an integer. | *required* | | `h` | [int](#int) | Forecast horizon. Defaults to 0. | 0 | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[DFType](#utilsforecast.compat.DFType), [DFType](#utilsforecast.compat.DFType)] | tuple\[pandas or polars DataFrame, pandas or polars DataFrame]: A tuple containing the original DataFrame with the computed features and DataFrame with future values. | ### `time_features` ```python theme={null} time_features(df, freq, features, h=0, id_col='unique_id', time_col='ds') ``` Compute timestamp-based features for training and forecasting **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Dataframe with ids, times and values for the exogenous regressors. | *required* | | `freq` | [str](#str) or [int](#int) | Frequency of the data. Must be a valid pandas or polars offset alias, or an integer. | *required* | | `features` | list of str or callable | Features to compute. Can be string aliases of timestamp attributes or functions to apply to the times. | *required* | | `h` | [int](#int) | Forecast horizon. Defaults to 0. | 0 | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[DFType](#utilsforecast.compat.DFType), [DFType](#utilsforecast.compat.DFType)] | tuple\[pandas or polars DataFrame, pandas or polars DataFrame]: A tuple containing the original DataFrame with the computed features and DataFrame with future values. | ### `future_exog_to_historic` ```python theme={null} future_exog_to_historic(df, freq, features, h=0, id_col='unique_id', time_col='ds') ``` Turn future exogenous features into historic by shifting them `h` steps. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | Dataframe with ids, times and values for the exogenous regressors. | *required* | | `freq` | [str](#str) or [int](#int) | Frequency of the data. Must be a valid pandas or polars offset alias, or an integer. | *required* | | `features` | list of str | Features to be converted into historic. | *required* | | `h` | [int](#int) | Forecast horizon. Defaults to 0. | 0 | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[DFType](#utilsforecast.compat.DFType), [DFType](#utilsforecast.compat.DFType)] | tuple\[pandas or polars DataFrame, pandas or polars DataFrame]: A tuple containing the original DataFrame with the computed features and DataFrame with future values. | ### `pipeline` ```python theme={null} pipeline(df, features, freq, h=0, id_col='unique_id', time_col='ds') ``` Compute several features for training and forecasting **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | Dataframe with ids, times and values for the exogenous regressors. | *required* | | `features` | list of callable | List of features to compute. Must take only df, freq, h, id\_col and time\_col (other arguments must be fixed). | *required* | | `freq` | [str](#str) or [int](#int) | Frequency of the data. Must be a valid pandas or polars offset alias, or an integer. | *required* | | `h` | [int](#int) | Forecast horizon. Defaults to 0. | 0 | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | **Returns:** | Type | Description | | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tuple](#typing.Tuple)\[[DFType](#utilsforecast.compat.DFType), [DFType](#utilsforecast.compat.DFType)] | tuple\[pandas or polars DataFrame, pandas or polars DataFrame]: A tuple containing the original DataFrame with the computed features and DataFrame with future values. | # utilsforecast Source: https://nixtlaverse.nixtla.io/utilsforecast/index.html Forecasting utilities ## Install ### PyPI ```sh theme={null} pip install utilsforecast ``` ### Conda ```sh theme={null} conda install -c conda-forge utilsforecast ``` *** ## How to use ### Generate synthetic data ```python theme={null} from utilsforecast.data import generate_series series = generate_series(3, with_trend=True, static_as_categorical=False) series ``` ``` | | unique_id | ds | y | |-----|-----------|------------|------------| | 0 | 0 | 2000-01-01 | 0.422133 | | 1 | 0 | 2000-01-02 | 1.501407 | | 2 | 0 | 2000-01-03 | 2.568495 | | 3 | 0 | 2000-01-04 | 3.529085 | | 4 | 0 | 2000-01-05 | 4.481929 | | ... | ... | ... | ... | | 481 | 2 | 2000-06-11 | 163.914625 | | 482 | 2 | 2000-06-12 | 166.018479 | | 483 | 2 | 2000-06-13 | 160.839176 | | 484 | 2 | 2000-06-14 | 162.679603 | | 485 | 2 | 2000-06-15 | 165.089288 | ``` *** ### Plotting ```python theme={null} from utilsforecast.plotting import plot_series fig = plot_series(series, plot_random=False, max_insample_length=50, engine='matplotlib') fig.savefig('imgs/index.png', bbox_inches='tight') ``` *** ### Preprocessing ```python theme={null} from utilsforecast.preprocessing import fill_gaps serie = series[series['unique_id'].eq(0)].tail(10) # drop some points with_gaps = serie.sample(frac=0.5, random_state=0).sort_values('ds') with_gaps ``` Example output with missing dates: ``` | | unique_id | ds | y | |-----|-----------|------------|-----------| | 213 | 0 | 2000-08-01 | 18.543147 | | 214 | 0 | 2000-08-02 | 19.941764 | | 216 | 0 | 2000-08-04 | 21.968733 | | 220 | 0 | 2000-08-08 | 19.091509 | | 221 | 0 | 2000-08-09 | 20.220739 | ``` ```python theme={null} fill_gaps(with_gaps, freq='D') ``` Returns: ``` | | unique_id | ds | y | |-----|-----------|------------|-----------| | 0 | 0 | 2000-08-01 | 18.543147 | | 1 | 0 | 2000-08-02 | 19.941764 | | 2 | 0 | 2000-08-03 | NaN | | 3 | 0 | 2000-08-04 | 21.968733 | | 4 | 0 | 2000-08-05 | NaN | | 5 | 0 | 2000-08-06 | NaN | | 6 | 0 | 2000-08-07 | NaN | | 7 | 0 | 2000-08-08 | 19.091509 | | 8 | 0 | 2000-08-09 | 20.220739 | ``` *** ### Evaluating ```python theme={null} from functools import partial import numpy as np from utilsforecast.evaluation import evaluate from utilsforecast.losses import mape, mase ``` ```python theme={null} valid = series.groupby('unique_id').tail(7).copy() train = series.drop(valid.index) rng = np.random.RandomState(0) valid['seas_naive'] = train.groupby('unique_id')['y'].tail(7).values valid['rand_model'] = valid['y'] * rng.rand(valid['y'].shape[0]) daily_mase = partial(mase, seasonality=7) evaluate(valid, metrics=[mape, daily_mase], train_df=train) ``` ``` | | unique_id | metric | seas_naive | rand_model | |-----|-----------|--------|------------|------------| | 0 | 0 | mape | 0.024139 | 0.440173 | | 1 | 1 | mape | 0.054259 | 0.278123 | | 2 | 2 | mape | 0.042642 | 0.480316 | | 3 | 0 | mase | 0.907149 | 16.418014 | | 4 | 1 | mase | 0.991635 | 6.404254 | | 5 | 2 | mase | 1.013596 | 11.365040 | ``` *** # Losses Source: https://nixtlaverse.nixtla.io/utilsforecast/losses.html Loss functions for model evaluation. The most important train signal is the forecast error, which is the difference between the observed value $y_{\tau}$ and the prediction $\hat{y}_{\tau}$, at time $y_{\tau}$: ```math theme={null} e_{\tau} = y_{\tau}-\hat{y}_{\tau} \qquad \qquad \tau \in \{t+1,\dots,t+H \} ``` The train loss summarizes the forecast errors in different evaluation metrics. ## 1. Scale-dependent Errors ### Mean Absolute Error ```math theme={null} \mathrm{MAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} |y_{\tau} - \hat{y}_{\tau}| ``` #### `mae` ```python theme={null} mae(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Mean Absolute Error (MAE) MAE measures the relative prediction accuracy of a forecasting method by calculating the deviation of the prediction and the true value at a given time and averages these devations over the length of the series. ### Mean Squared Error ```math theme={null} \mathrm{MSE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} (y_{\tau} - \hat{y}_{\tau})^{2} ``` #### `mse` ```python theme={null} mse(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Mean Squared Error (MSE) MSE measures the relative prediction accuracy of a forecasting method by calculating the squared deviation of the prediction and the true value at a given time, and averages these devations over the length of the series. ### Root Mean Squared Error ```math theme={null} \mathrm{RMSE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \sqrt{\frac{1}{H} \sum^{t+H}_{\tau=t+1} (y_{\tau} - \hat{y}_{\tau})^{2}} ``` #### `rmse` ```python theme={null} rmse(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Root Mean Squared Error (RMSE) RMSE measures the relative prediction accuracy of a forecasting method by calculating the squared deviation of the prediction and the observed value at a given time and averages these devations over the length of the series. Finally the RMSE will be in the same scale as the original time series so its comparison with other series is possible only if they share a common scale. RMSE has a direct connection to the L2 norm. ### Bias ```math theme={null} \mathrm{Bias}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} (\hat{y}_{\tau} - \mathbf{y}_{\tau}) ``` #### `bias` ```python theme={null} bias(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Forecast estimator bias. Defined as prediction - actual ### Cumulative Forecast Error ```math theme={null} \mathrm{CFE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \sum^{t+H}_{\tau=t+1} (\hat{y}_{\tau} - \mathbf{y}_{\tau}) ``` #### `cfe` ```python theme={null} cfe(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Cumulative Forecast Error (CFE) Total signed forecast error per series. Positive values mean under forecast; negative mean over forecast. ### Absolute Periods In Stock ```math theme={null} \mathrm{PIS}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \sum^{t+H}_{\tau=t+1} |y_{\tau} - \hat{y}_{\tau}| ``` #### `pis` ```python theme={null} pis(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Compute the raw Absolute Periods In Stock (PIS) for one or multiple models. The PIS metric sums the absolute forecast errors per series without any scaling, yielding a scale-dependent measure of bias. ### Linex ```math theme={null} \mathrm{Linex}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} (e^{a(y_{\tau} - \hat{y}_{\tau})} - a(y_{\tau} - \hat{y}_{\tau}) - 1) ``` where must be $a\neq0$. #### `linex` ```python theme={null} linex(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff', a=1.0) ``` Linex Loss (Linear Exponential) The Linex loss penalizes over- and under-forecasting asymmetrically depending on the parameter a. * If a > 0, under-forecasting ($y > \hat{y}$) is penalized more. * If a \< 0, over-forecasting ($\hat{y} > y$) is penalized more. * a must not be 0. **Parameters:** | Name | Type | Description | Default | | ---- | ---------------------------- | ------------------------------------------------------- | ---------------- | | `a` | [float](#float) | Asymmetry parameter. Must be non-zero. Defaults to 1.0. | 1.0 | ## 2. Percentage Errors ### Mean Absolute Percentage Error ```math theme={null} \mathrm{MAPE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{|y_{\tau}|} ``` #### `mape` ```python theme={null} mape(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Mean Absolute Percentage Error (MAPE) MAPE measures the relative prediction accuracy of a forecasting method by calculating the percentual deviation of the prediction and the observed value at a given time and averages these devations over the length of the series. The closer to zero an observed value is, the higher penalty MAPE loss assigns to the corresponding error. ### Symmetric Mean Absolute Percentage Error ```math theme={null} \mathrm{SMAPE}_{2}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{|y_{\tau}|+|\hat{y}_{\tau}|} ``` #### `smape` ```python theme={null} smape(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Symmetric Mean Absolute Percentage Error (SMAPE) SMAPE measures the relative prediction accuracy of a forecasting method by calculating the relative deviation of the prediction and the observed value scaled by the sum of the absolute values for the prediction and observed value at a given time, then averages these devations over the length of the series. This allows the SMAPE to have bounds between 0% and 100% which is desirable compared to normal MAPE that may be undetermined when the target is zero. ## 3. Scale-independent Errors ### Mean Absolute Scaled Error ```math theme={null} \mathrm{MASE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{\mathrm{MAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau})} ``` #### `mase` ```python theme={null} mase(df, models, seasonality, train_df, id_col='unique_id', target_col='y', cutoff_col='cutoff', time_col='ds') ``` Mean Absolute Scaled Error (MASE) MASE measures the relative prediction accuracy of a forecasting method by comparinng the mean absolute errors of the prediction and the observed value against the mean absolute errors of the seasonal naive model. The MASE partially composed the Overall Weighted Average (OWA), used in the M4 Competition. **Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, actuals and predictions. | *required* | | `models` | list of str | Columns that identify the models predictions. | *required* | | `seasonality` | [int](#int) | Main frequency of the time series; Hourly 24, Daily 7, Weekly 52, Monthly 12, Quarterly 4, Yearly 1. | *required* | | `train_df` | pandas or polars DataFrame | Training dataframe with id and actual values. Must be sorted by time. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. |
References \[1] [https://robjhyndman.com/papers/mase.pdf](https://robjhyndman.com/papers/mase.pdf)
### Relative Mean Absolute Error ```math theme={null} \mathrm{RMAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}, \mathbf{\hat{y}}^{base}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau}-\hat{y}_{\tau}|}{\mathrm{MAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{base}_{\tau})} ``` #### `rmae` ```python theme={null} rmae(df, models, baseline, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Relative Mean Absolute Error (RMAE) Calculates the RAME between two sets of forecasts (from two different forecasting methods). A number smaller than one implies that the forecast in the numerator is better than the forecast in the denominator. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, times, actuals and predictions. | *required* | | `models` | list of str | Columns that identify the models predictions. | *required* | | `baseline` | [str](#str) | Column that identifies the baseline model predictions. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. | ### Normalized Deviation ```math theme={null} \mathrm{ND}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \frac{\sum^{t+H}_{\tau=t+1} |y_{\tau} - \hat{y}_{\tau}|}{\sum^{t+H}_{\tau=t+1} | y_{\tau} |} ``` #### `nd` ```python theme={null} nd(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Normalized Deviation (ND) ND measures the relative prediction accuracy of a forecasting method by calculating the sum of the absolute deviation of the prediction and the true value at a given time and dividing it by the sum of the absolute value of the ground truth. ### Mean Squared Scaled Error ```math theme={null} \mathrm{MSSE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{(y_{\tau}-\hat{y}_{\tau})^2}{\mathrm{MSE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau})} ``` ### `msse` ```python theme={null} msse(df, models, seasonality, train_df, id_col='unique_id', target_col='y', cutoff_col='cutoff', time_col='ds') ``` Mean Squared Scaled Error (MSSE) MSSE measures the relative prediction accuracy of a forecasting method by comparinng the mean squared errors of the prediction and the observed value against the mean squared errors of the seasonal naive model. **Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, actuals and predictions. | *required* | | `models` | list of str | Columns that identify the models predictions. | *required* | | `seasonality` | [int](#int) | Main frequency of the time series; Hourly 24, Daily 7, Weekly 52, Monthly 12, Quarterly 4, Yearly 1. | *required* | | `train_df` | pandas or polars DataFrame | Training dataframe with id and actual values. Must be sorted by time. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. |
References \[1] [https://otexts.com/fpp3/accuracy.html](https://otexts.com/fpp3/accuracy.html)
### Root Mean Squared Scaled Error ```math theme={null} \mathrm{RMSSE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau}) = \sqrt{\frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{(y_{\tau}-\hat{y}_{\tau})^2}{\mathrm{MSE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau})}} ``` ### `rmsse` ```python theme={null} rmsse(df, models, seasonality, train_df, id_col='unique_id', target_col='y', cutoff_col='cutoff', time_col='ds') ``` Root Mean Squared Scaled Error (RMSSE) MSSE measures the relative prediction accuracy of a forecasting method by comparinng the mean squared errors of the prediction and the observed value against the mean squared errors of the seasonal naive model. **Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------- | | `df` | pandas or polars DataFrame | Input dataframe with id, actuals and predictions. | *required* | | `models` | list of str | Columns that identify the models predictions. | *required* | | `seasonality` | int | Main frequency of the time series; Hourly 24, Daily 7, Weekly 52, Monthly 12, Quarterly 4, Yearly 1. | *required* | | `train_df` | pandas or polars DataFrame | Training dataframe with id and actual values. Must be sorted by time. | *required* | | `id_col` | str | Column that identifies each serie. Defaults to 'unique\_id'. | *required* | | `target_col` | str | Column that contains the target. Defaults to 'y'. | *required* | | `cutoff_col` | str | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | *required* | **Returns:** | Type | Description | | ----------------------------------------------------------------------------------- | ----------- | | pandas or polars DataFrame: dataframe with one row per id and one column per model. | |
References \[1] [https://otexts.com/fpp3/accuracy.html](https://otexts.com/fpp3/accuracy.html)
### Scaled Absolute Periods In Stock ```math theme={null} \mathrm{PIS}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}_{\tau}) = \sum^{t+H}_{\tau=t+1} \frac{|y_{\tau} - \hat{y}_{\tau}|}{\bar{y}} ``` where $\bar{y}=\frac{1}{H}\sum^{t+H}_{\tau=t+1} y_{\tau}$. #### `spis` ```python theme={null} spis(df, models, train_df, id_col='unique_id', target_col='y', cutoff_col='cutoff', time_col='ds') ``` Compute the scaled Absolute Periods In Stock (sAPIS) for one or multiple models. The sPIS metric scales the sum of absolute forecast errors by the mean in-sample demand, yielding a scale-independent bias measure that can be aggregated across series. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, actuals and predictions. | *required* | | `models` | list of str | Columns that identify the models predictions. | *required* | | `train_df` | pandas or polars DataFrame | Training dataframe with id and actual values. Must be sorted by time. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. | ## 4. Probabilistic Errors ### Quantile Loss ```math theme={null} \mathrm{QL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q)}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \Big( (1-q)\,( \hat{y}^{(q)}_{\tau} - y_{\tau} )_{+} + q\,( y_{\tau} - \hat{y}^{(q)}_{\tau} )_{+} \Big) ``` #### `quantile_loss` ```python theme={null} quantile_loss(df, models, q=0.5, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Quantile Loss (QL) QL measures the deviation of a quantile forecast. By weighting the absolute deviation in a non symmetric way, the loss pays more attention to under or over estimation. A common value for q is 0.5 for the deviation from the median. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, times, actuals and predictions. | *required* | | `models` | dict from str to str | Mapping from model name to the model predictions for the specified quantile. | *required* | | `q` | [float](#float) | Quantile for the predictions' comparison. Defaults to 0.5. | 0.5 | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. | ### Scaled Quantile Loss ```math theme={null} \mathrm{SQL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q)}_{\tau}) = \frac{1}{H} \sum^{t+H}_{\tau=t+1} \frac{(1-q)\,( \hat{y}^{(q)}_{\tau} - y_{\tau} )_{+} + q\,( y_{\tau} - \hat{y}^{(q)}_{\tau} )_{+}}{\mathrm{MAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau})} ``` #### `scaled_quantile_loss` ```python theme={null} scaled_quantile_loss(df, models, seasonality, train_df, q=0.5, id_col='unique_id', target_col='y', cutoff_col='cutoff', time_col='ds') ``` Scaled Quantile Loss (SQL) SQL measures the deviation of a quantile forecast scaled by the mean absolute errors of the seasonal naive model. By weighting the absolute deviation in a non symmetric way, the loss pays more attention to under or over estimation. A common value for q is 0.5 for the deviation from the median. This was the official measure used in the M5 Uncertainty competition with seasonality = 1. **Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, times, actuals and predictions. | *required* | | `models` | dict from str to str | Mapping from model name to the model predictions for the specified quantile. | *required* | | `seasonality` | [int](#int) | Main frequency of the time series; Hourly 24, Daily 7, Weekly 52, Monthly 12, Quarterly 4, Yearly 1. | *required* | | `train_df` | pandas or polars DataFrame | Training dataframe with id and actual values. Must be sorted by time. | *required* | | `q` | [float](#float) | Quantile for the predictions' comparison. Defaults to 0.5. | 0.5 | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. |
References \[1] [https://www.sciencedirect.com/science/article/pii/S0169207021001722](https://www.sciencedirect.com/science/article/pii/S0169207021001722)
### Multi-Quantile Loss ```math theme={null} \mathrm{MQL}(\mathbf{y}_{\tau}, [\mathbf{\hat{y}}^{(q_{1})}_{\tau}, ... ,\hat{y}^{(q_{n})}_{\tau}]) = \frac{1}{n} \sum_{q_{i}} \mathrm{QL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q_{i})}_{\tau}) ``` #### `mqloss` ```python theme={null} mqloss(df, models, quantiles, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Multi-Quantile loss (MQL) MQL calculates the average multi-quantile Loss for a given set of quantiles, based on the absolute difference between predicted quantiles and observed values. The limit behavior of MQL allows to measure the accuracy of a full predictive distribution with the continuous ranked probability score (CRPS). This can be achieved through a numerical integration technique, that discretizes the quantiles and treats the CRPS integral with a left Riemann approximation, averaging over uniformly distanced quantiles. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, times, actuals and predictions. | *required* | | `models` | dict from str to list of str | Mapping from model name to the model predictions for each quantile. | *required* | | `quantiles` | numpy array | Quantiles to compare against. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. |
References \[1] [https://www.jstor.org/stable/2629907](https://www.jstor.org/stable/2629907)
### Scaled Multi-Quantile Loss ```math theme={null} \mathrm{MQL}(\mathbf{y}_{\tau}, [\mathbf{\hat{y}}^{(q_{1})}_{\tau}, ... ,\hat{y}^{(q_{n})}_{\tau}]) = \frac{1}{n} \sum_{q_{i}} \frac{\mathrm{QL}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{(q_{i})}_{\tau})}{\mathrm{MAE}(\mathbf{y}_{\tau}, \mathbf{\hat{y}}^{season}_{\tau})} ``` #### `scaled_mqloss` ```python theme={null} scaled_mqloss(df, models, quantiles, seasonality, train_df, id_col='unique_id', target_col='y', cutoff_col='cutoff', time_col='ds') ``` Scaled Multi-Quantile loss (SMQL) SMQL calculates the average multi-quantile Loss for a given set of quantiles, based on the absolute difference between predicted quantiles and observed values scaled by the mean absolute errors of the seasonal naive model. The limit behavior of MQL allows to measure the accuracy of a full predictive distribution with the continuous ranked probability score (CRPS). This can be achieved through a numerical integration technique, that discretizes the quantiles and treats the CRPS integral with a left Riemann approximation, averaging over uniformly distanced quantiles. This was the official measure used in the M5 Uncertainty competition with seasonality = 1. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, times, actuals and predictions. | *required* | | `models` | dict from str to list of str | Mapping from model name to the model predictions for each quantile. | *required* | | `quantiles` | numpy array | Quantiles to compare against. | *required* | | `seasonality` | [int](#int) | Main frequency of the time series; Hourly 24, Daily 7, Weekly 52, Monthly 12, Quarterly 4, Yearly 1. | *required* | | `train_df` | pandas or polars DataFrame | Training dataframe with id and actual values. Must be sorted by time. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. |
References \[1] [https://www.sciencedirect.com/science/article/pii/S0169207021001722](https://www.sciencedirect.com/science/article/pii/S0169207021001722)
### Coverage #### `coverage` ```python theme={null} coverage(df, models, level, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Coverage of y with y\_hat\_lo and y\_hat\_hi. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, times, actuals and predictions. | *required* | | `models` | list of str | Columns that identify the models predictions. | *required* | | `level` | [int](#int) | Confidence level used for intervals. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. |
References \[1] [https://www.jstor.org/stable/2629907](https://www.jstor.org/stable/2629907)
### Calibration #### `calibration` ```python theme={null} calibration(df, models, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Fraction of y that is lower than the model's predictions. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, times, actuals and predictions. | *required* | | `models` | dict from str to str | Mapping from model name to the model predictions. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. |
References \[1] [https://www.jstor.org/stable/2629907](https://www.jstor.org/stable/2629907)
### CRPS ```math theme={null} \mathrm{sCRPS}(\hat{F}_{\tau}, \mathbf{y}_{\tau}) = \frac{2}{N} \sum_{i} \int^{1}_{0} \frac{\mathrm{QL}(\hat{F}_{i,\tau}, y_{i,\tau})_{q}}{\sum_{i} | y_{i,\tau} |} dq ``` Where $\hat{F}_{\tau}$ is the an estimated multivariate distribution, and $y_{i,\tau}$ are its realizations. #### `scaled_crps` ```python theme={null} scaled_crps(df, models, quantiles, id_col='unique_id', target_col='y', cutoff_col='cutoff') ``` Scaled Continues Ranked Probability Score Calculates a scaled variation of the CRPS, as proposed by Rangapuram (2021), to measure the accuracy of predicted quantiles `y_hat` compared to the observation `y`. This metric averages percentual weighted absolute deviations as defined by the quantile losses. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, times, actuals and predictions. | *required* | | `models` | dict from str to list of str | Mapping from model name to the model predictions for each quantile. | *required* | | `quantiles` | numpy array | Quantiles to compare against. | *required* | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: dataframe with one row per id and one column per model. |
References \[1] [https://proceedings.mlr.press/v139/rangapuram21a.html](https://proceedings.mlr.press/v139/rangapuram21a.html)
### Tweedie Deviance For a set of forecasts $\{\mu_i\}_{i=1}^N$ and observations $\{y_i\}_{i=1}^N$, the mean Tweedie deviance with power $p$ is ```math theme={null} \mathrm{TD}_{p}(\boldsymbol{\mu}, \mathbf{y}) = \frac{1}{N} \sum_{i=1}^{N} d_{p}(y_i, \mu_i) ``` where the unit-scaled deviance for each pair $(y,\mu)$ is ```math theme={null} d_{p}(y,\mu) = 2 \begin{cases} \displaystyle \frac{y^{2-p}}{(1-p)(2-p)} \;-\; \frac{y\,\mu^{1-p}}{1-p} \;+\; \frac{\mu^{2-p}}{2-p}, & p \notin\{1,2\},\\[1em] \displaystyle y\,\ln\!\frac{y}{\mu}\;-\;(y-\mu), & p = 1\quad(\text{Poisson deviance}),\\[0.5em] \displaystyle -2\Bigl[\ln\!\frac{y}{\mu}\;-\;\frac{y-\mu}{\mu}\Bigr], & p = 2\quad(\text{Gamma deviance}). \end{cases} ``` * $y_i$ are the true values, $\mu_i$ the predicted means. * $p$ controls the variance relationship $\mathrm{Var}(Y)\propto\mu^{p}$. * When $1 2: Inverse Gaussian **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------- | | `df` | pandas or polars DataFrame | Input dataframe with id, actuals and predictions. | *required* | | `models` | list of str | Columns that identify the models predictions. | *required* | | `power` | [float](#float) | Tweedie power parameter. Determines the compound distribution. Defaults to 1.5. | 1.5 | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `cutoff_col` | [str](#str) | Column that identifies the cutoff point for each forecast cross-validation fold. Defaults to 'cutoff'. | 'cutoff' | **Returns:** | Type | Description | | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | | [IntoDataFrameT](#narwhals.stable.v2.typing.IntoDataFrameT) | pandas or polars DataFrame: DataFrame with one row per id and one column per model, containing the mean Tweedie deviance. |
References \[1] [https://en.wikipedia.org/wiki/Tweedie\_distribution](https://en.wikipedia.org/wiki/Tweedie_distribution)
# Plotting Source: https://nixtlaverse.nixtla.io/utilsforecast/plotting.html Time series visualizations ### `plot_series` ```python theme={null} plot_series(df=None, forecasts_df=None, ids=None, plot_random=True, max_ids=8, models=None, level=None, max_insample_length=None, plot_anomalies=False, engine='matplotlib', palette=None, id_col='unique_id', time_col='ds', target_col='y', seed=0, resampler_kwargs=None, ax=None) ``` Plot forecasts and insample values. **Parameters:** | Name | Type | Description | Default | | --------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | DataFrame with columns \[`id_col`, `time_col`, `target_col`]. Defaults to None. | None | | `forecasts_df` | pandas or polars DataFrame | DataFrame with columns \[`id_col`, `time_col`] and models. Defaults to None. | None | | `ids` | list of str | Time Series to plot. If None, time series are selected randomly. Defaults to None. | None | | `plot_random` | [bool](#bool) | Select time series to plot randomly. Defaults to True. | True | | `max_ids` | [int](#int) | Maximum number of ids to plot. Defaults to 8. | 8 | | `models` | list of str | Models to plot. Defaults to None. | None | | `level` | list of float | Prediction intervals to plot. Defaults to None. | None | | `max_insample_length` | [int](#int) | Maximum number of train/insample observations to be plotted. Defaults to None. | None | | `plot_anomalies` | [bool](#bool) | Plot anomalies for each prediction interval. Defaults to False. | False | | `engine` | [str](#str) | Library used to plot. 'plotly', 'plotly-resampler' or 'matplotlib'. Defaults to 'matplotlib'. | 'matplotlib' | | `palette` | [str](#str) | Name of the matplotlib colormap to use for the plots. If None, uses the current style. Defaults to None. | None | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestep, its values can be timestamps or integers. Defaults to 'ds'. | 'ds' | | `target_col` | [str](#str) | Column that contains the target. Defaults to 'y'. | 'y' | | `seed` | [int](#int) | Seed used for the random number generator. Only used if plot\_random is True. Defaults to 0. | 0 | | `resampler_kwargs` | [dict](#dict) | Keyword arguments to be passed to plotly-resampler constructor. For further custumization ("show\_dash") call the method, store the plotting object and add the extra arguments to its `show_dash` method. Defaults to None. | None | | `ax` | matplotlib axes, array of matplotlib axes or plotly Figure | Object where plots will be added. Defaults to None. | None | **Returns:** | Type | Description | | ------------------------------------------ | ----------- | | matplotlib or plotly figure: Plot's figure | | ```python theme={null} from utilsforecast.data import generate_series ``` ```python theme={null} level = [80, 95] series = generate_series(4, freq='D', equal_ends=True, with_trend=True, n_models=2, level=level) test_pd = series.groupby('unique_id', observed=True).tail(10).copy() train_pd = series.drop(test_pd.index) ``` ```python theme={null} plt.style.use('ggplot') fig = plot_series( train_pd, forecasts_df=test_pd, ids=[0, 3], plot_random=False, level=level, max_insample_length=50, engine='matplotlib', plot_anomalies=True, ) fig.savefig('imgs/plotting.png', bbox_inches='tight') ``` # Preprocessing Source: https://nixtlaverse.nixtla.io/utilsforecast/preprocessing.html Utilities for processing data before training/analysis ### `id_time_grid` ```python theme={null} id_time_grid(df, freq, start='per_serie', end='global', id_col='unique_id', time_col='ds') ``` Generate all expected combiations of ids and times. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | Input data | *required* | | `freq` | [str](#str) or [int](#int) | Series' frequency | *required* | | `start` | ([str](#str), [int](#int), [date](#datetime.date) or [datetime](#datetime.datetime)) | Initial timestamp for the series. \* 'per\_serie' uses each serie's first timestamp \* 'global' uses the first timestamp seen in the data \* Can also be a specific timestamp or integer, e.g. '2000-01-01', 2000 or datetime(2000, 1, 1) Defaults to "per\_serie". | 'per\_serie' | | `end` | ([str](#str), [int](#int), [date](#datetime.date) or [datetime](#datetime.datetime)) | Initial timestamp for the series. \* 'per\_serie' uses each serie's last timestamp \* 'global' uses the last timestamp seen in the data \* Can also be a specific timestamp or integer, e.g. '2000-01-01', 2000 or datetime(2000, 1, 1) Defaults to "global". | 'global' | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestamp. Defaults to 'ds'. | 'ds' | **Returns:** | Type | Description | | --------------------------------------------------- | ------------------------------------------------------------------ | | [DFType](#utilsforecast.compat.DFType) | pandas or polars DataFrame: Dataframe with expected ids and times. | ### `fill_gaps` ```python theme={null} fill_gaps(df, freq, start='per_serie', end='global', id_col='unique_id', time_col='ds') ``` Enforce start and end datetimes for dataframe. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `df` | pandas or polars DataFrame | Input data | *required* | | `freq` | [str](#str) or [int](#int) | Series' frequency | *required* | | `start` | ([str](#str), [int](#int), [date](#datetime.date) or [datetime](#datetime.datetime)) | Initial timestamp for the series. \* 'per\_serie' uses each serie's first timestamp \* 'global' uses the first timestamp seen in the data \* Can also be a specific timestamp or integer, e.g. '2000-01-01', 2000 or datetime(2000, 1, 1) Defaults to "per\_serie". | 'per\_serie' | | `end` | ([str](#str), [int](#int), [date](#datetime.date) or [datetime](#datetime.datetime)) | Initial timestamp for the series. \* 'per\_serie' uses each serie's last timestamp \* 'global' uses the last timestamp seen in the data \* Can also be a specific timestamp or integer, e.g. '2000-01-01', 2000 or datetime(2000, 1, 1) Defaults to "global". | 'global' | | `id_col` | [str](#str) | Column that identifies each serie. Defaults to 'unique\_id'. | 'unique\_id' | | `time_col` | [str](#str) | Column that identifies each timestamp. Defaults to 'ds'. | 'ds' | **Returns:** | Type | Description | | --------------------------------------------------- | ------------------------------------------------------- | | [DFType](#utilsforecast.compat.DFType) | pandas or polars DataFrame: Dataframe with gaps filled. |