1. 从热搜词看回归模型:这个实战项目到底在练什么
如果把“3回归项目实战:新冠病毒感染人数预测”这个标题扔进技术社区,点进来的大概分三类人:一是刚学完线性回归、想找个完整案例练手的初学者,二是被各种热搜词比如lightgbm回归模型、xgboost回归模型、随机森林回归算法轰炸过、想搞明白它们之间到底有什么区别的进阶者,三是纯粹对“预测疫情数字”这件事好奇、想看看机器学习能不能派上用场的路人。
这个项目的价值恰恰在于它把所有这些需求都串起来了。
先说回归本身。回归是监督学习里最基础也最实用的一类任务,核心就一句话:给定一堆历史数据,学出一个函数,让这个函数能对连续数值做预测。预测房价、预测销量、预测气温,本质上都是回归。而“感染人数预测”这个场景,比房价预测更有挑战性:数据有强的时间依赖性,早期数据少,后期波动大,还可能有政策干预带来的突变点。这些特性让简单的线性模型不够用,必须引入更复杂的算法,于是就有了热搜词列表里那串名字:线性回归、岭回归、Lasso、决策树回归、随机森林、XGBoost、LightGBM。
这个项目能练到的东西非常实在:特征怎么构造、数据怎么划分、模型怎么选型、参数怎么调、过拟合怎么防、结果怎么评估。这些都是面试里常问、工作中常用的硬功夫。你可能会说,直接用真实疫情数据不就行了吗?问题在于真实数据的获取和清洗本身就有门槛,而且不同国家、不同时期的统计口径差异很大,很容易把时间花在数据清洗上而不是模型学习上。所以更聪明的做法是构造一套基于真实趋势的模拟数据来做项目,把重心放在“建模”本身。
这个项目的目标读者,我建议是已经学过Python基础、了解pandas和sklearn基本用法的朋友。如果你连DataFrame是什么都还没搞清,建议先补一下基础;如果你已经会跑通一个简单的线性回归,那这个项目就是帮你从“会跑demo”进阶到“会做完整项目”的关键一步。
2. 方案选型:为什么这个项目要把经典模型和集成模型都过一遍
2.1 从Excel到Python:理解工具的边界在哪里
热搜词里有一条“如何在excel制作加乘回归模型”,这个搜索意图很典型,说明很多人最开始是想用熟悉的工具解决问题。Excel确实能做回归分析,它内置了线性回归、指数趋势线等功能,画个散点图、勾选“显示公式”,一条趋势线和R²就出来了。对于简单的一元线性回归,Excel完全够用,而且非常直观。
但Excel的边界很快就能碰到:一是它对多维特征的支持很弱,你没法方便地在Excel里构建一个包含历史滞后值、移动平均、星期几、节假日标记等多维特征的数据集;二是它没有内置的交叉验证和超参数搜索机制,模型好不好全靠肉眼判断;三是遇到随机森林、XGBoost这类需要大量迭代计算的模型,Excel完全无能为力。
所以这个项目虽然不排斥Excel做初步探索,但真正的建模环节必须用Python。这也是为什么我在后面的实操部分会给出完整的sklearn和LightGBM代码。工具没有高低之分,关键是知道在什么阶段用哪个工具。
2.2 为什么先跑经典回归再看集成模型
我在设计这个项目的算法链路时,刻意让经典回归算法和集成学习算法各占一半,不是没有理由的。
线性回归、岭回归、Lasso这些经典模型,它们的优势在于可解释性强、计算速度快、代码实现简单。线性回归的coef_直接告诉你每个特征对目标的贡献方向和大小;岭回归通过L2正则化解决特征共线性问题;Lasso通过L1正则化自动做特征选择。这些特性让它们成为理解回归问题的“第一站”。
而随机森林、XGBoost、LightGBM这些集成模型,它们的优势在于能捕捉非线性关系、能处理特征交互、对异常值和缺失值更鲁棒。在感染人数预测这个任务中,感染人数的增长往往不是线性的,早期可能近似指数增长,后期因为防控措施出现回落,这种复杂的动态关系用线性模型很难刻画,而树模型天然适合。
把它们放在同一个项目里对比,你会发现一个很有意思的现象:在这个数据集上,随机森林的默认参数可能就比线性回归调参后的效果还好,而XGBoost和LightGBM在这个中等规模的数据集上可能优势不明显,甚至因为数据量不够大而出现过拟合。通过这样的对比,你就会明白“模型不是越复杂越好,而是越合适越好”这句话的实际含义。
2.3 数据集构建背后的真实逻辑
我一直强调,这个项目要用模拟数据,不是为了偷懒,而是为了把精力放在建模上。但模拟数据也不是凭空编,我参照了2020年春季某地区疫情走势的真实形状:先是一段缓慢增长期,然后进入指数快速上升期,最后在强力干预下进入下降平台期,整体呈单峰曲线。
模拟数据的构造逻辑是这样的:用一组数学函数生成基础曲线,然后叠加合理的噪声和可控的随机性。这样做的最大好处是,你心里对“正确答案”有一个预期。比如你知道真实规律是“第20天前后进入快速增长期”,那你就能直观地检验模型有没有捕捉到这个拐点。如果用真实数据,你很难判断模型预测偏差到底来自数据噪声还是模型本身的问题。
这个“先构造已知规律的数据,再验证模型能否还原规律”的思路,是机器学习教学中非常经典的练习方式。它让你从“黑盒调参”转变为“验证假设”,这是质的区别。
3. 核心细节解析:回归模型的损失函数、评估指标与调参要点
3.1 回归模型的损失函数到底在优化什么
热搜词里有“逻辑回归损失函数头歌”和“逻辑回归”,这里必须澄清一个常见的概念混淆。
逻辑回归虽然名字里带“回归”,但它实际上是一个分类模型,输出的是样本属于某个类别的概率,因此它的损失函数是交叉熵损失(log loss),而非回归任务里的均方误差。举个例子,逻辑回归适合回答“明天感染人数会不会超过1000”这种是/否问题,而线性回归适合回答“明天感染人数大约是多少”这种数值问题。这个项目的任务是预测具体的感染人数,所以用的是回归模型家族的损失函数,不包括逻辑回归。
回归任务的损失函数主要有这么几个,初学者一定要搞清楚它们的区别:
- 均方误差(MSE):对误差取平方,意味着大误差会被放大。它是回归模型最常用的损失函数,对异常值非常敏感。
- 均绝对误差(MAE):对误差取绝对值,对异常值的容忍度更高,但它的导数在零点不连续,优化时不如MSE平滑。
- Huber损失:综合了MSE和MAE的优点,误差小时用平方损失,误差大时用绝对值损失,有一个超参数delta来控制切换阈值。在感染人数预测这种可能有异常波动(比如某天突然大量上报)的数据上,Huber损失是很实用的选择。
在sklearn的LinearRegression中,默认就是在最小化MSE;而在XGBoost和LightGBM中,默认也是回归平方误差损失,但你可以通过objective参数改成Huber或其它自定义损失。我实际跑下来的体会是,当数据里存在明显异常值时,用Huber损失训练的XGBoost模型比用默认MSE损失的表现更稳定,泛化误差也更低。
3.2 评估指标不能只看一个R²
“这个模型效果怎么样?”是每个做回归项目的人都要回答的问题。如果只盯着一个R²,很容易被误导。
R²(决定系数)的含义是模型能解释目标变量多大比例的方差,取值越接近1越好。但它有一个问题:在测试集上做预测时,R²的计算方式决定了它对尺度敏感,而且当测试集的均值与训练集差异较大时,R²会变得很奇怪。比如未来几天感染人数整体抬升,即使模型预测的方向是对的,R²也可能很低。
所以这个项目里我建议同时看三个指标:
- MAE:直观反映平均预测偏差的绝对值,单位跟原始数据一致,比如“平均偏差1234人”,非常好理解。
- RMSE:对大误差更敏感,如果模型在某几天预测严重偏差,RMSE会明显变大,用来捕捉“不可接受的错误”。
- MAPE(平均绝对百分比误差):反映相对误差,比如“平均偏差12%”。这个指标在疫情预测场景里特别直观,因为它直接告诉你预测偏离真实值几个百分点。
实操里我会这样用这些指标:先用MAE看整体偏差水平,用RMSE检查有没有大偏差点,用MAPE对比不同模型间的相对优劣。三个指标一起看,才能对模型能力形成完整认知。
3.3 树模型参数那么多,真正需要调的没几个
随机森林、XGBoost、LightGBM这三个模型,光参数就有几十个,新手一上来容易懵。我在跑完整个项目之后的体会是,在这类中小规模数据集上,真正需要重点关注的参数就那么几个。
对于随机森林,最核心的参数有两个:n_estimators(树的数量)和max_depth(树的最大深度)。n_estimators太少容易欠拟合,太多则训练变慢且收益递减,我一般先用100棵树看基线效果,再通过学习曲线观察是否继续增加树的数量能带来提升。max_depth控制单棵树的复杂度,如果深度过大,模型会把训练集里的噪声也记住,导致过拟合。特征随机采样的比例max_features也需要试一下,默认是特征数的平方根,但在这个项目里我试过全部用sqrt反而不如手动设置0.5到0.7的效果稳定。
对于XGBoost和LightGBM,核心参数可以归成三组:树的结构参数(max_depth、num_leaves)、正则化参数(reg_alpha、reg_lambda)、随机性参数(subsample、colsample_bytree)。其中LightGBM的num_leaves比max_depth更重要,因为它控制的是每棵树的叶子节点数量,直接决定模型的复杂度。在感染人数预测的数据集上,我建议把num_leaves控制在16到31之间,太大必过拟合。学习率learning_rate我一般设为0.05到0.1,配合早停法(early stopping)来防止过拟合。
不要一上来就搞网格搜索。正确的做法是先固定一组粗参数跑通全流程,然后针对最重要的两三个参数做小范围搜索,重点观察验证集上的表现,而不是训练集上的表现。
4. 实操全流程:用Python从数据到模型对比的一次完整演练
4.1 环境准备与模拟数据构造
我假设你的电脑上已经装好了Python 3.8以上版本,以及pandas、numpy、matplotlib、scikit-learn这些常用库。如果还没装,强烈建议直接用Anaconda,一步到位避免环境问题。本项目的核心依赖是scikit-learn和LightGBM,前者提供线性回归、岭回归、随机森林等模型,后者提供LightGBM模型。
下面是模拟数据的构造代码,我特意把数据生成函数写成了可调参数的形式,方便你控制曲线的形状、噪声的大小和数据的长度:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression, Ridge, Lasso from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import lightgbm as lgb np.random.seed(42) # 构造模拟数据:单峰曲线,前20天缓慢增长,中间20天指数上升,后20天回落 days = np.arange(0, 60) base = 100 + 5 * days # 基础线性趋势 peak = np.exp((days - 20) / 6.0) * 20 # 指数上升部分 peak = np.clip(peak, 0, 6000) # 限制峰值 noise = np.random.normal(0, 80, size=len(days)) # 随机噪声 cases = base + peak + noise df = pd.DataFrame({'day': days, 'cases': cases}) plt.figure(figsize=(10, 4)) plt.plot(days, cases, 'o-', label='模拟感染人数') plt.xlabel('天') plt.ylabel('人数') plt.legend() plt.title('模拟感染人数时序曲线') plt.show()如果只把日期当天数、人数当目标变量做回归,模型能学到的信息非常有限。真正的技巧在于做特征工程,把“天”这个一维变量扩展成一个高维特征空间。惯用的做法包括:把天数的二次项和三次项加入(捕捉非线性趋势)、把前5天的滞后值加入(捕捉自相关关系)、把7天移动平均加入(平滑短期波动)、把星期几和是否节假日的标记加入(捕捉周期性)。
# 特征工程:构造滞后特征、移动平均、多项式特征 for lag in [1, 3, 5]: df[f'lag_{lag}'] = df['cases'].shift(lag) df['rolling_7'] = df['cases'].rolling(window=7).mean() df['day_sq'] = df['day'] ** 2 df['day_cub'] = df['day'] ** 3 # 丢掉缺失行 df = df.dropna().reset_index(drop=True) feature_cols = [c for c in df.columns if c != 'cases'] X = df[feature_cols] y = df['cases'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False)注意train_test_split这里设置了shuffle=False,这是有意的。时间序列数据不能随机打乱,后20天的数据必须留出来当测试集,模拟的是“拿历史预测未来”的真实场景。这是新手最容易踩的坑之一。
4.2 经典回归模型基线效果
先把最简单的线性回归跑通,当作基线,后面所有复杂模型都要跟它对比,看看到底值不值得引入额外的复杂度。
model_lr = LinearRegression() model_lr.fit(X_train, y_train) y_pred_lr = model_lr.predict(X_test) print('线性回归 MAE:', round(mean_absolute_error(y_test, y_pred_lr), 2)) print('线性回归 RMSE:', round(np.sqrt(mean_squared_error(y_test, y_pred_lr)), 2)) print('线性回归 R2:', round(r2_score(y_test, y_pred_lr), 4))紧接着试岭回归和Lasso。岭回归跟线性回归的区别就在于多了L2正则化,惩罚系数alpha控制惩罚力度。alpha太小跟线性回归几乎没区别,alpha太大则会把系数压缩得过小,导致欠拟合。在这个小数据集上,alpha在1到10之间比较合理。
model_ridge = Ridge(alpha=1.0) model_ridge.fit(X_train, y_train) y_pred_ridge = model_ridge.predict(X_test) model_lasso = Lasso(alpha=1.0) model_lasso.fit(X_train, y_train) y_pred_lasso = model_lasso.predict(X_test)Lasso这里有个坑,Lasso默认用的是坐标下降法,对特征的量纲非常敏感,如果特征没有标准化,结果可能很不稳定。建议在Lasso之前用StandardScaler对特征做标准化,但这样一来,线性回归和岭回归的系数可解释性就会被破坏,因为它们基于的是标准化后的特征。实际项目里我一般会把特征标准化放在流水线里做,用Pipeline把StandardScaler和模型串起来,方便统一管理和交叉验证。
4.3 随机森林、XGBoost、LightGBM的登场
树模型不需要做特征标准化,这省了很大力气。但它们有另外一套问题:超参数多、训练结果有随机性。我在跑随机森林时,把n_estimators设成了200,max_depth设成了5,max_features设成了0.6,然后观察它在测试集上的表现。
model_rf = RandomForestRegressor( n_estimators=200, max_depth=5, max_features=0.6, random_state=42 ) model_rf.fit(X_train, y_train) y_pred_rf = model_rf.predict(X_test) print('随机森林 MAE:', round(mean_absolute_error(y_test, y_pred_rf), 2)) print('随机森林 RMSE:', round(np.sqrt(mean_squared_error(y_test, y_pred_rf)), 2)) print('随机森林 R2:', round(r2_score(y_test, y_pred_rf), 4))XGBoost的用法跟LightGBM很像,但XGBoost更慢一些,在小数据集上尤甚。我用XGBoost时踩过最典型的坑是忘了设置early_stopping,导致训练轮数一大就过拟合。正确的做法是用sklearn的API配合eval_set和early_stopping_rounds,让模型在验证集上不再提升时自动停止训练。
xgb_model = lgb.LGBMRegressor( objective='regression', n_estimators=500, learning_rate=0.05, num_leaves=16, max_depth=5, subsample=0.8, colsample_bytree=0.8, random_state=42 ) xgb_model.fit( X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(50)], ) y_pred_lgb = xgb_model.predict(X_test)LightGBM默认打印一堆训练日志,看起来有点吓人,其实没有什么神秘的,就是在不断报告每一轮在验证集上的损失。看到验证损失先降后升,说明过拟合开始了,早停就触发在这里。
4.4 统一评估与模型对比
所有模型跑完后,我习惯把指标整理成一张表格,一目了然地对比:
| 模型 | MAE | RMSE | R² |
|---|---|---|---|
| 线性回归 | 1220.34 | 1456.78 | 0.87 |
| 岭回归 | 1156.21 | 1398.10 | 0.88 |
| Lasso | 1180.45 | 1420.33 | 0.87 |
| 随机森林 | 823.16 | 1045.52 | 0.93 |
| LightGBM | 876.09 | 1105.23 | 0.92 |
(以上数值仅为演示,实际取决于你的随机种子和数据构造参数,但趋势基本是一致的:集成模型在这个非线性数据上明显优于线性模型。)
你可能会问,随机森林和LightGBM的排名能不能再提升?当然可以,但需要更系统的调参。我建议用GridSearchCV或者Optuna做超参数搜索,重点搜max_depth、num_leaves、learning_rate这三个参数。但一定要记住,调参的目标是测试集表现,不是训练集表现。
5. 实操中的高频问题与避坑指南
5.1 训练集拟合得很好,测试集一塌糊涂
这个问题我在带新手做项目时见到过无数次。原因无外乎两个:一是数据划分时用了shuffle=True,把未来数据混进了训练集,模型直接“偷看”了答案;二是模型复杂度过高,树模型深度太大,或者XGBoost训练轮数太多,把训练集里的随机噪声也当成了规律。
解决办法说起来简单:时间序列数据用时间顺序划分;给树模型加深度限制和正则化;用早停让模型在验证集效果不再提升时停止训练。真正操作的时候,很多人忘了在交叉验证里同样保持时间顺序,如果用TimeSeriesSplit代替普通的KFold,这个问题就能从机制上避免。
5.2 LightGBM的训练日志刷屏,怎么快速判断训练是否正常
LightGBM的verbose参数是关键。设置verbose=-1可以关闭所有日志;设置verbose=100表示每100轮打印一次。在调参阶段,我建议把verbose设成50或100,观察验证集上mae的变化趋势,如果验证集误差在连续50轮以上都没有改善,早停就会被触发,然后模型自动停在最佳迭代轮数上。
另外还有一个细节:在LGBMRegressor的sklearn接口中,早停回调的写法是lgb.early_stopping(stopping_rounds),这个API我只在较新的版本上见到过,如果你的版本比较老,可能需要换用lgb.early_stopping(50)这种带位置参数的老写法。遇到API报错的时候,先升级lightgbm到最新版,然后看官方文档,别硬记网上的老代码。
5.3 特征重要性结果靠不靠谱
随机森林和LightGBM都能输出feature_importances_,新手看到排序结果就奉为圭臬,这其实是有局限性的。树模型的特征重要性衡量的是特征在分裂时带来的不纯度下降总量,它偏向取值种类多的特征,而且高相关特征之间会“分摊”重要性。
举个例子,lag_1(前一天的感染人数)和rolling_7(7天移动平均)之间高度相关,如果同时放进模型,两者的特征重要性都会低于真实的贡献水平。这并不代表它们不重要,只是重要性被分散了。所以,特征重要性适合用来做粗筛和模型解释,不适合作为特征取舍的唯一依据。
遇到高相关特征,我建议的做法是:先跑一次基线,然后做特征的逐个剔除实验,或者用SHAP值做更细粒度的归因分析。SHAP库给出的shap值能告诉你每个样本上每个特征对预测值的具体贡献方向,是正向拉高还是负向压低,这个信息比单纯的feature_importances_丰富得多。
5.4 预测结果出现负值怎么办
线性回归模型在预测时完全没有边界约束,如果特征组合的结果偏小,线性预测值完全可能是一个负数。而“感染人数为负数”显然是荒谬的。
最简单的处理办法是把负值截断为0,可以用numpy的np.maximum(0, y_pred)一步到位。但是截断只是保底,背后的问题(模型对边界情况估计不准)并没有解决。在真实项目中,我建议对目标变量做log1p变换,也就是对y做log(y+1)后再建模,预测完再用expm1变换回来。这样做的理由是:感染人数的分布是右偏的,对数变换能压缩数量级的差异,让模型更容易捕捉相对变化而不是绝对变化,而且对数变换天然保证了预测值在变换回原空间时非负。
对数变换对树模型的影响不大,但对线性回归有实质帮助,如果你发现线性模型频繁预测出负值,优先考虑log变换而不是硬截断。
6. 进阶方向:这个项目还能怎么延伸
这个项目做到这里,已经覆盖了一个完整回归项目从数据到建模再到评估的全流程。如果还想继续深入,有几个方向我非常推荐:
第一,加入真实世界的数据源。可以找一个公开的传染病数据接口,把真实数据按同样的流程跑一遍。真实数据的挑战在于缺失值、异常点、统计口径变化,这些是模拟数据完全体会不到的“真实感”。我建议先用模拟数据把代码调试通顺,再切换到真实数据,这样排查问题时能有个确定性的参考。
第二,引入时序模型做对比。ARIMA、Prophet、LSTM这些时序模型本质上是回归的变体和延伸,把它们放进同一个评估框架里对比,你对“模型选型”的理解会上一个台阶。尤其是在短序列预测上,Prophet和简单回归模型的表现差距,会改变你对“复杂模型一定更好”这个印象的判断。
第三,做端到端的可视化产物。把模型的预测曲线、置信区间、未来7天外推线画在一起,再加一段文字结论,就是一份可以直接拿去汇报的分析报告。这个能力在实际工作中价值很大,因为大多数业务方想看的不是模型参数,而是“未来走势是什么”,以及“该不该做决策”。
我个人在实际操作中的体会是,这个项目最值得回味的不是最终的数字,而是整个过程中“假设不断被打破、方法不断被迭代”的那种节奏。第一次跑出线性回归那么差的预测值,你会意识到数据里非线性的成分有多强;第一次把LightGBM跑通并且看到它在验证集上持续下降的损失曲线,你会真正理解“模型学习到了规律”这句话的分量。如果你能从这篇文章里带走一样东西,我希望是:面对一个回归问题时,先搭一个最简单的基线,再逐步引入复杂度,每一步都问自己“加了复杂度之后收益是多少、代价是什么”。带着这个问题去做项目,比记住任何模型参数都管用。