1. 这不是又一篇“调包跑通就完事”的时间序列教程
你点开这个标题,大概率已经经历过至少一次这样的循环:搜“时间序列预测”,点进某篇号称“手把手”的教程,跟着复制粘贴几行代码,train_test_split、fit()、predict(),最后画出一条蓝色预测线和一条橙色真实值线——看起来严丝合缝,误差指标RMSE也漂亮得像PPT里的饼图。但当你把模型拿去跑下周的销售数据,或者下个月的服务器CPU使用率,结果要么是预测值平得像条直线,要么是波动剧烈到完全不可信,甚至出现负数销量、负数温度这种物理上根本不可能的结果。我试过七种不同框架、十二个公开数据集、在三个不同业务场景里部署过类似模型,最后发现:90%的时间序列预测失败,根源不在算法本身,而在于我们跳过了“理解数据”这一步,直接冲向了“拟合模型”这个动作。这篇教程不讲LSTM有多深、Transformer有多炫,它只解决一个最朴素的问题:当你拿到一列带时间戳的数字,怎么用最基础的工具,判断它到底适不适合用统计模型来预测?如果适合,该用哪一类方法?如果连数据质量都过不了关,再高级的模型也只是在垃圾进、垃圾出的闭环里自我感动。核心关键词是:Time Series Forecasting Tutorial、平稳性检验、季节性分解、残差诊断、滚动评估、业务可解释性。它适合三类人:刚学完pandas想实战的新手、被老板临时抓壮丁要“搞个预测看趋势”的业务分析师、以及已经用过ARIMA但总被问“为什么选这个参数”的工程师。你不需要会推导Yule-Walker方程,但得知道adfuller返回的p值小于0.05意味着什么;你不用手写GRU单元,但得明白为什么对原始销量数据直接做log变换可能比加一层注意力机制更能提升实际效果。
2. 时间序列预测的本质:不是拟合曲线,而是解构生成机制
2.1 预测失败的真相:我们总在给“非预测性数据”强行套模型
很多人把时间序列预测想象成一个黑箱:输入历史数据,输出未来数值。但现实是,绝大多数业务数据根本不满足经典预测模型的基本假设。我去年帮一家连锁超市优化生鲜损耗预测,他们提供了过去18个月每天的草莓销量。第一眼看上去很“标准”:有明显周末高峰、节假日 spikes、还有夏季销量整体抬升的趋势。但当我用seasonal_decompose做初步分解时,发现了一个致命问题:残差项(Residual)的方差随时间显著增大——6月的销量波动范围是±50公斤,到了12月,这个波动范围扩大到了±200公斤。这意味着什么?意味着数据的“噪声水平”本身就在变化,而ARIMA、Exponential Smoothing这类经典模型,其数学基础默认残差是同方差的(homoscedastic)。强行拟合,模型会把后期更大的波动误判为“新出现的模式”,从而过度调整参数,导致对未来几天的预测严重失真。后来我们改用一种更朴素的方法:先用移动标准差窗口(rolling std)量化波动率变化,再将原始销量除以对应日期的波动率基准值,得到一个“波动率归一化”序列。这个新序列的残差才真正稳定下来,此时再用Holt-Winters拟合,预测准确率(MAPE)从32%直接降到18%。这个案例说明,预测的第一步不是选模型,而是诊断数据是否“可预测”。可预测性(Predictability)不是玄学,它有三个硬性门槛:趋势可分离性、季节性稳定性、残差白噪声性。任何一个不达标,后续所有模型调优都是在沙上筑塔。
2.2 为什么“平稳性”是绕不开的生死线?
教科书里总说“ARIMA模型要求序列平稳”,但很少解释为什么。这里用一个生活化类比:想象你要预测一个正在匀速行驶的汽车下一秒的位置。只要你知道当前速度(一阶差分),就能准确外推。但如果这辆车在急加速、急刹车、甚至原地打转,你光看它过去10秒的位置,根本无法可靠推断第11秒在哪——因为它的运动规律本身就在剧烈变化。时间序列的“平稳性”就是这个道理:它要求序列的统计特性(均值、方差、自相关结构)不随时间推移而系统性改变。非平稳序列就像那辆失控的车,它的“运动规律”本身就是时间的函数。我见过太多人直接对股价日收盘价做ARIMA拟合,结果预测值发散到荒谬的程度。原因很简单:股价序列具有强趋势(drift)和变方差(volatility clustering),它天生就不满足平稳性假设。解决方案不是换更复杂的模型,而是先做“手术”:
- 趋势项处理:对存在明显线性/非线性趋势的数据,不能简单用
diff()一阶差分了事。比如某SaaS公司的月度付费用户数,过去三年呈指数增长。直接一阶差分后,序列仍残留强正相关(因为增长基数越来越大,每月新增绝对值也在涨)。这时应该先用np.log()取对数,再差分,让增长转化为相对增长率,此时序列才真正平稳。 - 季节性项处理:月度销售数据常有12个月周期,但“季节性”不等于“固定周期”。比如某电商的“双十一”效应,其影响强度每年都在变化(2021年峰值是均值的3倍,2022年是3.8倍,2023年是4.2倍)。如果用固定周期12的
seasonal_decompose,会把逐年增强的促销效应错误地吸收到“趋势”里,导致趋势项扭曲。正确做法是用STL(Seasonal and Trend decomposition using Loess)分解,它能自适应季节性强度的变化。 - 单位根检验的实操陷阱:
adfuller检验是金标准,但它有坑。比如样本量太小(<50个点),检验功效极低,容易把实际非平稳序列误判为平稳;反之,样本极大(>1000)时,微小的、业务上可忽略的漂移也会被检出为显著非平稳。我的经验是:p值只是参考,必须结合可视化诊断。画出原始序列、一阶差分序列、以及它们的ACF(自相关函数)图。如果原始序列ACF缓慢衰减(拖尾),而一阶差分后ACF在滞后1阶后迅速截尾,这才是平稳性的铁证。
2.3 季节性不是“有周期就行”,而是“周期模式是否可复现”
很多教程一看到ACF图在滞后12、24、36处有峰值,就断定存在“年度季节性”,然后直接上SARIMA。这是大忌。真正的季节性(Seasonality)必须满足两个条件:周期长度固定、且每个周期内的模式高度相似。举个反例:某共享单车平台的小时级订单量。工作日和周末的模式天差地别——工作日早高峰7-9点、晚高峰17-19点;周末则是全天平缓,午后略高。如果你把整周数据混在一起做ACF,确实能看到24小时周期,但这个“季节性”毫无预测价值,因为模型无法区分今天是周一还是周六。解决方案是分组建模:先用dt.dayofweek提取星期几特征,再为“工作日”和“周末”分别建立独立的小时级预测模型。另一个常见误区是混淆“季节性”和“周期性”(Cyclicity)。经济数据中的“商业周期”(如库存周期、投资周期)长度不固定(可能是3年,也可能是7年),且驱动因素复杂(政策、技术、外部冲击),它不符合统计学中对季节性的精确定义,强行用SARIMA拟合只会产生误导性结果。我的判断流程是:
- 用
plot_seasonal_decompose画出趋势、季节、残差三部分; - 重点观察“季节”子图:各年份的季节曲线是否基本重叠?如果2022年的“春节效应”峰值在1月25日,2023年却移到了1月22日,且形状差异巨大,这就不是稳定季节性,而是事件驱动的脉冲(Impulse),该用事件标记(Event Flag)而非季节项来处理;
- 残差图是否呈现随机散点?如果残差里还藏着明显的锯齿状波动,说明季节分解没到位,需要调整STL的
period参数或robust选项。
3. 从数据诊断到模型落地:一套可复用的六步工作流
3.1 第一步:加载与探索——用三张图锁定核心矛盾
不要一上来就写pd.read_csv()。先明确你的数据源是什么、更新频率如何、缺失值代表什么业务含义。比如IoT传感器数据,缺失可能意味着设备离线;而电商订单数据,缺失更可能是系统漏传。我的标准加载模板如下:
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 加载时强制指定时间索引和数据类型,避免隐式转换错误 df = pd.read_csv('sales_data.csv', parse_dates=['date'], # 明确指定日期列 index_col='date', # 直接设为索引 dtype={'sales': 'float32'}) # 节省内存,float32足够精度 # 关键检查:时间索引是否连续? print("时间索引是否连续:", df.index.is_monotonic_increasing and len(df) == (df.index[-1] - df.index[0]).days + 1) # 业务层面检查:缺失值是否集中在特定时段?(如系统维护期) df['is_missing'] = df['sales'].isna() missing_by_month = df['is_missing'].resample('M').sum() print("每月缺失记录数:\n", missing_by_month[missing_by_month > 0])探索阶段只画三张图,每张图解决一个核心问题:
- 图1:原始序列时序图(Raw Time Series Plot):用
df['sales'].plot(figsize=(12,4))。目标不是看美不美观,而是找“异常段落”。比如某段持续10天的销量为0,是真实业务停摆,还是数据采集故障?如果是后者,不能简单用前向填充(ffill),而应标记为NaN并后续用插值或模型补全。 - 图2:分布直方图+QQ图(Distribution & QQ Plot):
sns.histplot(df['sales'], kde=True)+from scipy import stats; stats.probplot(df['sales'], dist="norm", plot=plt)。目的检验数据是否近似正态。大多数统计模型(如ARIMA)对残差正态性有要求。如果QQ图严重偏离直线(尤其两端),说明存在厚尾(outliers)或偏态(skewness)。此时np.log1p()(log(1+x))通常是比np.log()更安全的选择,因为它能处理零值。 - 图3:自相关图(ACF)与偏自相关图(PACF):
from statsmodels.tsa.stattools import plot_acf, plot_pacf; plot_acf(df['sales'], lags=40); plot_pacf(df['sales'], lags=40)。这是ARIMA参数选择的基石。ACF拖尾、PACF在滞后p阶后截尾 → AR(p);ACF在q阶后截尾、PACF拖尾 → MA(q);两者都拖尾 → ARMA(p,q)。但注意:ACF/PACF图必须在序列平稳后绘制。对非平稳序列画的ACF图,其拖尾是假象,会误导你选择过大的p或q值。
3.2 第二步:诊断与清洗——让数据“说实话”的四个动作
清洗不是为了数据好看,而是为了让模型能“听懂”数据的语言。四个必做动作:
- 动作1:处理缺失值——拒绝盲目填充。对时间序列,线性插值(
interpolate(method='linear'))通常优于前向填充。但有一个例外:当缺失发生在已知业务事件期间(如门店装修停业7天),应将这7天的销量显式设为0,并添加一个is_renovation二元特征列。模型会学习到“装修期销量=0”这个强规则,比任何插值都可靠。 - 动作2:识别并处理异常值——用IQR而非3σ。时间序列的异常值常是脉冲型(spike)或水平位移(level shift)。
scipy.stats.zscore()基于正态假设,在偏态数据上失效。我用改进的IQR法:
这个函数返回一个0/1标记序列,后续可作为特征输入模型,告诉它“此处数据不可信”。def detect_outliers_iqr(series, multiplier=1.5): Q1 = series.quantile(0.25) Q3 = series.quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - multiplier * IQR upper_bound = Q3 + multiplier * IQR # 关键:只标记“孤立”异常点,连续N个点超限视为新稳态 is_outlier = (series < lower_bound) | (series > upper_bound) return is_outlier.replace(False, np.nan).fillna(0).astype(int) - 动作3:平稳化变换——组合拳比单招有效。对既有趋势又有季节性的数据,单一
diff()不够。我的标准流程:- 先
np.log1p()处理右偏和异方差; - 再用
seasonal_decompose(..., model='additive')分解,提取季节项seasonal; - 用
df['sales_log'] - seasonal得到去季节序列; - 对此序列做一阶差分
diff()。
最终得到的序列,才是ARIMA能健康工作的“土壤”。
- 先
- 动作4:创建滞后特征——让模型看见“昨天”。
shift(1)是最基础的,但业务中常需更精细的滞后:df['sales_lag_7'] = df['sales'].shift(7)(上周同日销量,对周季节性关键);df['sales_roll_mean_3'] = df['sales'].rolling(3).mean().shift(1)(前三天平均销量,捕捉短期惯性);df['is_weekend'] = (df.index.dayofweek >= 5).astype(int)(星期几,编码为0/1)。
这些特征不参与模型训练的“核心逻辑”,但能极大提升预测的鲁棒性,尤其在数据突变时。
3.3 第三步:基线模型构建——用最简方案建立能力标尺
在碰LSTM之前,必须先建立一个可靠的基线(Baseline)。没有基线,你根本不知道深度学习模型是真厉害,还是只是花了更多钱买了个更漂亮的幻觉。我的基线模型栈按复杂度递增:
- 基线1:朴素预测(Naive Forecast)——
y_hat = y_t-1。即预测明天销量=今天销量。这是所有时间序列的“零假设”。如果一个复杂模型连这个都 beat 不了,立刻停手。计算其MAPE:np.mean(np.abs((y_true - y_hat) / y_true)) * 100。 - 基线2:季节性朴素(Seasonal Naive)——
y_hat = y_t-m,m为季节周期(如月度数据m=12)。它抓住了最粗粒度的重复模式。 - 基线3:Holt-Winters(Triple Exponential Smoothing)—— 它同时建模水平、趋势、季节三要素,公式透明,训练快,可解释性强。
from statsmodels.tsa.holtwinters import ExponentialSmoothing; model = ExponentialSmoothing(df['sales'], trend='add', seasonal='add', seasonal_periods=12).fit()。它的优势在于:参数α(水平)、β(趋势)、γ(季节)都有明确业务含义,你可以问“为什么预测值上升?是因为β=0.3,趋势权重较高”。 - 基线4:ARIMA(p,d,q)—— 用
pmdarima.auto_arima()自动搜索最优参数。但注意:auto_arima默认用AIC准则,有时会选到过拟合的复杂模型。我强制加约束:max_p=3, max_d=2, max_q=3, max_P=2, max_D=1, max_Q=2, m=12,确保模型简洁。
建立基线的核心目的,是回答一个问题:业务可接受的最低准确率是多少?比如,销售部门说“预测误差超过20%就没法排产”,那么你的所有模型,MAPE必须<20%。如果基线3(Holt-Winters)已经做到15%,那投入两周时间调参LSTM就性价比极低。
3.4 第四步:滚动评估——告别“一次性切分”的虚假繁荣
train_test_split是时间序列预测最大的坑。它把数据随机打乱,破坏了时间依赖性。更糟的是,它用“最后20%数据”做测试,但实际业务中,你永远只能用“截至今天的全部数据”去预测“明天及以后”。所以必须用滚动起源评估(Rolling Origin Evaluation),也叫“时间序列交叉验证”。我的实现方式:
from sklearn.metrics import mean_absolute_percentage_error as mape def rolling_forecast_evaluation(model_func, data, horizon=7, step=1, train_size=365): """ model_func: 接受训练数据返回预测值的函数,如 lambda x: hw_model.fit(x).forecast(horizon) data: 时间序列Series horizon: 预测步长(如预测未来7天) step: 每次滚动步长(如每天滚动一次) train_size: 训练窗口大小(如用过去365天训练) """ results = [] for start in range(len(data) - train_size - horizon + 1): train_end = start + train_size test_start = train_end test_end = test_start + horizon if test_end > len(data): break train_data = data.iloc[start:train_end] test_data = data.iloc[test_start:test_end] try: pred = model_func(train_data) # 确保pred长度与test_data一致 pred = pred[:len(test_data)] error = mape(test_data, pred) results.append({'start_date': train_data.index[-1], 'end_date': test_data.index[-1], 'mape': error}) except Exception as e: results.append({'start_date': train_data.index[-1], 'end_date': test_data.index[-1], 'mape': np.nan, 'error': str(e)}) return pd.DataFrame(results) # 使用示例 hw_forecast = lambda x: ExponentialSmoothing(x, trend='add', seasonal='add', seasonal_periods=12).fit().forecast(7) results_df = rolling_forecast_evaluation(hw_forecast, df['sales'], horizon=7, step=1, train_size=365) print("滚动评估MAPE均值:", results_df['mape'].mean()) print("MAPE标准差:", results_df['mape'].std()) # 标准差大,说明模型稳定性差这个过程模拟了真实业务场景:每天用最新数据重新训练,每天产出新预测。它暴露出单次评估永远看不到的问题:比如某模型在“春节前后”预测极差(MAPE>50%),但在全年平均中被其他时段拉平到20%。滚动评估的MAPE标准差,是比均值更重要的指标——它告诉你模型的可靠性。
3.5 第五步:模型选择与调参——在“可解释性”和“精度”间做务实权衡
当基线模型无法满足业务需求时,才进入模型升级。我的选型决策树基于三个维度:
| 维度 | 低优先级(可牺牲) | 高优先级(必须保障) |
|---|---|---|
| 预测精度(MAPE/RMSE) | 如果业务容忍度宽(如>30%),精度可让位于其他项 | 当业务强依赖(如金融风控),精度是唯一KPI |
| 训练/预测速度 | 离线批量预测可接受分钟级 | 实时API需毫秒级响应,排除LSTM/Transformer |
| 可解释性 | 科研探索可接受黑箱 | 业务方需理解“为什么预测上涨”,否则拒绝上线 |
基于此,我的模型选择路径:
场景1:需要快速上线、业务方要懂原理→Prophet。它由Facebook开源,专为业务时间序列设计。核心优势是:
- 内置节假日效应建模(
holidays参数),一行代码加入春节、国庆等自定义假期; - 趋势变化点(changepoint)自动检测,能捕捉政策调整、新品发布等结构性变化;
- 预测结果天然分解为
trend、seasonality、holidays三部分,可直接向老板汇报“销量上涨20%中,12%来自季节性,5%来自618大促,3%来自新渠道引流”。 - 实操提示:
Prophet对缺失值敏感,务必在fit()前用df.fillna(method='ffill');changepoint_range参数控制变化点搜索范围,设为0.8(即只在最后80%数据中找变化点),避免在早期噪声中误检。
- 内置节假日效应建模(
场景2:数据量大(>10万点)、有强非线性关系→LightGBM/XGBoost。将时间序列转化为监督学习问题:
# 构造特征矩阵:每一行是一个预测样本 def create_features(df, target_col, lags=[1,2,7,14], windows=[3,7,30]): df = df.copy() # 滞后特征 for lag in lags: df[f'{target_col}_lag_{lag}'] = df[target_col].shift(lag) # 滚动统计特征 for window in windows: df[f'{target_col}_roll_mean_{window}'] = df[target_col].rolling(window).mean().shift(1) df[f'{target_col}_roll_std_{window}'] = df[target_col].rolling(window).std().shift(1) # 时间特征 df['dayofweek'] = df.index.dayofweek df['month'] = df.index.month df['dayofyear'] = df.index.dayofyear # 目标变量:预测未来1步 df['target'] = df[target_col].shift(-1) return df features_df = create_features(df, 'sales') X = features_df.drop(['sales', 'target'], axis=1).dropna() y = features_df['target'].dropna() # 对齐X,y索引 X, y = X.align(y, join='inner', axis=0)此方法优势是:能轻松融合外部特征(如天气、竞品价格、营销费用),且训练快、可解释性好(通过
feature_importance知道“昨日销量”比“上周销量”重要3倍)。缺点是:丢失了序列的时序结构信息,对长期依赖建模弱。场景3:精度是唯一目标,且有GPU资源→N-BEATS or N-HiTS。这是目前SOTA(State-of-the-Art)的纯时间序列模型,由Uber提出。它不依赖RNN/CNN,而是用堆叠的全连接层(FC)和可解释的“基础块”(basis block)来分解趋势和季节。相比LSTM,它:
- 训练稳定,不需担心梯度消失/爆炸;
- 预测结果可分解为
trend和seasonality两部分,和Prophet一样可解释; - 开源实现成熟(
darts库),pip install darts即可用。 - 实操提示:
N-BEATS对数据预处理要求高,必须做MinMaxScaler归一化;input_chunk_length(输入长度)建议设为2 * seasonal_period(如月度数据设24),output_chunk_length(预测长度)设为你需要的horizon。
3.6 第六步:部署与监控——让预测模型活在业务里
模型训练完成,只是万里长征第一步。真正的挑战在上线后。我见过太多模型“上线即死亡”:
- 问题1:数据漂移(Data Drift)—— 模型训练时用的是2022年数据,2023年市场环境剧变(如疫情后消费降级),导致输入分布变化。解决方案:部署
Evidently AI或Arize,每日计算新数据与训练数据的PSI(Population Stability Index)。PSI>0.1,触发告警,人工介入分析。 - 问题2:概念漂移(Concept Drift)—— 数据本身没变,但“销量”和“影响因素”的关系变了。比如某款手机,2022年销量主要受“价格”驱动,2023年突然变成“AI拍照功能”成为主因。解决方案:监控模型残差(预测值-真实值)的均值和标准差。如果残差均值持续>0(系统性高估)或<-0.05(系统性低估),且伴随标准差增大,大概率发生概念漂移,需重新训练。
- 问题3:业务逻辑变更未同步—— 比如公司上线新会员体系,老用户复购率提升,但模型还在用旧系数。解决方案:建立“特征版本管理”(Feature Store),每次业务规则变更,都生成新特征版本,并标注
valid_from和valid_to时间戳。模型预测时,自动加载对应时间戳的特征版本。
最后,交付物不是.pkl模型文件,而是一份《预测服务说明书》,包含:
- 输入规范:API接收的JSON格式,字段名、类型、取值范围(如
"date": "2023-10-01", "sales": 125.6); - 输出承诺:SLA(Service Level Agreement),如“99%请求在200ms内返回,预测误差MAPE<25%”;
- 降级方案:当模型服务不可用时,自动切换至Holt-Winters基线模型,保证业务不中断;
- 联系人:谁负责监控、谁有权触发重训练、谁审批新特征上线。
4. 避坑指南:那些只有踩过才知道的“幽灵陷阱”
4.1 “完美拟合”的幻觉:过拟合在时间序列中更隐蔽
在图像分类中,过拟合表现为训练集准确率99%、测试集70%。但在时间序列里,它更狡猾:训练集和测试集的误差都很好看,但上线后一塌糊涂。原因在于,传统测试集是“未来一段连续时间”,而真实世界是“每天一个新点”。模型可能记住了测试集那段特定的模式(比如某次促销的精确波形),而不是学会了泛化规律。我的检测方法:
- 多起点滚动评估:不只用一个起始点(如2023-01-01),而是用2023-01-01、2023-02-01、2023-03-01三个起点分别做滚动评估。如果三个起点的MAPE标准差>5%,说明模型对起始点敏感,存在过拟合风险。
- 扰动测试(Perturbation Test):对测试集数据,随机挑选5%的点,将其值替换为均值±2倍标准差的随机数(模拟数据异常),然后看预测误差增幅。如果误差增幅>100%,说明模型鲁棒性差,对噪声极度敏感。
4.2 特征工程的“暗礁”:时间特征编码的致命错误
新手最爱犯的错:把date列直接拆成year、month、day三个数字特征喂给模型。这会导致灾难性后果。month=12(12月)和month=1(1月)在数值上相差11,但业务上它们是相邻的(12月之后是1月)。模型会认为12月和1月“天差地别”,而忽略了它们的周期性邻近关系。正确做法是循环编码(Cyclical Encoding):
# 将month编码为sin/cos,保留周期性 df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12) df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12) # 同理处理hour, dayofweek等 df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)这样,12月(sin=0, cos=1)和1月(sin=0.5, cos=0.866)在特征空间中距离很近,模型能自然学习到“年末年初”的连续性。
4.3 评估指标的“温柔陷阱”:MAPE在零值附近的崩溃
MAPE(Mean Absolute Percentage Error)公式是|y_true - y_pred| / y_true。当y_true接近0时(如某天销量为2件),分母极小,整个项爆炸。我曾遇到一个案例:某SKU在淡季日均销量3件,模型预测为0,MAPE=100%;但另一天真实销量0件(缺货),模型预测为1件,MAPE=∞(无穷大),直接让整个评估集的平均MAPE失真。解决方案:
- 用sMAPE(Symmetric MAPE)替代:
2 * |y_true - y_pred| / (|y_true| + |y_pred|),分母永不为零; - 业务过滤:在评估前,剔除
y_true < threshold(如threshold=5)的样本,因为这些低销量SKU本就不适合用统计模型预测,应归入“长尾品类”,用规则(如“上月销量*0.8”)处理; - 分层评估:将SKU按销量分为“头部(Top 20%)”、“腰部(Middle 60%)”、“长尾(Bottom 20%)”,分别报告MAPE。业务方最关心头部SKU的预测,长尾的误差可以容忍更高。
4.4 模型更新的“温水煮青蛙”:不重训的代价
很多团队上线模型后,就再也不碰了。他们觉得“模型在测试集表现稳定,何必折腾”。这是巨大的认知偏差。时间序列模型不是静态的,它是对“当前业务状态”的快照。我的经验是:
- 高频更新(每日/每周):适用于促销、直播带货等强外部驱动场景。某美妆品牌,每次李佳琦直播后,相关SKU销量模式永久改变,必须直播次日就重训模型。
- 中频更新(每月):适用于常规销售、订阅服务。每月1号,用上月全量数据重训,覆盖季节性变化。
- 低频更新(每季):适用于宏观指标(如GDP、CPI),变化缓慢。
关键不是频率本身,而是建立更新触发器: - 数据漂移检测(PSI>0.1);
- 残差监控(连续3天残差均值>0.1);
- 业务事件(如新品发布、渠道关闭);
- 固定日历(每月1号)。
没有触发器的更新,就是拍脑袋;有触发器的更新,才是工程化。
5. 实战复盘:一个从0到上线的完整项目切片
5.1 项目背景:某区域电网的负荷预测
客户诉求:“预测未来24小时每15分钟的用电负荷,用于发电调度,误差需控制在±5%以内。”数据:2021-2023年,每15分钟一个点,共约35万条记录。初始探索发现:
- 强日周期(96点/天)、强周周期(672点/周);
- 夏季负荷峰值比冬季高40%,但日波动幅度夏季更大;
- 周末负荷曲线与工作日完全不同,且周五晚高峰延迟2小时。
5.2 我的六步执行实录
Step 1:诊断
- ACF图显示滞后96、192、288处有峰值,确认日周期;滞后672处也有峰,但较弱,说明周周期存在但不主导;
- STL分解后,残差图显示夏季残差方差是冬季的2.3倍,证实“变方差”问题;
- QQ图显示右偏,log变换后改善明显。
Step 2:清洗
- 缺失值:共127个点,集中在2022年7月某次区域性停电,全部设为0,并添加
is_outage特征; - 异常值:用IQR法检测到3个脉冲(雷击导致瞬时负荷飙升),标记为
is_surge; - 平稳化:
np.log1p(sales)→ STL分解去日季节 → 差分去趋势。
Step 3:基线
- 朴素预测(Naive)MAPE=8.2%;
- 季节性朴素(Lag-96)MAPE=6.7%;
- Holt-Winters(周期96)MAPE=5.3%;
- ARIMA(1,1,1)(1,1,1)[96] MAPE=