目录

引言

聚合-分解间歇性需求方法 (ADIDA) 是一种预测方法,用于预测具有间歇性需求模式的产品需求。间歇性需求模式的特点是存在大量零观察值,这使得预测具有挑战性。

ADIDA 方法采用时间聚合来减少零观察值的数量,并减轻区间中观察到的方差的影响。该方法使用大小相等的时间桶进行非重叠的时间聚合,并预测预先指定的提前期的需求。时间桶的大小等于平均需求间隔,即两次连续非零观察之间的平均时间。

该方法使用简单指数平滑 (SES) 技术来获得预测。SES 是一种流行的时间序列预测技术,因其简单和有效性而在生成准确预测方面被广泛使用。

ADIDA 方法具有几个优点。它易于实现,可用于多种间歇性需求模式。该方法还提供准确的预测,并可用于预测预先指定的提前期的需求。

然而,ADIDA 方法也有一些局限性。该方法假设时间桶大小相等,这可能不适用于所有间歇性需求模式。此外,该方法可能不适用于具有复杂模式或趋势的时间序列数据。

总的来说,ADIDA 方法是一种适用于间歇性需求模式的有用预测技术,可以帮助减轻零观察值的影响,并产生准确的需求预测。

ADIDA 模型

什么是间歇性需求?

间歇性需求是一种需求模式,其特点是事件或销售不规律地、零星地发生。换句话说,它指的是产品或服务需求间歇性发生的情况,其中存在没有销售或重大事件的时间段。

间歇性需求不同于恒定或规律的需求,在恒定或规律的需求中,销售随时间以可预测且一致的方式发生。相比之下,在间歇性需求中,没有销售的时间段可能很长,并且可能没有规律的事件序列。

这种类型的需求可能发生在不同的行业和背景下,例如低消费产品、季节性产品、高变异性产品、生命周期短的产品,或者需求取决于特定事件或外部因素的情况。

间歇性需求可能对预测和库存管理带来挑战,因为它难以预测销售何时发生以及发生多少。我前面提到的 Croston 模型等方法被用于解决间歇性需求,并为这种类型的需求模式生成更准确和适当的预测。

间歇性需求问题

间歇性需求可能在库存管理和需求预测中带来各种挑战和问题。一些与间歇性需求相关的常见问题如下

  1. 不可预测的变异性:间歇性需求具有不可预测的变异性,使计划和预测变得困难。需求模式可能不规则,并且在有销售期和无销售期之间剧烈波动。

  2. 低销售频率:间歇性需求的特点是长时间没有销售。这可能导致库存管理困难,因为在需求发生时需要持有足够的库存来满足需求,同时避免在无销售期出现库存过剩。

  3. 预测误差:预测间歇性需求比预测恒定需求更难确定。传统预测模型可能不足以捕捉间歇性需求的变异性和缺乏模式,这可能导致对未来需求估计的显著误差。

  4. 对供应链的影响:间歇性需求可能影响供应链的效率,并在生产计划、供应商管理和物流方面制造困难。必须调整提前期和库存水平以满足不可预测的需求。

  5. 运营成本:在间歇性需求情况下管理库存可能会增加运营成本。在无销售期间保持充足的库存以及管理库存水平可能需要额外的存储和物流投资。

为了解决这些问题,采用了针对间歇性需求的特定管理方法,例如专业预测模型、产品分类技术和量身定制的库存策略。这些解决方案旨在最大限度地减少间歇性需求中变异性和缺乏模式的影响,优化库存管理并提高供应链效率。

ADIDA 模型

ADIDA 模型基于简单指数平滑 (SES) 方法,并使用时间聚合来处理间歇性需求问题。该模型的数学推导可概括如下

设 St 为时间 tt 的需求,其中 t=1,2,...,Tt = 1, 2, ..., T。平均需求间隔记为 MI,它是两次连续非零需求之间的平均时间。时间桶的大小设置为等于 MI。

然后将需求数据聚合成大小为 MI 的非重叠时间桶。设 Bt 为桶 tt 中的需求,其中 t=1,2,...,T/MIt = 1, 2, ..., T/MI。聚合的需求数据可以表示为

Bt=St,for(t1)MI+1jtMIB_t = \sum S_t, for (t-1)*MI + 1 ≤ j ≤ t*MI

