ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

线性回归实战:从数据清洗到模型评估的完整机器学习流程

线性回归实战:从数据清洗到模型评估的完整机器学习流程 先说我做这个项目时的直观感受线性回归几乎是所有机器学习入门教程里的第一节。如果你刚接触数据相关的工作或者想自己动手做一次完整的建模流程强烈建议把线性回归小项目当作起点。这个小项目最大的好处是它足够简单能在几个小时内跑通“数据清洗 - 特征处理 - 模型训练 - 结果评估”的全过程同时又足够典型后期换各种算法都离不开这套框架。我这次用的数据集是一份房价数据特征包括面积、房龄、卧室数量、卫生间数量、楼层、朝向、周边配套等目标是预测房价。做这个项目我不只是想跑一个回归模型出来更想搞清楚每一步操作背后的逻辑为什么这样处理缺失值、为什么划分训练集和测试集、为什么评估指标选RMSE而不是R²。只有把这些想明白才算真正入门。1. 项目背景与整体思路设计1.1 为什么选线性回归作为第一个机器学习项目很多人一上来就学随机森林、XGBoost结果被大量参数和花哨的原理劝退。线性回归的好处在于它的逻辑非常透明输入特征和输出之间存在线性关系通过最小化误差来拟合一条直线或超平面。这种透明性让你能把更多精力放在理解机器学习的基本流程上而不是纠结于复杂模型的内部机制。我在实际指导新人时通常会让他们先不看任何高阶算法而是用线性回归完成一个完整小项目。原因很简单线性回归是理解“特征如何影响预测”的基石。比如面积增加一平米房价平均增加多少钱卧室数量增加一间房价升高还是降低这些问题都可以从线性回归的系数中直接读出答案。换到其他算法特征重要性虽然也能看但解释性没有线性回归那么清晰。打个比方线性回归就像驾驶中的“手动挡”虽然操作比自动挡繁琐但你能真切感受到换挡时机和转速的关系。一旦手动挡开熟了再去开自动挡就是降维打击。同样熟练掌握线性回归之后再去理解正则化、逻辑回归、神经网络的前向传播和损失函数会顺畅很多。1.2 项目目标与数据来源的考量做项目之前我先明确这个项目的目标不是拿个高分模型而是完成一次标准建模流程并能够复述每一步。所以我定下了三个具体指标训练好的模型在测试集上的R²达到0.8以上考虑到数据质量这个目标不是太高训练一套完整的评估报告包括RMSE、R²、残差分布能解释每个特征与目标变量之间的定量关系。数据来源上我选了一份公开的波士顿房价风格的模拟数据。很多人不敢用公开数据集觉得太陈旧其实只要数据字段完整、样本量足够一般500条以上就非常适合练手。关键在于过程中你要保持“主动思考”比如思考为什么某些特征会缺失、某些字段是否需要做归一化。我在这个项目里刻意不用现成的sklearn内置数据集而是自己构造了一份带有缺失值、离群点的csv文件。这样能贴近真实工作场景因为实际业务里你拿到的数据几乎不会是干干净净的。2. 环境准备与工具选型2.1 开发环境搭建与库版本说明我建议直接用 Anaconda 创建一个独立的虚拟环境避免项目依赖相互污染。本项目的核心库非常经典我用的是Python 3.9pandas 1.5.1做数据加载、清洗、聚合numpy 1.24.3做数值计算scikit-learn 1.2.2提供线性回归模型和评估工具matplotlib 3.7.1 seaborn 0.12.2做可视化。创建环境的命令很简单conda create -n lr_project python3.9 conda activate lr_project pip install pandas numpy scikit-learn matplotlib seaborn实际做的时候我遇到过一个挺有意思的坑如果直接安装最新版pandas当时是2.x部分sklearn的兼容性会有小问题比如数据格式转换时报错。所以如果不是必须建议锁定我上面给的版本组合跑起来非常稳。另外建议在项目目录下建一个data/目录存放原始数据一个notebooks/目录存放实验记录一个output/目录保存图片和模型文件。这样后续迭代时每个环节的产出都能追踪到。2.2 为什么用Scikit-learn而不是手写算法我知道不少初学者喜欢手写线性回归用梯度下降一步步更新权重这的确是加深理解的好办法。但作为一个小项目我更推荐先学会用 Scikit-learn原因有两点它可以帮你屏蔽底层重复代码让你聚焦于数据处理和结果分析Sklearn 的接口统一后续换 Lasso、Ridge、甚至决策树代码几乎不用改。当然在理解原理时我会建议大家动手实现一次梯度下降。我在 4.2 节会贴一段简短的实现方便你对比库函数的输出看看两者的系数是否接近。这样既懂原理又会用工具才算真正的“既知道菜怎么炒也会点外卖”。3. 数据探索与预处理实操3.1 加载数据并观察结构拿到数据的第一件事不是直接跑模型而是先“看一眼”。我通常用df.info()和df.describe()快速了解数据的整体情况import pandas as pd df pd.read_csv(data/house_prices.csv) print(df.shape) print(df.info()) print(df.describe())输出会告诉你每个字段有几个非空值、数据类型是什么、数值字段的均值、标准差、最小值、最大值。我当时这份数据大概有 600 行其中LotArea、YearBuilt有少量缺失SalePrice是目标列没有缺失但存在几个明显的极大值。这个“看一眼”的环节千万不能省因为后续所有决策都建立在对数据的直观感受上。建议再把特征之间的相关性简单看一遍。我会用df.corr()[SalePrice].sort_values(ascendingFalse)打印出与房价相关性最高的特征心里有个预期。比如面积类特征相关性通常最高而房龄通常与房价负相关。3.2 缺失值处理均值填充还是删除缺失值处理没有银弹核心原则是不可引入信息泄漏。我这次的处理方式如下# 查看缺失比例 missing_ratio df.isna().mean().sort_values(ascendingFalse) print(missing_ratio[missing_ratio 0])对于数值特征如果缺失比例低于5%我直接用均值填充如果高于5%会考虑对缺失做标记新增一列“是否缺失”并填充中位数。为什么用中位数而不是均值因为均值容易受离群点影响比如某个房子价格极高会拉高整体均值。中位数更稳健。对于类别特征比如Neighborhood如果有缺失我会填充众数。实际上这份数据里并没有类别缺失但作为一种常见处理方式值得提一下。我还在这个阶段做了一件事把SalePrice取对数作为新的目标列。原因是房价分布普遍右偏取对数后更接近正态分布模型更容易拟合且评估指标RMSE的尺度更均匀。这一点在实际赛题里非常常见。import numpy as np df[LogPrice] np.log1p(df[SalePrice])3.3 特征工程与数据集划分特征工程是这个项目的点睛环节。虽然线性回归不要求特征必须归一化但如果特征数值尺度差异太大比如面积几百、房龄几十梯度下降会收敛得比较慢。使用sklearn的StandardScaler做标准化是个稳妥的方案。我这里把数值特征统一归一化并为部分类别特征做One-Hot编码。地区这种类别特征如果不处理模型没法理解编码后就会变成二值特征。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer numeric_features [LotArea, YearBuilt, TotalBsmtSF, GrLivArea, GarageArea] categorical_features [Neighborhood, BldgType] preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), numeric_features), (cat, OneHotEncoder(handle_unknownignore), categorical_features) ]) X df[numeric_features categorical_features] y df[LogPrice] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) X_train_processed preprocessor.fit_transform(X_train) X_test_processed preprocessor.transform(X_test)这里最常见的错误对全量数据做标准化然后再划分训练集和测试集。这样会导致模型在训练时“偷看”了测试集的分布造成评估虚高。我刚开始做项目时踩过这个坑后面在6.1节会单独详细说明。4. 模型训练与核心参数解析4.1 线性回归模型原理与损失函数线性回归的数学模型很简单y w1*x1 w2*x2 ... wn*xn b其中w是特征权重b是偏置。模型训练的过程就是寻找一组 w 和 b使得预测值和真实值的差异最小。最常用的损失函数是均方误差MSE公式为MSE (1/n) * sum((y_true - y_pred)^2)为什么用平方误差而不是绝对误差因为平方误差是光滑可导的方便用梯度下降求导同时它会对大误差施以更重的惩罚让模型更努力地修正那些预测差很大的样本。当然缺点也很明显对异常值敏感。所以我在数据预处理阶段就处理了明显离群点否则一个极端值会把整条拟合线带偏。sklearn 的LinearRegression默认使用最小二乘法求解也就是说它直接通过求解正规方程得到唯一最优解不需要手动设置学习率。当特征较少少于几千个且没有严重共线性时这种解法很快也很稳定。4.2 训练流程从梯度下降到sklearn实现为了让你真懂原理我先把梯度下降手写出来。这个过程非常有助于理解“迭代”、“损失”、“学习率”这些概念。import numpy as np def compute_loss(X, y, w, b): n len(y) y_pred X.dot(w) b loss (1 / (2 * n)) * np.sum((y_pred - y) ** 2) return loss def gradient_descent(X, y, w, b, lr, epochs): n len(y) for epoch in range(epochs): y_pred X.dot(w) b dw (1 / n) * X.T.dot(y_pred - y) db (1 / n) * np.sum(y_pred - y) w - lr * dw b - lr * db if epoch % 100 0: print(fepoch {epoch}, loss {compute_loss(X, y, w, b):.4f}) return w, b实际在 sklearn 里只需要三行代码from sklearn.linear_model import LinearRegression model LinearRegression() model.fit(X_train_processed, y_train)这里有个很有意思的点两个方法得到的系数非常接近但sklearn的批量解更快。只有在特征维度特别大比如上百万或者矩阵不可逆时才会需要用梯度下降。所以当作算法拼图的一部分理解就够了。4.3 模型评估指标为什么先用RMSE再用R²模型训练完成后我通常会同时看RMSE和R²。RMSE均方根误差的单位和预测目标一致能直观告诉你“预测平均偏差多少”R²则是一个无量纲的拟合优度取值范围从0到1越靠近1说明模型解释掉了越多的方差。from sklearn.metrics import mean_squared_error, r2_score y_pred model.predict(X_test_processed) rmse np.sqrt(mean_squared_error(y_test, y_pred)) r2 r2_score(y_test, y_pred) print(fRMSE: {rmse:.4f}) print(fR²: {r2:.4f})我这次得到的结果测试集RMSE约0.152R²约0.873。因为目标变量做的是对数变换所以RMSE需要反变换才能解释成房价的实际差异。expm1()之后0.15的对数误差大约对应房价波动15%左右这个精度在小项目里已经算不错。如果只报告R²容易忽略大误差样本的影响如果只报告RMSE又看不出模型解释力。两个一起看才算完整。我建议以后所有回归项目都同时输出这两个指标。5. 可视化分析与结果解读5.1 回归拟合效果图看预测与真实值的散点数字永远没有图直观。我画了两张图第一张是真实值与预测值的散点图理想情况是点落在45度线附近。import matplotlib.pyplot as plt import seaborn as sns plt.figure(figsize(8, 6)) sns.scatterplot(xy_test, yy_pred, alpha0.6) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--, lw2) plt.xlabel(True LogPrice) plt.ylabel(Predicted LogPrice) plt.title(True vs Predicted) plt.tight_layout() plt.savefig(output/true_vs_pred.png)从这张图里我能非常清楚地看到中低价位段的样本预测很集中但高价位段点变得稀疏有些点偏离对角线。这说明模型对极端高值的学习不够充分很大原因是高房价样本在数据集中占比本来就少。如果后续要提升效果可以考虑单独对高房价样本做加权或者收集更多此类样本。5.2 残差分析模型是否违反基本假设线性回归有一个重要假设残差应该随机分布在0附近且不随预测值增大而产生喇叭状。画残差图是最直接的检验方式residuals y_test - y_pred plt.figure(figsize(8, 5)) sns.scatterplot(xy_pred, yresiduals, alpha0.6) plt.axhline(y0, colorr, linestyle--) plt.xlabel(Predicted LogPrice) plt.ylabel(Residuals) plt.title(Residual Plot) plt.tight_layout() plt.savefig(output/residual_plot.png)我这次看到的残差图整体比较随机没有明显漏斗形说明模型没有严重违反方差齐性假设。但在左侧低预测值处有几个负残差比较大的点也即模型把某些低价房的价格预测得偏高。这通常意味着这些房子可能有一些独特的负面特征没有被模型捕捉到比如紧邻铁路、地下室漏水等。线性回归目前没有包含这些信息所以只能通过误差体现。这正是“模型解释偏差”的意义误差里藏着数据里没告诉你的故事。5.3 特征重要性初探系数背后的业务含义线性回归之所以解释性强是因为系数可以直接当作权重。我把模型的系数对应到特征名上输出排序结果feature_names (numeric_features list(preprocessor.named_transformers_[cat].get_feature_names_out(categorical_features))) coefs pd.Series(model.coef_, indexfeature_names).sort_values(keynp.abs, ascendingFalse) print(coefs)在我的结果里GrLivArea地面以上居住面积的系数最突出而标准化后的系数大小可以直接比较相对重要性面积增加1个标准差对数房价相应增加0.42个标准差。其次是TotalBsmtSF地下室总面积和GarageArea车库面积。而YearBuilt系数为负说明在不考虑其他因素时房龄越新的房子住起来越受人喜欢这在房价上体现为正贡献。这里需要注意系数是“控制其他特征不变”时该特征的影响如果特征之间存在强相关性系数解释会出现偏差这就是下面要说的多重共线性问题。6. 常见问题与排查技巧实录6.1 数据泄露最容易犯且最难察觉的错误我最初做这个项目的时候犯了非常经典的数据泄漏错先用全部数据求均值和标准差再切训练测试集。这种做法的后果是测试集的一部分分布信息在训练阶段就已经“暴露”给模型了导致测试集上的评估过于乐观仿佛模型在“开卷考试”。正确做法是先切分再训练集上拟合格标准化器再用同样的转换参数处理测试集。我前面3.3节代码里已经写了标准写法。检验是否泄漏可以对比训练集和测试集上标准化后的均值如果两者都接近0且方差接近1说明基本正常。更重要的是养成习惯任何变换都要先fit训练集再transform测试集。具体来说不只是标准化还有缺失值填充时的均值、中位数也都要用训练集统计得到而不是用全量数据。真实工作中这一点直接决定模型上线后的真实效果。6.2 多重共线性当特征开始互相说谎如果两个高度相关的特征同时放进线性回归会让系数估计变得不稳定甚至出现正负号反转。比如LotArea和GrLivArea都可能代表房子规模它们的相关系数可能高达0.7以上。这时候LotArea的系数可能被压得很小或者变为负值看起来很不合理。我排查的方法是看方差膨胀因子VIF一般经验阈值是VIF大于10就认为存在明显共线性。简单算一下from statsmodels.stats.outliers_influence import variance_inflation_factor from statsmodels.tools.tools import add_constant X_vif add_constant(X_train_processed) vif pd.Series([variance_inflation_factor(X_vif, i) for i in range(X_vif.shape[1])], indexX_vif.columns) print(vif.sort_values(ascendingFalse))遇到高VIF的特征可以删除其中一个或者用主成分分析降维或者改用Ridge/Lasso正则化模型。在小项目里我直接删除了相关性高且业务重要性相对弱的那个特征模型的R²并没有下降系数却稳定了许多。这里要明白线性回归关注的是“每个特征独立解释的部分”共线性会削弱这种解释力。6.3 过拟合与欠拟合怎么判断模型状态线性回归模型简单但如果特征太多也可能过拟合。这套数据特征不算多我通过对比训练集和测试集的R²来判断如果训练R²很高比如0.98而测试R²低比如0.6基本可以断定过拟合。相反如果训练和测试的R²都很低则要考虑漏掉关键特征或数据关系不是线性。我这次的训练R²是0.894测试R²是0.873差距很小说明模型泛化能力不错。对于线性回归这类低方差模型过拟合通常不严重但一旦采用岭回归或Lasso就需要通过交叉验证调节正则化强度这是后话。有一个实际感受不要一味追求测试R²高有时候高出来的0.01是靠运气而不是模型变强。在小项目里更值得关注的是模型的稳定性和可解释性。6.4 实际踩坑记录三件事值得单独说第一件是np.log1p和np.expm1配对被很多人忽略。如果你目标列做了对数变换评估的时候一定要把预测值反变换回原始量纲。如果直接拿预测的对数价去和原房价对比RMSE计算出来会大得离谱而且“房价的对数误差”不好向业务解释。第二件是One-Hot编码后测试集可能遇到训练集中没出现的类别。我这里就出现过一次“新房型”在测试集中出现但训练集没有的情况。使用OneHotEncoder(handle_unknownignore)能绕过这个问题否则predict时会直接崩溃。第三件是数据排序问题。如果原始数据是按售价排序的直接切分会让训练集全是低价房、测试集全是高价房结果看似评估很差实则是懒惰切分造成的假象。所以在切分数据时要么保证数据是随机顺序要么设置shuffleTruetrain_test_split默认会打乱。这个细节我在初次实验时就被坑过。6.5 快速排查速查表下面是我构建线性回归项目时总结的排查表按症状对号入座症状可能原因排查/解决方案训练得特别好测试特别差数据泄漏或过拟合检查预处理是否只用了训练集尝试正则化预测值普遍偏低目标变量分布偏斜对目标取对数或使用其他变换系数符号不符合业务常识多重共线性计算VIF删除冗余特征测试集出现未知类别导致报错One-Hot编码无法处理新类别设置handle_unknownignore预测结果整体偏移但形状一致模型没有充分学习截距检查是否设置了fit_interceptTrue残差呈喇叭形异方差线性模型不满足假设尝试对y做变换或改用加权回归RMSE特别大且带单位困惑预测值和真实值量纲不一致检查是否忘记逆变换这个表可以打印出来以后做任何回归项目都能帮上忙。7. 项目收尾与进一步扩展方向7.1 这个项目还可以怎么扩展如果你跑完上面的流程觉得不过瘾我建议按下面几个方向二选一或全部做一遍加入正则化用Ridge或Lasso重复这个流程再用交叉验证选择正则化系数看看模型效果和系数稀疏度有什么变化。这是线性回归最自然的进阶。做特征交叉把面积乘以房龄或者把卧室数量与卫生间数量做差尝试手工构造新特征看R²有没有提升。试一下分位数回归它不只预测均值还能预测中位数和上下分位数适合房价这种分布偏斜的数据。这个方向很多人并不知道但面试说起来会非常加分。把模型部署成一个简单的Web接口用Flask或者FastAPI写一个接口传入房屋特征返回预测价格。这个扩展能让项目从“实验”变成“产品”。我自己的选择是做了Ridge回归对比实验结果发现正则化稍微降低了训练R²但测试R²几乎不变说明原模型本身就处于比较健康的偏差-方差平衡状态。这个结论本身也是项目的一种收获。7.2 我的几点实操心得最后说几段真心话。第一线性回归永远值得认真对待即使你现在已经会各种Boosting模型回头重新审视线性回归的假设、诊断和处理流程依然会有收获。机器学习的很多工程问题比如数据泄漏、样本划分、特征变换在线性回归里体现得最纯粹。第二学会“讲数据故事”。单纯跑出0.87的R²并不算完成项目你还要说明哪些特征最重要、误差集中在哪类样本、模型在什么场景下会失效。这些分析能力恰恰是从线性回归这种简单模型开始训练的。第三建议养成写实验记录的习惯。我在做这个项目时每改一个处理步骤都记录下当时的结果和想法。后面回看时能清楚地看到自己的成长轨迹也能避免重复踩坑。如果你也动手做了这个项目不用太在意初始分数高低。重要的是把每个环节为什么要这么做讲清楚。这个过程坚持下来你会发现机器学习里最难的不是算法而是如何正确、诚实地评估“我的模型到底学到了什么”。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进