
1. 项目缘起与整体设计思路波士顿房价数据集算得上是机器学习回归任务里的“Hello World”了。我最早接触它是在带一个数据分析入门小组的时候当时想找一个既有真实感、又不会在数据清洗上耗费太多精力的数据集试了几个之后还是回到了这个经典选项。它的特征维度适中——13个自变量目标变量是区域房价中位数样本量五百出头跑一次线性回归在普通笔记本上几秒钟就出结果反馈极快非常适合用来训练“从读数据到出模型”的完整代码编写顺序。这个项目的核心目标不是把预测精度刷到多高而是通过一个完整的线性回归流程把代码编写的先后顺序刻进肌肉记忆里。很多人学机器学习的时候容易陷入一个误区拿到数据就急着fit跑出个结果就完事。但真正做过项目的人都知道代码顺序错了后面调参调到天亮也白搭。比如你先把数据标准化了再做训练集划分看起来只是两行代码的顺序问题实际上已经引入了数据泄露模型评估结果会偏乐观上线之后直接翻车。所以这个训练笔记的重点在于用波士顿房价数据作为载体把线性回归项目的代码编写顺序梳理清楚每一步为什么放在这个位置、换顺序会出什么问题都讲透。适合刚入门机器学习、已经会写Python但不知道项目代码该怎么组织的人也适合做过一些项目但想回头夯实流程基础的读者。下面我会按照实际编码的顺序从导入库开始一步步走到模型评估和残差分析中间穿插参数选择的计算过程和踩坑经验。整个流程的设计遵循一条原则先理解数据再划分数据然后预处理接着训练最后评估。这条链路看起来简单但每一步都有讲究。我见过太多人把标准化放在划分之前或者用整个数据集去计算均值和方差结果测试集的分布信息泄露到了训练过程中。线性回归虽然是个简单模型但流程上的严谨性和深度神经网络是一模一样的。2. 核心环节拆解与实操要点2.1 数据加载与初步探查的正确姿势拿到数据的第一步永远是看而不是跑模型。波士顿房价数据集可以通过sklearn.datasets.load_boston加载不过这个接口在新版本中因为伦理原因被移除了你可以用fetch_openml或者直接读CSV文件。我一般习惯把数据转成DataFrame这样列名和数据类型一目了然。import pandas as pd import numpy as np from sklearn.datasets import fetch_openml # 加载数据 boston fetch_openml(nameboston, version1, as_frameTrue) df boston.frame print(df.shape) print(df.head()) print(df.describe()) print(df.isnull().sum())这几行代码看起来平淡无奇但顺序不能乱。先看形状确认样本量和特征数再看前几行感受数据的量纲和类型然后看描述性统计检查有没有异常值或者量纲差异过大的特征最后查缺失值。我试过跳过describe()直接进入建模结果发现某个特征的范围是0到1另一个是0到100线性回归的系数解释完全被大尺度特征主导了后来回头补标准化才解决。注意fetch_openml返回的数据类型可能全是category需要手动转成float。我一般用df df.astype(float)统一处理但要注意目标列如果被转成了类别型得单独拿出来。2.2 特征与目标的分离逻辑数据探查完之后下一步是把特征矩阵和目标向量分开。这个顺序不能颠倒因为后面的划分和预处理都是针对特征矩阵操作的目标向量只是跟着走。X df.drop(MEDV, axis1) y df[MEDV]这里有个细节drop的时候要确认目标列的列名。波士顿房价数据集的目标列通常叫MEDV但不同来源可能命名不同。我建议先用df.columns打印一下列名确认无误再操作。另外axis1表示按列删除这个参数如果写错成axis0会把整行删掉数据直接报废。特征和目标分离之后建议立刻检查一下X和y的维度是否匹配。X.shape[0]应该等于y.shape[0]如果不相等说明分离过程中出了问题。这个检查只需要一行代码但能帮你省下后面调试的半小时。2.3 训练集与测试集的划分时机划分训练集和测试集是整个流程中最容易被轻视的一步。很多人觉得这就是个形式随便设个test_size0.2就完事了。但实际上划分的时机和参数选择直接影响后续所有步骤的有效性。划分必须在任何预处理之前完成。原因很简单标准化的均值和方差、缺失值的填充策略、异常值的处理阈值这些都应该只从训练集中学习然后应用到测试集上。如果你先对全量数据做了标准化再划分测试集的分布信息就已经泄露到训练过程中了模型在测试集上的表现会虚高。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )random_state这个参数我强烈建议固定下来。不固定的话每次运行划分结果都不一样模型评估指标会波动你根本分不清是模型改了有效果还是数据划分碰巧好了。42这个数字没什么特殊含义只是社区习惯你用别的也行关键是整个项目里保持一致。test_size的选择取决于样本量。波士顿房价只有506条数据20%的测试集大约是101条这个量级做评估勉强够用。如果样本量更小比如只有一百多条那可能要降到10%甚至用交叉验证。样本量大的话30%也无所谓。核心原则是测试集要足够大能稳定反映模型泛化能力又不能太大浪费训练数据。2.4 标准化处理的参数计算与选择线性回归本身对特征尺度不敏感因为它的损失函数是均方误差系数的尺度会自适应调整。但如果你用了正则化Ridge、Lasso或者后续想换梯度下降优化器标准化就是必须的。另外标准化之后系数的绝对值可以直接比较方便做特征重要性分析。标准化的公式是$$z \frac{x - \mu}{\sigma}$$其中$\mu$是训练集均值$\sigma$是训练集标准差。注意这里的关键均值和标准差只能从训练集计算然后应用到测试集。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)fit_transform和transform的区别是新手最容易搞混的地方。fit_transform做了两件事计算训练集的均值和方差然后对训练集做标准化。transform只做一件事用已经计算好的均值和方差对测试集做标准化。如果你对测试集也用了fit_transform那就等于用测试集自己的均值和方差来标准化训练集和测试集的尺度基准不一致模型评估就失去意义了。实操心得我习惯在标准化之后打印一下X_train_scaled.mean()和X_train_scaled.std()确认均值接近0、标准差接近1。如果某个特征的标准化结果异常比如标准差还是很大可能是该特征存在极端异常值需要回头检查。3. 模型训练与评估的完整实现3.1 线性回归模型的拟合与系数解读到了这一步数据已经准备好了可以开始训练模型。线性回归的API非常简单但简单不代表没有坑。from sklearn.linear_model import LinearRegression lr LinearRegression() lr.fit(X_train_scaled, y_train) print(截距:, lr.intercept_) print(系数:, lr.coef_)fit之后模型就学到了每个特征的权重。因为之前做了标准化这些系数的绝对值可以直接比较绝对值越大说明该特征对房价的影响越强。我实测下来RM房间数和LSTAT低收入人群比例的系数绝对值通常最大这也符合直觉——房间越多房价越高低收入人群比例越高房价越低。截距项intercept_表示所有特征都为0时的预测值。在标准化之后特征为0意味着该特征等于训练集均值所以截距实际上是“平均特征条件下的平均房价”。这个解释在业务沟通时很有用。3.2 预测与评估指标的计算过程模型训练完之后先别急着看测试集先在训练集上做一次预测看看模型有没有欠拟合。from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score y_train_pred lr.predict(X_train_scaled) y_test_pred lr.predict(X_test_scaled) print(训练集MSE:, mean_squared_error(y_train, y_train_pred)) print(测试集MSE:, mean_squared_error(y_test, y_test_pred)) print(训练集R2:, r2_score(y_train, y_train_pred)) print(测试集R2:, r2_score(y_test, y_test_pred))MSE均方误差的单位是目标变量的平方解释起来不太直观。我一般会再算一个RMSE均方根误差单位就和房价一致了。rmse_train np.sqrt(mean_squared_error(y_train, y_train_pred)) rmse_test np.sqrt(mean_squared_error(y_test, y_test_pred)) print(训练集RMSE:, rmse_train) print(测试集RMSE:, rmse_test)R2分数表示模型解释了目标变量多少比例的方差。波士顿房价的线性回归R2通常在0.7左右也就是说模型解释了70%左右的房价变化。这个数字不算高但考虑到只用了线性模型和13个特征已经可以接受了。评估指标的计算顺序也有讲究先算训练集的再算测试集的。如果训练集R2远高于测试集说明过拟合如果两者都很低说明欠拟合。我见过有人只算测试集指标结果模型过拟合了都不知道。3.3 残差分析与模型诊断评估指标只是给了你一个数字残差分析才能告诉你模型哪里出了问题。残差就是真实值减去预测值理想情况下残差应该随机分布在0附近没有明显的模式。import matplotlib.pyplot as plt residuals y_test - y_test_pred plt.figure(figsize(10, 6)) plt.scatter(y_test_pred, residuals, alpha0.6) plt.axhline(y0, colorr, linestyle--) plt.xlabel(预测值) plt.ylabel(残差) plt.title(残差 vs 预测值) plt.show()如果残差图呈现漏斗形预测值越大残差越大说明存在异方差性可能需要用加权最小二乘法或者对目标变量做变换。如果残差呈现U形或倒U形说明线性模型可能不够需要考虑多项式回归。波士顿房价数据集的残差图我实测下来在高房价区域有一些偏大的残差说明模型对高价房的预测能力偏弱。另外还可以画QQ图检查残差的正态性import scipy.stats as stats plt.figure(figsize(8, 6)) stats.probplot(residuals, distnorm, plotplt) plt.title(残差QQ图) plt.show()QQ图上的点如果大致沿着对角线分布说明残差接近正态分布线性回归的统计假设成立。如果偏离严重置信区间和假设检验的结果就不可靠了。3.4 交叉验证的代码实现与参数选择单次划分的评估结果有一定随机性交叉验证可以给出更稳定的估计。K折交叉验证把训练集分成K份每次用K-1份训练、1份验证循环K次。from sklearn.model_selection import cross_val_score cv_scores cross_val_score( lr, X_train_scaled, y_train, cv5, scoringneg_mean_squared_error ) rmse_cv np.sqrt(-cv_scores) print(交叉验证RMSE:, rmse_cv) print(平均RMSE:, rmse_cv.mean()) print(标准差:, rmse_cv.std())cv5表示5折交叉验证这是最常用的选择。K太小比如3训练集不够充分K太大比如10计算开销大且每折的训练集差异小收益递减。5折是个平衡点。scoringneg_mean_squared_error返回的是负MSE因为sklearn的交叉验证默认是“分数越大越好”而MSE是越小越好所以取了负号。这个设计一开始让我困惑了很久后来习惯了就好。交叉验证的标准差反映了模型在不同数据子集上的稳定性。如果标准差很大说明模型对数据划分敏感可能需要更多数据或者更简单的模型。4. 常见问题与排查技巧实录4.1 数据泄露的典型场景与排查方法数据泄露是机器学习项目中最隐蔽也最致命的错误。除了前面提到的标准化顺序问题还有几个常见场景场景一用全量数据做特征选择。比如你先用所有数据计算了特征与目标的相关性选了相关性最高的5个特征然后再划分训练集和测试集。这时候测试集的信息已经通过特征选择过程泄露了。正确做法是在训练集上做特征选择然后应用到测试集。场景二时间序列数据的随机划分。波士顿房价虽然不是时间序列但如果你处理的是时间相关数据随机划分会让未来数据泄露到训练集中。正确做法是按时间顺序划分。场景三目标编码泄露。如果你对类别特征做目标编码用目标变量的均值替换类别必须只在训练集上计算均值然后映射到测试集。用全量数据计算均值就是泄露。排查数据泄露的方法如果模型在测试集上的表现异常好好到不符合常理先怀疑数据泄露。另外可以做一个“泄露检测”故意打乱测试集的标签如果模型性能没有明显下降说明测试集信息已经泄露到训练过程中了。4.2 多重共线性的识别与处理线性回归假设特征之间相互独立但波士顿房价数据集中有几个特征相关性很高比如TAX税率和RAD辐射指数的相关系数通常在0.9以上。多重共线性会导致系数估计不稳定换一组数据系数符号可能就反了。识别多重共线性的方法from statsmodels.stats.outliers_influence import variance_inflation_factor vif_data pd.DataFrame() vif_data[feature] X.columns vif_data[VIF] [variance_inflation_factor(X_train_scaled, i) for i in range(X_train_scaled.shape[1])] print(vif_data)VIF方差膨胀因子大于10通常认为存在严重多重共线性。处理方法是删除相关性高的特征之一或者改用Ridge回归。Ridge回归通过L2正则化压缩系数可以缓解共线性问题。from sklearn.linear_model import Ridge ridge Ridge(alpha1.0) ridge.fit(X_train_scaled, y_train) print(Ridge测试集R2:, ridge.score(X_test_scaled, y_test))alpha是正则化强度越大惩罚越重。我一般从1.0开始试然后根据交叉验证结果调整。4.3 异常值对线性回归的影响与处理线性回归对异常值非常敏感因为损失函数是平方误差一个极端异常值可以大幅拉动回归线。波士顿房价数据集中目标变量MEDV在50处有截断原本是50万美金以上的房价被统一记为50这导致高房价区域的预测偏差较大。检测异常值的方法# 用IQR方法检测 Q1 df[MEDV].quantile(0.25) Q3 df[MEDV].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR outliers df[(df[MEDV] lower_bound) | (df[MEDV] upper_bound)] print(异常值数量:, len(outliers))处理异常值的方式有几种直接删除、用中位数替换、或者对目标变量做对数变换。我试过对MEDV做对数变换残差的正态性明显改善R2也有小幅提升。但要注意对数变换后预测值需要做指数还原还原后的预测值是有偏的需要做修正。4.4 常见问题速查表问题现象可能原因排查方法解决方案测试集R2远低于训练集过拟合对比训练集和测试集指标增加正则化、减少特征、增加数据训练集和测试集R2都很低欠拟合检查特征与目标的相关性增加特征、改用非线性模型系数符号与直觉相反多重共线性计算VIF删除相关特征、改用Ridge残差图呈漏斗形异方差性画残差vs预测值图加权最小二乘、目标变量变换交叉验证标准差大模型不稳定检查数据分布增加数据、简化模型标准化后均值不为0计算错误打印均值和标准差检查fit_transform和transform的使用避坑技巧每次修改代码后重新运行整个流程不要只跑修改的那一段。我踩过的坑是改了标准化方式但忘了重新划分数据结果评估指标对不上排查了半天才发现是中间变量没更新。5. 代码组织与工程化建议5.1 函数封装与流程编排把整个流程写成一个脚本虽然能跑但不利于复用和调试。我习惯把每个步骤封装成函数然后用一个主函数串联起来。def load_data(): boston fetch_openml(nameboston, version1, as_frameTrue) df boston.frame.astype(float) return df def split_features_target(df, target_colMEDV): X df.drop(target_col, axis1) y df[target_col] return X, y def split_train_test(X, y, test_size0.2, random_state42): return train_test_split(X, y, test_sizetest_size, random_staterandom_state) def scale_features(X_train, X_test): scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) return X_train_scaled, X_test_scaled, scaler def train_model(X_train, y_train): model LinearRegression() model.fit(X_train, y_train) return model def evaluate_model(model, X_train, y_train, X_test, y_test): train_pred model.predict(X_train) test_pred model.predict(X_test) metrics { train_rmse: np.sqrt(mean_squared_error(y_train, train_pred)), test_rmse: np.sqrt(mean_squared_error(y_test, test_pred)), train_r2: r2_score(y_train, train_pred), test_r2: r2_score(y_test, test_pred) } return metrics这样封装的好处是每个函数只做一件事测试和调试都方便流程清晰别人看你的代码能快速理解换数据集的时候只需要改load_data函数。5.2 随机种子的统一管理随机种子散落在各处是代码复现性的大敌。我建议在脚本开头定义一个全局种子所有需要随机的地方都引用它。RANDOM_SEED 42 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_stateRANDOM_SEED )这样如果有一天你想换一个种子看看结果稳定性只需要改一个地方。另外交叉验证的cv参数如果用了KFold对象也要设置random_state。5.3 结果记录与实验对比做项目的时候经常需要对比不同参数的效果比如标准化和不标准化的对比、不同test_size的对比。我习惯用一个列表把每次实验的结果存下来最后转成DataFrame对比。results [] for test_size in [0.15, 0.2, 0.25, 0.3]: X_train, X_test, y_train, y_test train_test_split( X, y, test_sizetest_size, random_stateRANDOM_SEED ) X_train_scaled, X_test_scaled, _ scale_features(X_train, X_test) model train_model(X_train_scaled, y_train) metrics evaluate_model(model, X_train_scaled, y_train, X_test_scaled, y_test) metrics[test_size] test_size results.append(metrics) results_df pd.DataFrame(results) print(results_df)这种对比方式比每次手动改参数、记结果要高效得多而且不容易出错。5.4 模型持久化与复现训练好的模型可以保存到磁盘下次直接加载使用不用重新训练。import joblib joblib.dump(model, linear_regression_model.pkl) joblib.dump(scaler, standard_scaler.pkl) # 加载 loaded_model joblib.load(linear_regression_model.pkl) loaded_scaler joblib.load(standard_scaler.pkl)注意保存模型的同时也要保存标准化器因为预测新数据时需要先用同样的标准化器做变换。只保存模型不保存标准化器加载后预测结果会完全错误。6. 从线性回归延伸的优化方向线性回归跑通之后可以尝试几个优化方向来提升效果。第一个是特征工程比如对RM做平方变换、对LSTAT做对数变换捕捉非线性关系。第二个是正则化用Ridge或Lasso回归通过交叉验证选择最优的alpha。第三个是多项式回归用PolynomialFeatures生成交互项和平方项但要注意特征维度爆炸的问题。from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import Pipeline pipeline Pipeline([ (poly, PolynomialFeatures(degree2, include_biasFalse)), (scaler, StandardScaler()), (ridge, Ridge(alpha1.0)) ]) pipeline.fit(X_train, y_train) print(多项式Ridge测试集R2:, pipeline.score(X_test, y_test))用Pipeline的好处是把多个步骤串在一起交叉验证的时候不会出现数据泄露。fit的时候Pipeline会自动按顺序执行每个步骤predict的时候也会自动做同样的变换。我实测下来二次多项式加Ridge的R2能到0.85左右比纯线性回归有明显提升。但特征数从13个膨胀到100多个解释性下降了不少。所以如果项目目标是解释特征影响纯线性回归更合适如果目标是预测精度多项式加正则化是更好的选择。最后分享一个小技巧每次跑完模型把系数和对应的特征名整理成表格按绝对值排序这样一眼就能看出哪些特征最重要。这个习惯在做特征筛选和业务汇报的时候特别有用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。