然后将 SES 方法应用于聚合的需求数据以获得预测。桶 tt 的预测记为 FtF_t。SES 方法涉及根据时间 t 的实际需求 DtD_t 和前一时期的估计水平 Lt1L_{t-1},使用以下公式估计时间 t 的水平 LtL_t

Lt=αDt+(1α)Lt1L_t = \alpha * D_t + (1 - α) * L_{t-1}

其中 α\alpha 是控制当前需求值权重的平滑参数。

然后使用前一时期的估计水平 Lt1L_{t-1} 获得桶 tt 的预测,如下所示

Ft=Lt1F_t = L_{t-1}

然后将预测值分解,以获得原始时间段的需求预测。设 YtY_t 为时间 tt 的需求预测。分解可以使用以下公式进行

Yt=Ft/MI,for(t1)MI+1jtMIY_t = F_t / MI, for (t-1)*MI + 1 ≤ j ≤ t*MI

如何确定 ADIDA 模型是否适用于特定数据集?

要确定 ADIDA 模型是否适用于特定数据集,可以按照以下步骤进行

  1. 分析需求模式:检查数据的需求模式,确定其是否符合间歇性模式。间歇性数据的特点是在特定时期内零值比例高,需求零星。

  2. 评估季节性:检查数据中是否存在明显的季节性。ADIDA 模型假设没有季节性,或者可以通过时间聚合处理。如果数据显示复杂季节性,或无法通过时间聚合处理,ADIDA 模型可能不适用。

  3. 数据要求:考虑 ADIDA 模型的数据要求。该模型需要历史需求数据以及计算非零需求之间平均间隔的能力。确保您有足够的数据来估计参数,并且数据具有适合时间聚合的频率。

  4. 性能评估:在特定数据集上对 ADIDA 模型进行性能评估。将模型生成的预测值与实际需求值进行比较,并使用平均绝对误差 (MAE) 或均方误差 (MSE) 等评估指标。如果模型在该数据集上表现良好并生成准确的预测,则表明它适用于该数据集。

  5. 与其他模型比较:将 ADIDA 模型与适用于间歇性数据的其他预测模型进行比较。考虑 Croston、Syntetos-Boylan Approximation (SBA) 等模型,或基于专门为间歇性数据开发的指数平滑技术模型。如果 ADIDA 模型显示出与其他模型相似或更好的性能,则可以认为它是合适的。

请记住,ADIDA 模型是否适用取决于数据的具体性质和预测问题的背景。建议进行彻底的分析并试验不同的模型,以确定针对特定数据集的最合适方法。

加载库和数据

提示

需要 Statsforecast。要安装,请参阅说明

接下来,我们导入绘图库并配置绘图样式。

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)
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()
semanasmalaria_falciparum
02007-12-3150.0
12008-01-0762.0
22008-01-1476.0
32008-01-2164.0
42008-01-2838.0

StatsForecast 的输入始终是一个长格式的数据框,包含三列:unique_id、ds 和 y

  • unique_id(字符串、整型或分类)表示序列的标识符。

  • ds(日期戳)列的格式应符合 Pandas 的要求,日期最好采用 YYYY-MM-DD 格式,时间戳最好采用 YYYY-MM-DD HH:MM:SS 格式。

  • y(数值)表示我们希望预测的度量值。

df["unique_id"]="1"
df.columns=["ds", "y", "unique_id"]
df.head()
dsyunique_id
02007-12-3150.01
12008-01-0762.01
22008-01-1476.01
32008-01-2164.01
42008-01-2838.01
print(df.dtypes)
ds            object
y            float64
unique_id     object
dtype: object

我们需要将 object 类型转换为日期时间类型和数值类型。

df["ds"] = pd.to_datetime(df["ds"])
df["y"] = df["y"].astype(float).astype("int64")

使用 plot 方法探索数据

使用 StatsForecast 类中的 plot 方法绘制序列。此方法打印数据集中的一个随机序列,对于基本 EDA(探索性数据分析)非常有用。

from statsforecast import StatsForecast

StatsForecast.plot(df)

自相关图

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();

时间序列分解

如何分解时间序列以及为什么要分解?

