1. 为什么我建议你从"抄代码顺序"开始学线性回归
很多人第一次接触机器学习,都是从波士顿房价数据集开始的。这个数据集在sklearn里叫load_boston,虽然因为伦理问题在新版本被移除了,但它依然是无数人入门回归任务的第一站。我见过太多人一上来就打开教程,把代码从头到尾复制一遍,跑通了,看到MSE是20几,然后就觉得自己"学会了"。结果换一个数据集,连第一步该干什么都不知道。
问题出在哪?出在你抄的是代码,不是顺序。
线性回归这个模型本身简单到不能再简单——找一条直线(或者超平面),让所有点到这条线的距离平方和最小。数学上就是解一个最小二乘问题,闭式解一写就完事。但真正做一个完整的回归项目,代码编写是有严格顺序的。这个顺序不是随便定的,它背后对应的是数据科学工作流的逻辑:先看清数据长什么样,再决定怎么处理,然后才是建模、评估、调优。顺序错了,后面全是白费功夫。
这篇笔记就是把我自己反复做这个项目时总结出来的代码编写顺序拆开讲。每一步为什么放在这个位置,不放在这个位置会出什么问题,我都会说清楚。适合刚入门机器学习、想搞明白"一个完整项目到底该怎么写"的人,也适合已经跑通过教程但想重新梳理逻辑的人。
2. 项目整体设计与代码顺序拆解
2.1 为什么顺序比代码本身更重要
先讲一个我踩过的坑。刚开始学的时候,我拿到数据第一件事就是train_test_split,然后直接LinearRegression().fit(),最后算MSE。跑出来MSE大概在25左右,我觉得还行。后来有个前辈问我:你看过数据分布吗?特征之间有没有共线性?目标变量有没有异常值?我全答不上来。
这就是顺序错误的典型症状。正确的顺序应该是:先理解数据,再预处理,然后划分数据集,接着建模,最后评估和诊断。每一步都有它存在的理由,跳过任何一步,你得到的模型都是"盲盒"。
具体到波士顿房价这个项目,我推荐的代码编写顺序是这样的:
- 导入库并加载数据
- 数据初探(形状、类型、缺失值、统计描述)
- 探索性数据分析(相关性、分布、异常值)
- 特征工程与预处理
- 划分训练集和测试集
- 建模训练
- 预测与评估
- 残差诊断与模型改进
这个顺序的核心逻辑是:信息从粗到细,操作从安全到冒险。先做不会破坏数据的操作(看、统计),再做会改变数据的操作(标准化、删列),最后才是建模。如果你反过来,先建模再回头看数据,很可能发现特征里有个ID列混进去了,或者目标变量有极端值把模型带偏了,这时候前面的工作全白做。
2.2 波士顿房价数据集的特点与项目定位
波士顿房价数据集一共506个样本,13个特征,目标变量是MEDV,也就是自有住房的中位数价值(单位是千美元)。13个特征包括犯罪率、住宅用地比例、非零售商业用地比例、是否临河、一氧化氮浓度、平均房间数、房龄、到就业中心距离、公路可达性指数、房产税率、师生比,以及两个关于黑人比例的统计量。
这个数据集有几个特点值得注意。第一,样本量很小,506条,放在今天动辄几十万条的数据集里简直微不足道。第二,特征量纲差异极大,犯罪率是小数,房产税率是几百,房间数是几位数。第三,目标变量MEDV在50处有截断,也就是说房价超过5万美元的样本被统一记为50,这会导致模型在高价段预测偏差。
这些特点决定了这个项目的定位:它是一个教学性质的回归任务,不是工业级应用。所以我们的重点不是把MSE压到多低,而是把流程走完整、走正确。理解了这一点,你就不会纠结于"为什么我的MSE比别人高0.5"这种问题。
2.3 工具选型与依赖说明
这个项目用到的库很基础:numpy做数值计算,pandas做数据操作,matplotlib和seaborn做可视化,sklearn做建模和评估。不需要深度学习框架,不需要GPU,一台普通笔记本几分钟就能跑完。
版本方面,注意load_boston在sklearn 1.2之后被移除了。如果你用的是新版本,有两个选择:一是降级到1.1版本,二是用fetch_openml从OpenML拉取,或者直接用CSV文件。我建议用CSV文件,因为这样你能练习数据加载的完整流程,而不是依赖sklearn的内置数据集。
import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from sklearn.model_selection import train_test_split, cross_val_score from sklearn.linear_model import LinearRegression, Ridge, Lasso from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score这段导入代码看起来简单,但顺序有讲究。我习惯把标准库放最上面,第三方库按numpy、pandas、可视化、sklearn的顺序排列。不是为了好看,而是当项目变大时,你能快速定位某个库有没有导入。
3. 核心细节解析与实操要点
3.1 数据加载与初探:别急着建模
数据加载这一步,很多人就是一行pd.read_csv()完事。但我建议你至少做三件事:看形状、看类型、看缺失。
df = pd.read_csv('boston.csv') print(df.shape) print(df.dtypes) print(df.isnull().sum()) print(df.describe())shape告诉你样本量和特征数,dtypes告诉你有没有被误读成字符串的数值列,isnull().sum()告诉你缺失情况,describe()给你每个特征的均值、标准差、最小值、四分位数和最大值。
为什么这一步不能省?因为波士顿数据集虽然干净,但你要养成习惯。我见过太多真实项目,数据加载进来发现某一列全是NaN,或者数值列被读成了object类型,后面建模直接报错。提前花30秒检查,能省你半小时debug。
describe()的输出特别值得细看。比如你会发现ZN(住宅用地比例)的75分位数是12.5,但最大值是100,说明这个特征严重右偏。CRIM(犯罪率)的均值是3.6,最大值是88.9,也是极端右偏。这些信息在后面的特征工程里会用到。
3.2 探索性数据分析:三个必看的图
EDA这一步,我不建议你画十几张图,浪费时间。三个图就够了:目标变量分布图、特征相关性热力图、目标变量与关键特征的散点图。
目标变量分布图用直方图加核密度曲线:
sns.histplot(df['MEDV'], kde=True) plt.show()你会看到MEDV在50处有一个明显的尖峰,这就是截断效应。这意味着模型在高价段会系统性低估,因为真实价格被"压"到了50。这个发现会影响你后面评估模型时的判断——不要只看整体MSE,要分段看。
相关性热力图用df.corr()加sns.heatmap()。重点看两件事:哪些特征和目标变量相关性高,哪些特征之间相关性高。波士顿数据集里,RM(平均房间数)和MEDV的相关性大概在0.7左右,LSTAT(低地位人口比例)和MEDV的相关性大概在-0.74。这两个是最强的预测特征。特征之间,TAX和RAD的相关性很高,接近0.9,说明存在共线性。共线性不会让线性回归的预测变差,但会让系数估计不稳定,解释性变差。
散点图选RM和LSTAT分别对MEDV画。你会看到RM和MEDV大致线性正相关,但LSTAT和MEDV的关系有点非线性,可能是曲线。这提示你后面可以考虑加多项式项,但线性回归作为基线,先不加。
注意:EDA阶段不要做任何数据修改。看就是看,改就是改,混在一起你会忘记自己改过什么。
3.3 特征工程与预处理:标准化不是必须的
很多人以为线性回归必须做标准化,其实不是。线性回归的闭式解对特征缩放不敏感,因为缩放只是改变了系数的尺度,不影响预测值。但如果你用梯度下降求解,或者用正则化(Ridge、Lasso),标准化就是必须的,因为正则项对系数大小敏感。
波士顿数据集我建议做标准化,原因有两个:一是特征量纲差异大,标准化后系数可以直接比较重要性;二是后面如果要试Ridge,不用重新处理。
X = df.drop('MEDV', axis=1) y = df['MEDV'] scaler = StandardScaler() X_scaled = scaler.fit_transform(X)这里有个关键细节:fit_transform只能在训练集上做,测试集只能用transform。如果你在划分数据集之前就标准化了,测试集的信息就"泄露"到了训练过程中,这叫数据泄露。正确顺序是先划分,再在训练集上fit,然后transform测试集。
异常值处理方面,波士顿数据集里有一些MEDV等于50的样本,还有一些特征值极端大的样本。我建议不要直接删,而是先标记出来,建模后看残差。如果这些点残差特别大,再考虑处理。
3.4 划分数据集:随机种子与分层
X_train, X_test, y_train, y_test = train_test_split( X_scaled, y, test_size=0.2, random_state=42 )random_state设成42是惯例,目的是让结果可复现。test_size=0.2意味着101个测试样本,405个训练样本。对于506条数据来说,这个比例合理。如果你数据量很大,可以设0.1;如果数据量很小,可以设0.3。
回归任务一般不用分层抽样,因为目标变量是连续的,分层没有明确标准。但如果你发现目标变量分布很不均匀,可以先分箱再分层。波士顿数据集的目标变量分布还算正常,直接随机划分就行。
实操心得:划分数据集后,立刻把
X_train、X_test、y_train、y_test保存成文件。这样你后面调参、换模型时不用重新跑前面的步骤,省时间。
4. 实操过程与核心环节实现
4.1 建模训练:从最简单开始
建模这一步,我的原则是从最简单、最可解释的模型开始。线性回归就是最好的起点。
lr = LinearRegression() lr.fit(X_train, y_train) print(lr.intercept_) print(lr.coef_)intercept_是截距,coef_是13个特征的系数。因为做了标准化,系数的大小可以直接反映特征的重要性。你会发现RM的系数是正的且较大,LSTAT的系数是负的且绝对值较大,这和EDA阶段的发现一致。
这里有个细节:coef_的顺序和X的列顺序一致。如果你后面改了列顺序,记得同步更新特征名列表,否则解释系数时会搞混。
4.2 预测与评估:三个指标一起看
y_pred = lr.predict(X_test) mse = mean_squared_error(y_test, y_pred) rmse = np.sqrt(mse) mae = mean_absolute_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f'MSE: {mse:.2f}, RMSE: {rmse:.2f}, MAE: {mae:.2f}, R2: {r2:.2f}')MSE是均方误差,单位是目标变量单位的平方,不好解释。RMSE是它的平方根,单位回到千美元,可以解释为"平均预测偏差大约是多少千美元"。MAE是平均绝对误差,对异常值不敏感。R2是决定系数,表示模型解释了目标变量多少比例的方差。
波士顿数据集上,线性回归的RMSE大概在4.5到5.5之间,R2大概在0.65到0.75之间。如果你的结果差很多,检查一下是不是忘了标准化,或者数据划分有问题。
注意:不要只报一个指标。MSE对异常值敏感,MAE对异常值不敏感,R2看整体拟合。三个一起看,才能全面判断模型表现。
4.3 残差诊断:模型有没有系统性偏差
评估指标告诉你模型"平均"表现如何,残差诊断告诉你模型"哪里"表现不好。
residuals = y_test - y_pred plt.scatter(y_pred, residuals) plt.axhline(y=0, color='r', linestyle='--') plt.xlabel('Predicted') plt.ylabel('Residuals') plt.show()理想的残差图应该是随机分布在0线两侧,没有明显模式。如果你看到残差随着预测值增大而增大(喇叭形),说明存在异方差性。如果你看到残差和预测值有曲线关系,说明模型欠拟合,可能需要加非线性项。
波士顿数据集上,你大概率会看到在预测值等于50附近,残差是负的且比较大。这就是截断效应导致的,模型预测不出超过50的值,但真实值被截断在50,所以残差为负。
4.4 模型改进:Ridge和Lasso的对比
线性回归跑通后,可以试试正则化版本。Ridge加L2正则,Lasso加L1正则。L1可以让部分系数变成0,起到特征选择的作用。
ridge = Ridge(alpha=1.0) ridge.fit(X_train, y_train) y_pred_ridge = ridge.predict(X_test) lasso = Lasso(alpha=0.1) lasso.fit(X_train, y_train) y_pred_lasso = lasso.predict(X_test)alpha是正则化强度,越大正则越强。我建议用交叉验证选alpha,而不是手动试。
from sklearn.linear_model import RidgeCV ridge_cv = RidgeCV(alphas=[0.01, 0.1, 1.0, 10.0], cv=5) ridge_cv.fit(X_train, y_train) print(ridge_cv.alpha_)交叉验证的思路是把训练集再分成5份,每次用4份训练、1份验证,轮流5次,取平均误差最小的alpha。这样选出来的alpha比手动试更可靠。
对比三个模型的测试集RMSE,你会发现Ridge和Lasso通常比普通线性回归略好,但提升有限。这是因为波士顿数据集的特征共线性不算特别严重,正则化的收益不大。但这个对比过程本身很有价值,它让你理解正则化什么时候有用、什么时候没用。
5. 常见问题与排查技巧实录
5.1 数据加载与预处理阶段的坑
问题一:load_boston报错说被移除了。这是sklearn 1.2之后的正常行为。解决方案是用CSV文件,或者用fetch_openml(name='boston', version=1)。我推荐CSV,因为你能控制数据加载的每一步。
问题二:标准化后系数看不懂了。标准化后的系数表示"特征每变化一个标准差,目标变量变化多少个单位"。如果你想要原始尺度的系数,用scaler.inverse_transform反变换,或者干脆不标准化,直接用原始数据拟合。
问题三:测试集MSE远大于训练集MSE。这是过拟合的典型症状。但对于线性回归,过拟合通常不严重,因为模型复杂度低。如果差距很大,检查是不是数据泄露了,比如标准化时用了全部数据。
5.2 建模与评估阶段的坑
问题四:R2是负数。R2为负意味着模型比"直接用均值预测"还差。这通常发生在模型严重欠拟合,或者测试集分布和训练集差异很大时。检查一下特征有没有选错,或者数据划分是不是随机的。
问题五:系数符号和预期相反。比如RM(房间数)的系数是负的,这不合常理。原因通常是共线性。RM和LSTAT可能相关,导致系数估计不稳定。解决方案是删掉一个相关特征,或者用Ridge。
问题六:残差图有规律。如果残差和预测值呈曲线关系,说明线性模型不够,需要加多项式项。如果残差呈喇叭形,说明需要做目标变量变换,比如取对数。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| MSE异常大 | 特征未标准化、数据泄露、异常值 | 检查预处理顺序、看数据分布 | 标准化、修正划分、处理异常值 |
| R2为负 | 模型欠拟合、测试集分布不同 | 对比训练集和测试集统计量 | 增加特征、检查划分随机性 |
| 系数符号异常 | 特征共线性 | 计算VIF、看相关矩阵 | 删特征、用Ridge |
| 残差有规律 | 模型欠拟合、异方差 | 画残差图 | 加多项式、变换目标变量 |
| 预测值集中在50附近 | 目标变量截断 | 看目标变量直方图 | 分段评估、考虑截断回归 |
独家避坑技巧:每次跑完模型,把
y_test和y_pred保存成CSV。这样你后面想换模型对比时,不用重新跑前面的步骤,直接加载预测结果算指标就行。我习惯用pd.DataFrame({'true': y_test, 'pred': y_pred}).to_csv('pred_lr.csv'),简单但省事。
5.4 交叉验证的实操细节
交叉验证是选超参数的标准方法,但有几个细节容易忽略。第一,cross_val_score默认用R2作为评分,如果你想用MSE,要显式指定scoring='neg_mean_squared_error'。注意是负的,因为sklearn的评分函数统一是"越大越好"。第二,交叉验证的折数cv,小数据集用5折或10折,大数据集用3折就够。第三,交叉验证要在训练集上做,不要碰测试集。
scores = cross_val_score(lr, X_train, y_train, cv=5, scoring='neg_mean_squared_error') rmse_scores = np.sqrt(-scores) print(f'CV RMSE: {rmse_scores.mean():.2f} +/- {rmse_scores.std():.2f}')这个输出告诉你模型在训练集上的泛化能力。如果CV RMSE和测试集RMSE差距很大,说明数据划分有问题,或者数据量太小导致波动大。
6. 从线性回归延伸到更实用的建模思路
线性回归跑通之后,你可以顺着这个项目往几个方向延伸。第一个方向是多项式回归,给RM和LSTAT加平方项,看能不能捕捉非线性关系。第二个方向是正则化调参,用RidgeCV和LassoCV自动选alpha,对比哪个模型在测试集上更稳。第三个方向是特征选择,用Lasso的系数为0的特性,筛掉不重要的特征,看简化后的模型表现如何。
但我想强调的是,这个项目的价值不在于把MSE从5.0降到4.8,而在于你通过它建立了一套可复用的回归项目流程。这套流程包括:数据初探、EDA、预处理、划分、建模、评估、诊断、改进。每一步都有明确的输入和输出,每一步都有检查点。你把这个流程走熟之后,换任何回归数据集,都能快速上手。
我个人在实际操作中的体会是,初学者最容易犯的错误是"跳步"。看到数据就想建模,建完模就想调参,调完参就想换模型。结果每一步都是半吊子,出了问题也不知道是哪一步的锅。线性回归这个项目简单,正好用来练"不跳步"的习惯。你把这个习惯养成了,后面学更复杂的模型,只是换工具,流程是不变的。
最后分享一个小技巧:每次做完一个项目,把代码整理成函数或者类。比如load_data()、explore_data()、preprocess()、train_model()、evaluate()。这样下次做类似项目,直接调用函数,不用重新写。我自己的回归项目模板就是这么攒出来的,现在跑一个新数据集,从加载到出评估报告,20分钟搞定。