去年有个创业的朋友找我喝茶,神神秘秘地掏出手机,给我看一个App。他说他用深度学习预测A股,回测年化收益百分之四十,准备辞职全职炒股。我看了他的模型代码,LSTM叠了三层,输入特征包括开盘价、收盘价、成交量、MACD、KDJ、RSI,还有他手动标注的“重大利好事件”哑变量。训练集从2015年到2020年,测试集是2021年上半年,预测准确率高得离谱。
我只问了他一个问题:“你的训练集和测试集是怎么划分的?”
他说:“随机抽样啊,百分之八十训练,百分之二十测试。”
我当时差点把茶喷出来。随机抽样。他把五年的时间序列数据打散了,随机抽百分之八十训练,剩下百分之二十测试。这意味着什么?意味着模型在训练的时候已经看过了2019年的数据,然后去预测2017年的股价。这不是预测,这是作弊。时间序列的因果箭头只能从过去指向未来,你用未来的信息去预测过去,模型当然学得会。在真实场景里,你永远只能用今天之前的数据预测明天之后的事情。
后来他按时间顺序重新划分数据集,前四年训练,最后一年测试。准确率直接腰斩,年化收益变成了负数。他没辞职,App也卸载了。这个故事我讲过很多遍,因为它是时间序列预测里最常见也最致命的错误:你以为你在预测未来,其实你只是在背诵历史。
一、时序问题里的时间陷阱,远不止随机抽样
时间序列数据是带记忆的,这个记忆会让一切看起来更美好,也让一切变得更脆弱。
我做过一个App日活预测的项目。一开始用ARIMA,拟合效果很好,残差白噪声检验也通过了,看着挺美。上线跑了三个月,预测准确率越来越差。查了一个星期才发现问题:我的模型训练数据里包含了一段疫情期间的居家隔离期,那几个月日活暴涨了百分之六十。模型学到的是一个巨大的人造脉冲,它把这个脉冲当成了某种周期性模式。疫情结束后,模型还在试图预测下一个类似的脉冲会出现,每一次预测都偏高。
这种“异常事件污染训练集”的问题,在时间序列里简直无解。你没法把疫情数据当成异常值删掉,因为它代表了真实发生过的事情,而且影响了之后很长时间的趋势。但你保留它,它会把模型的参数估计拉偏。我最后的折中方案是,在训练集里加入一个虚拟变量,标注“是否处于特殊事件期间”,让模型把这部分变异归因到虚拟变量上,而不是学进趋势和季节成分里。效果好了不少,但远远称不上完美。
还有一种更隐蔽的时间陷阱:数据本身的生成机制在变化。你做一个电商平台GMV预测,三年前的数据和三后的数据,背后的用户结构、品类分布、促销策略、竞对格局全变了。你拿三年前的数据训练模型,等于假设数据生成过程是平稳的。这个假设在现实里几乎从来不成立。老数据不仅不能帮助预测,还会拖后腿。我现在的习惯是,训练数据尽量不超过两年的窗口,越近的数据权重越高,必要时直接上遗忘因子,让模型逐渐淡忘远古的记忆。
二、非平稳性,教科书给你讲明白了,但真实数据从不配合
时间序列预测的第一课:平稳性。单位根检验,差分,ACF图,PACF图,一个不能少。这个流程我在学校考过满分,在工作中被虐得满地找牙。
教科书上的例子,差分一次趋势就没了,数据漂漂亮亮变平稳。现实里你拿日活数据做一阶差分,周期性还在,季节性还在,方差还跟着水平一起涨。再做一次季节性差分,十二阶差分之后,序列是平稳了,但你也快不认识它了——原始的业务含义被差分操作洗得一干二净,预测出来的差分值再还原回去,误差会被层层放大,最后给出一个令人啼笑皆非的置信区间:明天日活预测值一百万,下界五十万,上界两百万。运营部门拿着这个区间问我:“所以你的意思是,明天用户可能会来,也可能不来?”
我后来慢慢放弃了对完美平稳性的执念。真实业务场景里,与其花大量精力把非平稳序列强行扭成平稳,不如直接用能处理非平稳特性的模型。Prophet对趋势和季节性的分解很鲁棒,LightGBM直接上滞后特征和时间特征也不怎么怕非平稳,甚至XGBoost加足够的滞后阶数也能硬扛。这些模型的预测能力不一定比严谨的ARIMA差,而且省心太多了。当然,省心是有代价的——放弃了统计推断的严谨性,你没法优雅地给出预测区间和显著性检验。但在老板只需要一个“大概多少”的数字来做预算的时候,优雅不值钱。
三、外部变量,你以为找到了圣杯,其实是打开了潘多拉魔盒
做时序预测的人,都有一个不断膨胀的特征表。先加滞后值,一阶二阶三阶;再加滑动窗口统计量,过去七天均值,过去三十天标准差;然后加日历特征,星期几,是不是周末,是不是月初,是不是节假日。模型效果依然不够好,你开始引入外部变量:天气、竞品动态、广告投放金额、搜索指数、社交媒体声量。
外部变量一加进去,模型精度确实上去了。但在预测场景下,你用外部变量就陷入了逻辑循环:你要预测明天的销量,你需要知道明天的广告投放金额——但明天的投放金额还没确定呢。你要预测下周的日活,你需要下周的天气数据——但气象台的七天预报准确率比你想象的感人得多。你用未来的不确定去预测未来的不确定,不确定性不是变小了,是叠加了。
更现实的做法是,在预测时间点之前能拿到的所有信息,才叫特征。明天的天气你不确定,但你可以在今天拿到“明天天气预报的气温预测值”,这个预测值本身就是可用的特征,即使它不准确,它代表了你做决策时实际掌握的信息。明天的广告投放金额没定,但你可以用“预算计划中的投放金额”作为特征,上线之后再根据实际投放做误差修正。预测模型不是要预知未来,而是要模拟“在当下这个时点,人拿着所有已知信息能做出的最合理的判断”。这个定位一旦想清楚,很多外部变量的使用困境就迎刃而解了。
四、评估指标,越低越好,但越低可能越糟
时间序列预测最常用MAPE,平均绝对百分比误差。MAPE好用,直观,能跨量级比较。但它有两个道德败坏的问题。
第一,真实值是零的时候,MAPE直接爆炸,分母为零。日销量在某些品类某些天确实可能是零,你没法算MAPE,只能跳过或者用SMAPE,但SMAPE在真实值和预测值都接近零的时候也不稳定。第二,MAPE对低估的惩罚比高估轻。真实值一百,你预测一百五,误差百分之五十;真实值一百,你预测五十,误差也是百分之五十。看起来对称对吧?实际不是。供应链场景里,你低估了销量导致缺货,损失远大于高估导致的库存积压。用MAPE当优化目标,模型会倾向于给出保守的低预测,因为这样优化更快,但业务伤害更大。
我后来在很多项目里弃用了MAPE,改用自定义的加权损失函数。高估和低估的惩罚系数不一样,惩罚系数由业务部门根据实际成本来定。比如一个缺货成本是积压成本三倍的场景,损失函数就设成:低估误差权重三,高估误差权重一。这个改动让模型的业务价值直接上了个台阶,预测数量虽然没有更精准,但预测错误的代价大幅下降了。
还有一个几乎没人提但很要命的点:不同时间点的预测误差,业务代价也不一样。你在大促当天的预测误差,和平日一个普通周三的预测误差,能相提并论吗?大促日备货翻车,全年奖金泡汤。我现在的评估体系里,会给关键日期设样本权重,双十一、618、黑色星期五这些大节点的误差,在模型优化时被反复强调,平时的小误差允许稍微放飞一下。这比任何花哨的模型升级都更直接有效。
五、预测的意义,有时候不是预测本身
做了这么多年时间序列,我越来越觉得,预测的最大价值不在于给出那个数字,而在于让你理解系统的动态规律。
你花了两个月搭了一个精确度还可以的销量预测模型,这个过程中你搞清楚了销售的季节性模式、促销的衰减效应、竞对活动的冲击时滞。这些东西写进文档里,业务部门拿去排生产计划、定安全库存、做预算分配,比模型吐出来的那一个数字有用得多。模型会过时,会漂移,会被突发事件打脸,但你对业务节奏的理解,是实打实沉淀下来的资产。
而且很多时候,业务方要的不是精确预测,而是一个合理的范围加上应对方案。你告诉他们下个月销量大概率在一百万到一百二十万之间,如果低于一百万我们启动什么预案,如果高于一百二十万我们怎么调配资源。这个比咬死“下个月销量一百零八万三千二百”有用一百倍。精确到个位数的预测反而会给人一种虚假的安全感,让人放弃准备应急预案。当真实值偏离预测的时候,所有人手忙脚乱。
我现在汇报预测结果的时候,一定会花至少一半的时间讲不确定性。这个预测基于什么假设?哪些假设最容易翻车?翻车了数值会往哪个方向偏?我们能提前观察哪些信号来判断假设还成不成立?这些问题回答清楚了,即使预测数字本身错了,决策者心里也有底,知道接下来该怎么调。
说到底,时间序列预测是一门在不确定中寻找确定边界的活计。你永远猜不准明天中午吃啥,但你知道大概率不会是龙虾。你永远猜不准明天股价涨跌,但你知道在财报发布日和美联储议息日,波动会更大。把能确定的边界画清楚,把不确定的部分诚实地标注出来,剩下的,交给现实去演绎。至于那些宣称自己模型能精准预测股价的,要么在卖课,要么还没睡醒。