在时间序列分析中,为了预测新值,了解过去的数据非常重要。更正式地说,了解数值随时间变化的模式非常重要。导致预测值方向错误的原因有很多。基本上,一个时间序列由四个组成部分构成。这些组成部分的变动导致时间序列模式的变化。这些组成部分是

  • 水平项 (Level): 这是随时间平均的主要值。
  • 趋势项 (Trend): 趋势是导致时间序列中出现增加或减少模式的值。
  • 季节性 (Seasonality): 这是时间序列中在短时间内发生的周期性事件,并导致时间序列中出现短期增加或减少的模式。
  • 残差/噪声 (Residual/Noise): 这是时间序列中的随机变动。

将这些组成部分随时间组合起来就形成了时间序列。大多数时间序列由水平项和噪声/残差组成,趋势项或季节性是可选值。

如果时间序列中包含季节性和趋势项,那么预测值将会受到影响。因为预测时间序列的模式可能与之前的时间序列不同。

时间序列组成部分的组合可以是两种类型: * 加法模型 * 乘法模型

加法时间序列

如果时间序列的组成部分是相加的,就构成了加法时间序列。通过可视化,我们可以说,如果时间序列的增加或减少模式在整个序列中相似,那么该时间序列就是加法时间序列。任何加法时间序列的数学函数可以表示为:y(t)=Level+Trend+Seasonality+Noisey(t) = Level + Trend + Seasonality + Noise

乘法时间序列

如果时间序列的组成部分是相乘的,那么该时间序列就称为乘法时间序列。通过可视化,如果时间序列随时间呈指数增长或下降,那么该时间序列可以被认为是乘法时间序列。乘法时间序列的数学函数可以表示为。

y(t)=LevelTrendseasonalityNoisey(t) = Level * Trend * seasonality * Noise

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
plot_seasonal_decompose(
    df["y"],
    model="additive",
    period=52,
    title="Seasonal Decomposition")

将数据分割为训练集和测试集

我们将数据分为以下两组:1. 用于训练 ADIDA 模型 的数据。2. 用于测试模型的数据。

对于测试数据,我们将使用最后 25 周的数据来测试和评估模型的性能。

train = df[df.ds<='2022-07-04'] 
test = df[df.ds>'2022-07-04']
train.shape, test.shape
((758, 3), (25, 3))

现在我们绘制训练数据和测试数据。

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()

StatsForecast 中的 ADIDA 模型 实现

要了解更多关于 ADIDA 模型 函数参数的信息,请参考下面列表。更多信息,请访问文档

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.

加载库

from statsforecast import StatsForecast
from statsforecast.models import ADIDA

实例化模型

导入并实例化模型。设置参数有时比较棘手。Rob Hyndmann 大师关于季节周期的文章对于设置 season_length 参数可能会有帮助。

season_length = 52 # Hourly data 
horizon = len(test) # number of predictions

# We call the model that we are going to use
models = [ADIDA()]

我们通过实例化一个新的 StatsForecast 对象来拟合模型,参数如下

models: 模型列表。从模型中选择您想要的模型并导入它们。

  • freq: 表示数据频率的字符串。(请参阅 pandas 可用的频率。)

  • n_jobs: n_jobs: 整型,并行处理中使用的作业数,使用 -1 表示所有核心。

  • fallback_model: 如果某个模型失败,将使用的备用模型。

所有设置都传递给构造函数。然后调用其 fit 方法并传入历史数据框。

sf = StatsForecast(models=models,
                   freq='7d', 
                   n_jobs=-1)

拟合模型

这里,我们调用 fit() 方法来拟合模型。

sf.fit(df=train)
StatsForecast(models=[ADIDA])

让我们看看我们的 ADIDA 模型 的结果。我们可以通过以下指令观察它

result=sf.fitted_[0,0].model_
result
{'mean': array([336.74736919])}

预测方法

如果您希望在具有多个序列或模型的生产环境中提高速度,我们建议使用 StatsForecast.forecast 方法,而不是 .fit.predict

主要区别在于 forecast() 方法不存储拟合值,并且在分布式环境中具有高度可扩展性。

forecast 方法接受两个参数:预测未来 h(预测期)和 level(置信水平)。

  • h (整型): 表示预测未来 h 步。在本例中,预测未来 25 周。

这里的 forecast 对象是一个新的数据框,包含一列模型名称和 y 的预测值 (y hat),以及不确定性区间的列。根据您的计算机情况,此步骤大约需要 1 分钟。

