做数据分析这几年,我被问得最多的一句话就是:能不能帮我预测一下明天的数据?换成疫情相关的场景,就变成“能不能预测一下某传染病的走势”。说实话,这种问题听起来很宏大,但落到技术层面,很多人第一步想到的就是跑个线性回归试试。我最初接手这个“线性回归:新冠病毒的预测”风格的项目时,也是从最基础的模型开始摸起。今天这篇文章,我不打算空谈理论,而是把从数据清洗、特征工程、模型训练到踩坑复盘的全过程摊开来,给同样在学机器学习、想拿真实场景练手的朋友当参考。你不需要多强的数学基础,只要会用Python、看过一点sklearn的文档,就能跟上我的节奏。
线性回归是个老生常谈的模型,但真拿去预测疫情数据,你会发现教科书里没写的一堆门道。比如数据怎么切分、特征怎么造、为什么直接拟合原始曲线会崩、怎么处理拐点、评估指标到底看哪个,这些都是我在实际项目里摸出来的经验。文章会按照实操流程走一遍,最后单独整理一份避坑清单,全是血泪教训,能帮你少走大半个月的弯路。
1. 项目背景与线性回归的适配性分析
1.1 疫情数据的特点决定了建模方式
疫情数据本质上是一串时间序列,常见的字段就三样:日期、某个区域的累计感染人数、当天的报告新增人数。看上去简单,但它和普通的销售数据、房价数据不一样,有几个很明显的特征。
第一,它是按时间顺序记录的,前后强相关。今天的新增人数会影响明天的累计值,这种自相关性会让普通线性回归的残差不满足独立性假设,如果你不管不顾直接拟合,很容易得到“看起来很完美但完全没用”的模型。
第二,疫情数据是非平稳的。在早期阶段,感染人数往往呈指数式上升,曲线是个J型或者更陡的形态,直接拿线性回归去拟合原始数值,误差会大到没法看。教科书不会提醒你这一点,但真实拿数据跑一遍,你会发现预测值在后期崩得不成样子。
第三,它受外部干预影响特别大。比如某段时间加强了检测、改变了统计口径、采取了隔离措施,数据的生成机制就变了。这些干预因素如果不加进模型里,线性回归就会把曾经的趋势无限外推,结果自然失真。
我用一个生活化类比来解释:疫情数据就像开车时的速度表,前段是加速阶段,中段可能匀速,后段也许刹车。线性回归只会盲人摸象一样画一条平均线,根本不管你中间踩过几次刹车。
1.2 为什么第一版方案仍然选线性回归
既然疫情数据这么难搞,为什么我不一开始就用LSTM、Prophet、梯度提升树这些“高级货”?因为做项目有个原则:先用最简单可解释的模型打底,拿到一条基准线,再决定要不要上复杂模型。
线性回归在这个场景里有三个不可替代的优势。
一是可解释性强。你拟合完之后能直接看到系数,能明确说出“日期每增加一天,累计感染人数大约增加多少”。这个解释性在给非技术背景的汇报对象同步结论时特别值钱,对方不需要听懂梯度,只需要看懂一条趋势线和几个数字。
二是训练成本低。以当前的时间序列数据量级,线性回归基本是毫秒级出结果,写代码也只需要十几行。你可以很轻松地做滚动预测、调整特征、对比不同窗口期,反复试错不需要等训练时间。
三是它能帮你暴露问题。上复杂模型之前,先用线性回归摸一遍数据,能发现数据质量差、特征缺失、切分方式错误这些隐藏问题。很多情况下,根本不是模型先进不先进的问题,而是你的数据根本喂不进高级模型。
所以我的技术选型思路是这样的:第一版用线性回归作为基线,把数据工程和评估体系的流程跑通,后续再迭代更复杂的模型。这个思路放到任何预测类项目里都通用。
2. 数据准备与特征工程:决定成败的隐藏工程
2.1 数据从哪里来、长什么样
我这次的项目没有去抓真实世界的最新数据,而是用了一份模拟的公开格式数据集,原因很简单:方便复现,也避免学的时候被真实数据的噪声带了节奏。不过整理数据的方式和真实场景完全一致。
先说字段结构。典型的数据表一般有这几列:日期、区域编号、累计确诊数、每日新增确诊数、是否节假日、是否为加强管控阶段标记。后面两列需要你自己想办法构造,原始数据通常不会给现成的特征,这恰恰是特征工程的意义。
有一点必须提醒:日期格式要统一,最好转成datetime类型。很多初学者拿到的表格里日期是字符串,没法直接参与数值计算。我在处理的时候第一件事就是:
import pandas as pd df = pd.read_csv('simulated_data.csv', parse_dates=['date']) df = df.sort_values('date').reset_index(drop=True)排序这步尤其别省。时间序列最怕乱序,万一原始数据是按区域汇总的,日期排列是跳跃的,模型学到的规律就全乱了。
2.2 缺失值和异常值怎么处理
疫情报告数据最让人头疼的其实就是缺失和异常。比如某天数据没更新,对应的累计确诊数可能是空值;又比如某天突然出现一个负值,明显是口径错误。
我采用的策略是:累计值缺失用插值法填充,新增值缺失用前后平均,异常值则直接标记成缺失再补。所谓插值法,简单说就是在缺失点前后各取一个正常值,拉一条线把你缺的点补在中间。
这里有几个判断准则供你参考:
- 如果累计值某天比后一天还大,说明数据录入倒置,需要修正顺序或剔除。
- 如果新增值连续多天为零,且之前明显处于上升期,八成不是真实归零,而是漏报。
- 节假日和周末的检测量减少,会导致新增出现周期性下跌,这种波动不能当作异常值处理。
表层的脏数据清理干净之后,你才有资格谈建模。很多模型效果差,根本不是模型问题,而是数据都没收拾利索。
2.3 构造特征:模型能不能学好就看这一步
线性回归的表达式很简单,y = a1*x1 + a2*x2 + ... + b,但x从哪来,直接决定预测效果。我这次共构造了三类特征。
第一类叫时间趋势特征。最简单就是把日期转换成数值序号,从第0天开始递增。这条特征让模型能捕捉到“随着时间推移,累计量整体在上升还是下降”的趋势,是线性回归的主力特征。
第二类叫周期性特征。疫情的七日周期波动经常出现,因为检测和报告节奏受周末影响。为了向模型表达“今天是周几”这个信息,我生成了一组星期哑变量。如果你只有一个“是否周末”字段,也是可以的,哑变量只是更细一些。
第三类叫干预事件特征。前面说过,外部干预会改变数据生成机制,所以我构造了一个布尔型字段:从某个日期之后标记为1,之前为0。加入这个特征后,回归系数就能体现“进入下一阶段后,斜率出现了多大偏移”,模型可以拟合出分段式的趋势。
特征不是越多越好。我一开始把区域编号的独热编码也堆进去了,结果模型复杂了,解释性却变差。特征工程的原则是每加一个特征都要有业务解释,否则宁可不要。
2.4 训练集和测试集切分:时序数据不是随机抽牌
普通的机器学习项目,用train_test_split随机切分训练集和测试集完全没问题,但时间序列数据不能这么做。原因在于时间序列的先后顺序是信息本身,随机切分等于把未来数据泄漏给了过去,测试结果会虚高得离谱。
我用的是按时间切分:
split_idx = int(len(df) * 0.8) train = df.iloc[:split_idx] test = df.iloc[split_idx:]前80%的数据用来训练,后20%用来验证。这样模型在训练时完全没见过“未来”,评估结果才贴近真实应用。这是时间序列建模的一个基本常识,但实际踩坑的人特别多。
3. 模型构建与核心实现:从原理到代码一气呵成
3.1 最小二乘法到底在做什么
线性回归的参数求解方法叫最小二乘法。听着高深,其实直觉很简单:你拿尺子在一堆散点中间画一条直线,这条线到所有点的竖直距离之和(误差平方和)最小,那这条线就是回归线。
写成人话就是:
- 目标是找到这么一组系数a和b,让 \sum (真实值 - 预测值)^2 尽量小。
- 因为误差平方函数是一个凸函数,存在全局最小点,数学家推导出了闭式解,可以直接算出来。
我在实际讲解的时候喜欢用射箭来类比。每一箭就是模型的一次预测,箭射出去离靶心有的偏左有的偏右,你要调整姿势,最终目标是所有箭离靶心的总距离最小。最小二乘法做的就是这个“调整姿势”的数学过程,它解出来的参数就是当前数据下最好的姿势。
3.2 完整建模代码实战
我用的库是sklearn,完整流程可以浓缩成下面这个代码块:
import numpy as np from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score X_train = train[['time_index', 'is_weekend', 'intervention']] y_train = train['cumulative_cases'] X_test = test[['time_index', 'is_weekend', 'intervention']] y_test = test['cumulative_cases'] model = LinearRegression() model.fit(X_train, y_train) print("回归系数:", model.coef_) print("截距:", model.intercept_) y_pred = model.predict(X_test) print("测试集MSE:", mean_squared_error(y_test, y_pred)) print("测试集R2:", r2_score(y_test, y_pred))这段代码看起来简单,但里面有几个细节值得展开。
第一,特征矩阵X必须是一个二维结构,哪怕你只有一个特征,也要写成DataFrame或二维数组的形状。很多新手在这一步直接用Series,sklearn直接报错,实际上是维度问题。
第二,fit和predict一定分开调。训练和预测是两件事,拿训练集fit完之后,要拿着求到的参数在测试集上predict,这个过程模拟的是“用历史规律预测未来”。
第三,回归系数要逐个看。如果时间趋势的系数是正的,说明整体还在上升期,后续预测出来的累计值也会持续走高,这是线性回归的外推本性。
3.3 对数变换:把指数曲线拉成直线
前面说过疫情早期数据近似指数增长。指数增长的曲线是什么样的?每天以固定比例往上翻。这种形态下,累计值的变化率不是恒定的,而是随着基数变大变得越来越快,用直线去拟合指数曲线就非常勉强。
解决办法是做一个对数变换:把预测目标从“累计病例数”变成“累计病例数的自然对数”。
变换之后呈现在你面前的就不再是J型曲线,而是一条近似直线。这时再做线性回归,拟合效果会明显改善。代码上的差别就一行:
model.fit(X_train, np.log(y_train)) pred_log = model.predict(X_test) y_pred = np.exp(pred_log)注意,拟合的是对数,预测出来的值也是对数尺度,要恢复成原始数值,必须做一次指数变换。
我用自己的模拟数据试过,不取对数直接拟合,测试集的R²可能只有0.85,看着不算差,但后段的绝对偏差已经大到没法接受。取对数之后,残差规律明显改善,尤其是前半段的预测和真实值几乎能贴上。
需要提醒的是,对数变换不是万能的。一旦数据出现明显拐点,比如疫情进入下行期,对数值就不再线性了,这时候继续用线性模型拟合,效果也会崩。所以变换是手段,不是原则,用不用要结合具体阶段。
3.4 多变量回归与分段斜率
当我把干预标记加进模型后,回归输出的截距和系数就变成了分段式的表达。具体来说,干预特征是一个0和1组成的列,在干预前它是0,干预后它是1。回归系数会算出两个斜率:
- 干预前,时间趋势的系数对应第一阶段的增长斜率。
- 干预后,时间趋势的系数加干预系数的加总,对应第二阶段的增长斜率。
你可能还会碰到更复杂的情况,比如数据出现了多个拐点,需要设置多段干预标记。这时模型会变得更灵活,但也要小心过拟合,特征越多,模型越容易记住训练集上的偶然波动。
4. 模型评估与优化迭代:别被R²骗了
4.1 R²和MSE哪个更重要
很多教程把R²(拟合优度)当作头号指标,但我在实际评估时会把MSE(均方误差)也拉出来一起看。原因是R²描述的是模型解释了多少方差,而MSE给出的是误差的绝对值量级。两者结合你才能判断模型是“相对好用”还是“绝对好用”。
如果R²很高、MSE也很高,说明模型对趋势的拟合不错,但预测值和真实值之间还有系统性偏差。如果R²低、MSE也低,说明数据波动本来就小,但模型没抓到重点。最理想的状态是两者都展现出合理水平。
另外给个经验值:对于时间序列外推模型,宣称R²达到0.99以上时要非常警惕。越高的R²越可能是过拟合,或是评估方式出了问题,比如错误地用了随机切分导致数据泄漏。预测类项目的核心是“输出在未来依然有效”,而不是“训练集上漂亮”。
4.2 滚动预测:更真实的实战模拟
用固定切分训练测试集只是第一层评估。更接近真实使用的评估方法是滚动预测。思路是:
- 第一次训练用第0到第90天的数据,预测第91到第100天。
- 然后真实数据来到第100天,你要把训练窗口滚到第100天,重新拟合模型,预测第101到第110天。
- 如此往复,每次预测时用的都是当时能够得到的最新历史数据。
这样做的最大价值是模拟了真实的决策场景。你用模型预测未来,未来来到之后历史窗口会变长,模型理应重新训练。如果你的模型只能在固定的训练数据上预测,而在随后的每一次迭代中都不更新,时间越长,误差就会越大。
我在项目里写了一个简单的滚动回归循环,每次拟合只取最近30天窗口的数据,效果比全量数据训练明显稳一些。原因是疫情数据的生成机制会变化,旧数据里包含过时的规律,及时丢掉反而是好事。
4.3 简单正则化要不要上
线性回归有一种变体叫岭回归,还有Lasso回归。它们做的事情是给系数加上惩罚项,防止系数膨胀。
在疫情预测这个场景里,如果特征少,我一般不急着上正则化。因为普通的线性回归已经完全够用,加上正则化反而多了一个超参数要调。但如果特征数量上去了,尤其是造了十几列哑变量,这时候系数容易变得不稳定,上岭回归就是合理的优化手段。
以sklearn里的实现来说,岭回归和线性回归的代码几乎一样,就是把类名换一下:
from sklearn.linear_model import Ridge model = Ridge(alpha=1.0) model.fit(X_train, y_train)alpha这个参数表示惩罚强度,默认是1.0。在实际调的时候,我一般会从0.1、1、10这个尺度去试,对比测试集MSE选最优。不过如果你在项目中连普通线性回归都还没跑顺,先别整这些花活,基线模型比什么都重要。
5. 踩坑实录与排查技巧:我替你趟过的雷
5.1 测试集上很准,新数据全崩的外推陷阱
这是我踩过最深的一个坑。用某个包含上升和下行完整周期的模拟数据训练模型,测试集是后面几个月的数据,预测曲线竟然和真实曲线几乎重合,当时一度以为找到了最优解。
但当我把训练窗口缩到只有上升期数据,再预测后面的下行期时,模型崩得彻底。真实数据一路向下,预测线还在往上爬。
原因很简单:线性回归只会沿着训练集中学到的斜率向外延伸,它不具备“产生新趋势”的能力。模型没见过下行期,它当然不知道要拐弯。这就是所谓的外推限制。
应对办法是:预测结果必须结合业务判断来解读。模型说会继续涨,你就要追问一句“历史数据里有没有出现过拐点先例”。没有的话,这个结论充其量只是惯性延伸,决策时必须打折使用。
5.2 多重共线性让回归系数犯神经
另一种情况是特征之间高度相关。比如同时加了“时间序号”和“累计病例数”作为特征,而后者本身和前者强相关,这时候线性回归的系数会变得极不稳定。
它的表现很迷惑:训练集上指标还不错,但你把单个系数拿出来看,正负号都可能是反的。有人会把负系数解读成“时间越久感染越少”,但事实可能完全相反。
排查方式是算特征之间的相关系数矩阵,或者看方差膨胀因子。一旦发现强相关的特征,优先删除冗余特征。我后来直接去掉了“累计病例数”这个特征,只用时间和干预标记,模型反而更稳。
5.3 报告滞后和数据修正的坑
现实中的疫情数据往往不是一次成型的,而是会事后修正。头一天报告的新增数字,过了几天可能被重新调整。这意味着你在今天建模用到的“历史真实数据”,也许明天就变了。
这种情况下,模型训练和预测的基础已经漂移,你的误差计算也会失真。我的处理习惯是使用官网统一发布的快照数据,并且在建模时固定数据版本。如果数据发生过修正,就在文档里标注出来,避免团队内部对结论产生争执。
5.4 常见问题排查速查表
| 现象 | 可能原因 | 优先排查动作 |
|---|---|---|
| 训练集评分极高,测试集评分很低 | 数据泄漏或过拟合 | 检查是否随机切分、特征是否包含未来信息 |
| 预测曲线趋势和真实完全不同 | 线性回归外推限制 | 看训练窗口是否完整包含多种趋势阶段 |
| 回归系数的正负号不符合常识 | 多重共线性 | 计算特征相关矩阵,删除冗余特征 |
| 预测值出现负数 | 特征尺度或模型没有限定非负 | 考虑目标取对数、模型加约束 |
| 评估指标好但实际应用总偏一截 | 数据版本或统计口径变动 | 检查数据源是否被事后修正 |
5.5 评估指标好但业务用不了
最后一种情况最气人,就是所有指标都在合理范围内,你跑到业务面前一顿展示,业务却说你预测的和实际感受不对。
这个问题往往不是模型错,而是预测对象本身定义不匹配。比如你预测的是“累计感染数”,业务关心的是“当日新增床位需求”,这中间还隔着年龄构成、重症比例等因素。线性回归把一个简单目标预测得再准,也回答不了业务真正想问的那个问题。
所以开始建模前先和业务对齐预测目标,这个步骤省不了。所谓“好模型”的前提永远是预测目标和业务目标同频。
6. 事后复盘:这几次实操改变了我的哪些认知
项目收尾之后,我特意把整个流程复盘了一遍,有三点体会想特别分享给初学者。
第一,别迷信复杂模型。用线性回归把数据工程、特征思路、评估方法全部跑通之后,你的核心能力已经建立起来了。以后换成LSTM、Prophet,本质上改的只是模型部分,数据工程和评估框架是复用的。最怕的是数据还没洗干净,就开始堆模型复杂度。
第二,“可解释”本身就是竞争力。线性回归的系数可以直接读,遇到质疑时你能清楚解释预测依据是什么。对比之下,树上模型和深度学习模型虽然精度可能更高,但解释成本很高。在很多产品化不高的场景里,可解释甚至比精度更重要。
第三,时间序列建模不只是调参。数据切分是否合规、特征是否包含未来信息、预测目标和业务需求是否一致,这些才是决定项目成败的关键。我在这个项目里最大的收获不是学会了调用LinearRegression这个API,而是养成了从数据机制出发审视方案的思维方式。
如果你正在用类似结构做预测类练习项目,我建议你也按照“数据清洗 → 特征构造 → 基线回归 → 滚动评估 → 复盘迭代”这个顺序走一遍。先把线性回归跑透,再看它到底在哪些地方失灵,这时候你对机器学习的边界感会比看一百篇教程都管用。