时间序列预测入门到实战(一)· 开篇:时间序列预测到底难在哪
本文是「码海寻道」《时间序列预测入门到实战》系列的第 1 篇。整个系列会从 ARIMA 一路讲到时序大模型,覆盖统计、机器学习、深度学习、大模型四个时代。这一篇不讲任何具体模型,只做两件事:把"时序为什么难"讲清楚,以及立下贯穿全系列的规矩——一个统一的数据集和一套统一的评估框架。后面每一篇的模型,都会在同一份数据、同一套指标上跑,让你能横向对比"同一个问题,16 种解法各自的精度和成本"。
一个让人栽跟头的场景
你用 XGBoost 做过用户流失预测、点击率预估、风控评分,效果都不错。有一天业务找过来:帮忙预测下个月每天的服务器负载吧,好提前扩容。
你很自然地把它当成一个回归问题:把日期拆成年、月、日、星期几当特征,把 CPU 负载当标签,train_test_split一切,XGBoost 一跑,验证集 R² 高达 0.95。你很满意地上线了。
然后线上预测惨不忍睹。
问题出在哪?不是模型不行,是你从一开始就用错了范式。train_test_split默认会把数据打乱随机切分——于是你的模型在训练时"见过"了 5 月 20 日之后的数据,再回头去预测 5 月 15 日。验证集 0.95 是用未来预测过去刷出来的假象,这在时序里有个专门的名字:数据泄露(data leakage)。
时间序列预测的第一课,就是先忘掉一部分你在普通监督学习里养成的习惯。
一、时序预测 vs 普通回归,本质差在哪
普通监督学习有一条几乎不言自明的假设:样本独立同分布(i.i.d.)。每一行数据是独立的一次观测,行与行之间没关系,所以你可以随便打乱、随便切分、随便做交叉验证。
时间序列把这条假设彻底打破了:
| 普通回归 | 时间序列 | |
|---|---|---|
| 样本关系 | 相互独立 | 顺序即信息,前后强相关 |
| 打乱数据 | 无所谓 | 直接毁掉信号 |
| X 和 y | 边界清晰 | 模糊——过去的 y 就是未来的 X |
| 划分数据集 | 随机切分 | 必须按时间切 |
| 目标 | 拟合 X→y 的映射 | 建模"历史如何影响未来" |
其中最要命、也最常被工程师忽视的一条是数据集划分。时序里训练集必须严格在测试集之前,否则就是拿未来的信息训练、回头预测过去,指标虚高,上线打脸。
fromsklearn.model_selectionimporttrain_test_split# ❌ 错误:随机打乱,未来数据泄露进训练集X_tr,X_te,y_tr,y_te=train_test_split(X,y,test_size=0.2,shuffle=True)# ✅ 正确:按时间顺序切,训练集永远在测试集之前n=len(df)train=df.iloc[:int(n*0.7)]val=df.iloc[int(n*0.7):int(n*0.85)]test=df.iloc[int(n*0.85):]记住这条铁律:时序里任何形式的随机打乱、随机 K 折交叉验证,默认都是错的。(后面讲评估时我们会介绍时序专用的"滚动回测",那才是正确姿势。)
二、时序到底难在哪:三大挑战
如果只是"不能打乱",那还不算难。真正让时间序列成为一个独立研究领域的,是下面三个特性。
挑战 1:时间依赖(自相关)
今天的值和昨天的值高度相关,昨天又和前天相关——这叫自相关。它带来一个普通回归没有的麻烦:误差会沿时间传递并累积。
预测未来 1 步通常不难,难的是预测未来 30 步。因为第 2 步的输入里往往包含了第 1 步的预测值,第 1 步错一点,第 2 步在错误的基础上继续错,误差像滚雪球一样放大。这就是为什么后面我们会专门区分"单步预测"和"多步预测",以及为什么多步预测有好几种不同的策略。
挑战 2:非平稳(分布随时间漂移)
平稳大致是指:序列的统计特性(均值、方差、自相关结构)不随时间改变。现实世界的数据几乎都是非平稳的:
- 有趋势:用户量逐年增长,均值在漂移;
- 有方差变化:促销季波动剧烈,平时很平;
- 有结构突变:一次改版、一场疫情,整个规律都变了。
非平稳的致命之处在于:你在训练集上学到的分布,到了测试集可能已经不成立了。这也是为什么传统统计方法(如 ARIMA)第一步往往是"做差分把序列变平稳",以及为什么"概念漂移"在时序里是个绕不开的话题。
挑战 3:季节性与周期
用电量白天高、深夜低(日周期);工作日高、周末低(周周期);夏冬两季空调开满(年周期)。这些周期还经常叠加在一起——一条真实的用电量曲线,往往同时含有日、周、年三个周期。
季节性本身不难,难的是它和趋势、噪声混在一起,你得先把它们拆开才能看清。这正是下一篇要讲的"时序分解",也是 Prophet、TimesNet 等模型的核心思想来源。
一句话总结:自相关让误差累积,非平稳让分布失效,季节性让信号纠缠。三者叠加,才有了时间序列这门学问。
用几行代码,把三个挑战看在眼里
空口无凭,这三个挑战全都能直接画出来。正式的数据体检留到下一篇,这里先用几行代码建立直觉:
importmatplotlib.pyplotaspltfromstatsmodels.graphics.tsaplotsimportplot_acf s=df["OT"]# 挑战②·非平稳:30 天滚动均值/标准差在漂移,说明分布不稳定roll=s.rolling(24*30)roll.mean().plot(label="滚动均值")roll.std().plot(label="滚动标准差")plt.legend();plt.title("非平稳:统计量随时间漂移");plt.show()# 挑战③·季节性:按小时聚合看日周期,按星期聚合看周周期s.groupby(s.index.hour).mean().plot(marker="o",title="日周期:24 小时内的规律")plt.show()s.groupby(s.index.dayofweek).mean().plot(marker="o",title="周周期:周一到周日")plt.show()# 挑战①·自相关:ACF 一眼看出“过去影响未来”能延伸多远plot_acf(s,lags=72)# 看 72 小时内的自相关plt.show()跑完你会看到:ACF 在滞后 24、48 小时处规律性地冒出尖峰——这正是"今天几点"和"昨天、前天同一时刻"强相关的铁证。三大挑战不是抽象概念,它们明明白白写在数据里。
三、动手之前:先想清楚你要解决哪类问题
拿到一个时序任务,别急着套模型。先用三个维度给它定位——这直接决定你该选什么模型、怎么评估。
维度 1:单步 vs 多步
- 单步:只预测下一个时刻(下一小时的负载)。
- 多步:预测未来 H 个时刻(未来 7 天每天的负载)。H 叫预测长度 / horizon。
多步预测要难得多,光"怎么一次预测多个点"就有递归、直接、多输出三种策略,我们会在讲机器学习和深度学习时反复碰到它。
维度 2:单变量 vs 多变量
- 单变量:只用目标序列自己的历史(只看负载的过去预测负载的未来)。
- 多变量:还引入其他协变量(温度、是否节假日、促销计划……)。
这里有个容易被忽略的细分:协变量又分过去才知道的(如实际气温)和未来已知的(如下周的促销日历、法定节假日)。能不能把"未来已知信息"喂进模型,是很多模型的分水岭——这正是后面 TFT 那一篇的主角。
维度 3:点预测 vs 概率预测
- 点预测:给一个数——“明天负载 8000”。
- 概率预测:给一个区间或分布——“明天负载有 90% 概率落在 7000~9000”。
工业界里,区间往往比点更有用。做容量规划时,你要的是"最坏情况下要准备多少资源",一个光秃秃的点估计给不了这个答案。DeepAR、TFT 这些模型之所以重要,很大程度就是因为它们输出的是分布而非点。
四、立规矩:贯穿全系列的数据集与评估框架
这是本篇最"实"的部分,也是整个系列的地基。从下一篇开始,所有模型都在这套框架上跑。
4.1 数据集:ETT(电力变压器温度)
我们全程使用公开数据集ETT(Electricity Transformer Temperature),具体用小时级的ETTh1。选它的理由很实在:
- 公开可下载,来自论文 Informer(AAAI 2021),GitHub 仓库一键获取;
- 规模适中:约 2 年、17420 个小时级采样点,笔记本就能跑;
- 周期丰富:目标列油温
OT有清晰的日周期和年周期,适合演示季节性; - 单变量、多变量都能做:除
OT外还有 6 个负荷相关的协变量(HUFL、HULL、MUFL等),想做单变量就只用OT,想做多变量就把 6 个负荷一起喂进去; - 是学术 benchmark:后面讲 Informer、Autoformer、PatchTST、时序大模型时,我们跑出来的数字能直接和论文对照,心里有底。
importpandasaspd# ETTh1.csv 可从 https://github.com/zhouhaoyi/ETDataset 下载df=pd.read_csv("ETTh1.csv",parse_dates=["date"]).set_index("date")print(df.shape)# (17420, 7)print(df.columns.tolist())# ['HUFL', 'HULL', 'MUFL', 'MULL', 'LUFL', 'LULL', 'OT']# 我们的预测目标是 OT(油温)统一的切分方式(按时间,绝不打乱):
n=len(df)train=df.iloc[:int(n*0.6)]# 前 60% 训练val=df.iloc[int(n*0.6):int(n*0.8)]# 中间 20% 验证test=df.iloc[int(n*0.8):]# 最后 20% 测试先画一眼,对数据有个感性认识——能明显看到一天一个来回的日周期:
importmatplotlib.pyplotasplt df["OT"].iloc[:24*14].plot(figsize=(12,3))# 前 14 天plt.title("ETTh1 · 油温 OT 前 14 天");plt.ylabel("温度")plt.tight_layout();plt.show()4.2 评估指标:四个数字,各有脾气
我们统一汇报四个指标。它们不是越多越好,而是各自照亮一个侧面:
- MAE(平均绝对误差):最直观,和原数据同量纲,"平均差 200 度"一句话就懂。
- RMSE(均方根误差):对大误差惩罚更重(先平方),你在意"偶尔错得离谱"就看它。
- MAPE(平均绝对百分比误差):相对误差,跨量级可比。但有坑:真实值接近 0 时会爆炸,有 0 值时直接除零。
- sMAPE(对称 MAPE):用预测和真实的均值做分母,缓解了 MAPE 的除零和不对称问题。
一个可复用的评估函数,后面每篇import它就行:
importnumpyasnpdefevaluate(y_true,y_pred):y_true,y_pred=np.asarray(y_true),np.asarray(y_pred)err=y_pred-y_true mae=np.mean(np.abs(err))rmse=np.sqrt(np.mean(err**2))# MAPE:屏蔽真实值接近 0 的点,避免除零爆炸mask=np.abs(y_true)>1e-6mape=np.mean(np.abs(err[mask]/y_true[mask]))*100# sMAPE:分母用 |真实|+|预测|,天然规避除零smape=np.mean(2*np.abs(err)/(np.abs(y_true)+np.abs(y_pred)+1e-6))*100return{"MAE":mae,"RMSE":rmse,"MAPE(%)":mape,"sMAPE(%)":smape}选指标的经验:同量纲看 MAE/RMSE,跨序列比较看 MAPE/sMAPE;数据里有 0 或接近 0 的值,果断放弃 MAPE 改用 sMAPE 或 MAE。
4.3 一定要有的基准线:别一上来就上大模型
这是整个系列我最想让你养成的习惯:任何模型,都要先和一个"naive 基准"比。如果你的深度学习模型累死累活还打不过"照抄上一个值",那它就没有存在的意义。
最朴素的基准有两个,都不需要训练:**Naive(持续性)**用最后一个值预测未来所有点(“下一刻和此刻一样”);**Seasonal Naive(季节性)**用上一个周期同相位的值(“这个周二和上个周二一样”)。哪个更强?不能想当然,必须实测——很多人默认"有周期就该用 Seasonal Naive",但这得看数据的脾气。
defnaive(series,horizon):"""Persistence:用最后一个观测值预测未来所有点,最朴素的基准。"""returnnp.full(horizon,series[-1])defseasonal_naive(series,horizon,period=24):"""用一个周期前同相位的值预测。period=24 即日周期(小时级数据)。"""hist=series[-period:]returnnp.array([hist[i%period]foriinrange(horizon)])# 端到端:在 test 上同时评估两个基准线,看差距有多大target=test["OT"].values period,horizon=24,24history=target[:period]# 用“昨天”这 24 小时y_true=target[period:period+horizon]# 预测“今天”这 24 小时forname,y_predin[("Naive",naive(history,horizon)),("SeasonalNaive",seasonal_naive(history,horizon,period)),]:print(f"{name:14s}",evaluate(y_true,y_pred))上面只是单个窗口,偶然性很大。真正该做的是在整个测试集上滚动评估——用一个个不重叠的 24 小时窗口逐段预测再汇总(完整的滚动回测第 16 篇细讲)。在 ETTh1 上滚动 145 个窗口,真实结果是:
| 基准 | MAE | RMSE | sMAPE |
|---|---|---|---|
| Naive | 1.67 | 2.24 | 26.1% |
| Seasonal Naive | 1.71 | 2.24 | 30.5% |
意外吧?Naive 反而赢了 Seasonal Naive。因为 ETTh1 的油温是缓变量、日周期很弱,"和上一小时一样"比"和昨天此刻一样"更靠谱。这上了宝贵的一课:基准线必须实测,别被"有周期就用季节基准"的直觉骗了。
所以本系列的及格线,就取两者中更强的Naive:MAE 1.67。你会惊讶于它有多难打败——下一篇 ARIMA 就来试试,看能不能撬动这个看似平平无奇的数字。
全系列路线图
本篇建立了认知、立好了规矩,接下来我们沿着"方法演化史"往下走:
| 阶段 | 篇目 | 关键词 |
|---|---|---|
| 打地基 | 2 | 平稳性、季节性分解 |
| 统计方法 | 3–4 | ARIMA、指数平滑 |
| 机器学习 | 5–7 | 特征工程、LightGBM、Prophet |
| 深度学习 | 8–14 | LSTM、TCN/TimesNet、Transformer 家族、DLinear/PatchTST/iTransformer、N-BEATS/N-HiTS、DeepAR、TFT |
| 大模型 | 15 | TimesFM、Chronos、Moirai |
| 收官 | 16 | 选型决策树 + 工程落地 |
小结
- 时序预测不是普通回归:样本不独立,绝不能随机打乱、随机切分,否则数据泄露、指标虚高。
- 它难在三点:自相关让误差累积、非平稳让分布失效、季节性让信号纠缠。
- 动手前先给任务定位:单步/多步、单变量/多变量、点预测/概率预测——三个维度决定选型。
- 全系列立了规矩:数据集用ETTh1,指标用MAE/RMSE/MAPE/sMAPE,并且任何模型都要先打败 Naive 基准线(MAE 1.67)。
下一篇,我们给数据做一次"体检":如何判断平稳性、如何用 STL 把一条曲线拆成趋势、季节和残差——这是所有时序建模的第一步。
思考题:为什么"随机 K 折交叉验证"在时序里是错的,而"滚动回测"是对的?如果你已经有答案,那你已经理解了本篇的一半。
看到这里,说明你是认真的。关注公众号「码海寻道」,我会把这 16 篇一路更完——从 ARIMA 到时序大模型,用同一份数据、同一套指标,带你把时间序列彻底走一遍。下一篇见。