Y_hat = sf.forecast(df=train, h=horizon)
Y_hat
unique_iddsADIDA
012022-07-11336.747375
112022-07-18336.747375
212022-07-25336.747375
2212022-12-12336.747375
2312022-12-19336.747375
2412022-12-26336.747375
sf.plot(train, Y_hat.merge(test))

带置信区间的预测方法

使用 predict 方法生成预测。

predict 方法接受两个参数:预测未来 h(预测期)和 level(置信水平)。

  • h (整型): 表示预测未来 h 步。在本例中,预测未来 25 周。

这里的 forecast 对象是一个新的数据框,包含一列模型名称和 y 的预测值 (y hat),以及不确定性区间的列。

此步骤应该花费不到 1 秒。

forecast_df = sf.predict(h=horizon) 
forecast_df.head()
unique_iddsADIDA
012022-07-11336.747375
112022-07-18336.747375
212022-07-25336.747375
312022-08-01336.747375
412022-08-08336.747375

交叉验证

在之前的步骤中,我们使用历史数据来预测未来。然而,为了评估其准确性,我们也希望知道模型在过去是如何表现的。为了评估模型在您的数据上的准确性和鲁棒性,请执行交叉验证。

对于时间序列数据,交叉验证通过在历史数据上定义一个滑动窗口并预测其后续期间来进行。这种形式的交叉验证使我们能够更准确地估计模型在更广泛的时间实例上的预测能力,同时保持训练集中的数据连续,这是我们的模型所要求的。

下图描述了这种交叉验证策略

执行时间序列交叉验证

时间序列模型的交叉验证被认为是最佳实践,但大多数实现非常慢。statsforecast 库将交叉验证实现为分布式操作,从而减少了执行所需的时间。如果您有大型数据集,也可以使用 Ray、Dask 或 Spark 在分布式集群中执行交叉验证。

在本例中,我们希望使用 5 个窗口 (n_windows=5) 评估每个模型的性能,窗口之间步长为 12 个时间单位 (step_size=12)。根据您的计算机情况,此步骤大约需要 1 分钟。

StatsForecast 类中的 cross_validation 方法接受以下参数。

  • df: 训练数据框

  • h (整型): 表示预测未来 h 步。在本例中,预测未来 12 个月。

  • step_size (整型): 每个窗口之间的步长。换句话说:您希望多久运行一次预测过程。

  • n_windows(整型): 用于交叉验证的窗口数。换句话说:您想评估过去多少个预测过程。

crossvalidation_df = sf.cross_validation(df=df,
                                         h=horizon,
                                         step_size=30,
                                         n_windows=5)

crossvaldation_df 对象是一个新的数据框,包含以下列

  • unique_id: 序列标识符
  • ds: 日期戳或时间索引
  • cutoff: n_windows 的最后一个日期戳或时间索引。
  • y: 真实值
  • model: 包含模型名称和拟合值的列。
crossvalidation_df
unique_iddscutoffyADIDA
012020-03-232020-03-16317.0251.901505
112020-03-302020-03-16332.0251.901505
212020-04-062020-03-16306.0251.901505
12212022-12-122022-07-04151.0336.747375
12312022-12-192022-07-0497.0336.747375
12412022-12-262022-07-0442.0336.747375

模型评估

现在我们将根据预测结果评估模型,我们将使用不同类型的指标 MAE、MAPE、MASE、RMSE、SMAPE 来评估准确性。

from functools import partial

import utilsforecast.losses as ufl
from utilsforecast.evaluation import evaluate
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指标ADIDA
01mae114.527585
11mape0.820029
21mase0.874115
31rmse129.749320
41smape0.221878

参考文献

  1. Changquan Huang • Alla Petukhina. Springer 系列 (2022)。使用 Python 进行时间序列分析和预测的实践应用。
  2. Ivan Svetunkov. 使用增强动态自适应模型 (ADAM) 进行预测和分析
  3. James D. Hamilton. 时间序列分析 普林斯顿大学出版社,新泽西州普林斯顿,第一版,1994。
  4. Nixtla 参数.
  5. Pandas 可用频率.
  6. Rob J. Hyndman 和 George Athanasopoulos (2018)。“预测原则与实践 (第 3 版)”.
  7. 季节周期 - Rob J Hyndman.