简介:基于线性回归实现波士顿房价预测的项目源码,是一份面向机器学习初学者的完整期末作业。项目已获得导师指导并通过审核,最终得分九十七分,适合作为课程设计、期末大作业或线性回归算法练手参考。压缩包共十二个文件,核心是三个Python脚本,分别完成线性回归模型的训练、评估与预测,另含六张过程可视化图片、一个已训练好的joblib模型文件以及README说明文档,整体大小约为733千字节,解压后即可运行。读者可结合代码与说明文档复现波士顿房价预测流程,学习从数据读取、特征处理到利用sklearn构建线性回归模型并完成评估的完整思路,同时通过可视化图像直观理解特征与房价之间的线性关系。目前已有1123人学习/下载,目录结构清晰,实用性和可操作性都比较强。
1. 波士顿房价预测为什么绕不开线性回归
期末大作业拿 97 分,靠的不是调参,而是把线性回归算法在波士顿房价预测这条链路上每一步都做对了。这个数据集虽然 2019 年后被 scikit-learn 移出,但作为课程设计和期末大作业的标配,它依然是最适合讲透回归问题的素材。原因是特征有真有假、有连续有离散,目标变量 MEDV 接近正态但不完全正态,样本量只有 506 条,恰好能暴露过拟合、共线性、离群点三类经典问题。本文不是纸上谈兵,而是按一份可直接运行的源码包拆解,从数据清洗、模型训练到评估诊断,全部给命令和代码。适合两类人:一是正在做期末大作业的学生,可以直接对着复现;二是写过分类模型但没系统做过回归项目的开发者,重点看第四章的交叉验证和第五章的残差诊断,能补上你平时容易忽略的环节。
2. 数据来源与特征工程:先搞清楚 13 个特征里哪些能信
2.1 波士顿房价数据集的前世今生与读取方式
波士顿房价数据集由 Harrison 和 Rubinfeld 于 1978 年收集,包含 506 条样本,每条样本记录波士顿地区不同街区的房产信息。原始数据里有 14 个字段,其中 MEDV(自有住房价格中位数,单位千美元)是目标变量,其余 13 个是特征。值得注意的是,sklearn 1.2 版本已经移除了load_boston()接口,理由是数据集包含可能引发伦理讨论的特征 B(非裔美国人比例),所以现在的项目源码里一般会内置一份 CSV 文件,直接pd.read_csv读取。
常见做法是项目根目录下放一份 boston_housing.csv,列名已改成可读形式。读取和数据探查的代码很固定:
import pandas as pd import numpy as np df = pd.read_csv('boston_housing.csv') print(df.shape) # (506, 14) print(df.head()) # 检查缺失值 print(df.isnull().sum()) # 目标变量分布 print(df['MEDV'].describe())这段代码做了三件事:确认样本量和列数、检查是否有空值、观察目标变量的分布形态。describe()输出的四分位数和均值中位数对比能快速判断数据是否偏态——如果均值明显大于中位数,说明存在右偏或离群值,这在 MEDV 上确实存在,因为部分高价位街区把均值拉高了。
2.2 特征含义速查表
13 个特征里有连续型、离散型、二值型,理解它们的物理含义比直接扔进模型更重要,尤其是答辩时老师大概率会逐个问。
| 特征名 | 含义 | 数据类型 | 对房价的预期影响 |
|---|---|---|---|
| CRIM | 城镇人均犯罪率 | 连续 | 负向 |
| ZN | 占地面积超过 25000 平方英尺的住宅用地比例 | 连续 | 弱正向 |
| INDUS | 城镇非零售商业用地比例 | 连续 | 负向 |
| CHAS | 是否邻近查理斯河(1/0) | 二值 | 正向 |
| NOX | 一氧化氮浓度 | 连续 | 负向 |
| RM | 平均房间数 | 离散 | 强正向 |
| AGE | 1940 年前建成的自住单位比例 | 连续 | 负向 |
| DIS | 到波士顿五个就业中心的加权距离 | 连续 | 正向 |
| RAD | 径向高速公路可达性指数 | 离散 | 负向 |
| TAX | 每 10000 美元的不动产税率 | 连续 | 负向 |
| PTRATIO | 城镇学生与教师比例 | 连续 | 负向 |
| B | 黑人比例的变换值 | 连续 | 弱正向 |
| LSTAT | 低收入人群占比 | 连续 | 强负向 |
2.3 相关性分析与离群点处理
在做线性回归之前,先看特征与目标的相关性矩阵,这一步能筛掉明显无关的特征,也能预判多重共线性问题。一般情况下用corr()和热力图完成:
import matplotlib.pyplot as plt import seaborn as sns corr_matrix = df.corr().abs() # 与 MEDV 相关性最高的特征 print(corr_matrix['MEDV'].sort_values(ascending=False)) plt.figure(figsize=(10, 8)) sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='RdBu_r') plt.show()从相关系数排序可以看到,LSTAT(低收入人群占比)与 MEDV 的负相关系数通常达到 -0.74 左右,RM(平均房间数)则在 0.70 左右,这两个特征贡献了模型绝大部分解释力。而特征之间的高相关则需要警惕:RAD 与 TAX 的相关系数通常超过 0.9,NOX 与 INDUS 也高度相关,这意味着标准线性回归的系数估计会不稳定,第五章会进一步验证。
离群点处理有一个我常用的判断方法:对每个特征计算 Z-score,绝对值超过 3 的样本标记为离群点。但要注意,线性回归对目标变量 MEDV 的离群点比特征离群更敏感,尤其当 MEDV 大于 50 时,这些值在原始数据里实际是被截断的(capped)。因此实践中不直接删除,而是先建模看残差,如果某条样本的残差超过 3 倍标准差,再决定是否剔除。
3. 线性回归算法原理与 sklearn 实现:从最小二乘法到可运行模型
3.1 最小二乘法的几何意义和解析解
线性回归假设目标值与特征之间满足线性关系:
y = w_1 x_1 + w_2 x_2 + ... + w_13 x_13 + b
写成矩阵形式为 y = Xw + b。模型训练的目标是最小化残差平方和:
L(w) = |y - Xw|^2_2
对 w 求导并让导数为零,得到闭式解:
w = (X^T X)^{-1} X^T y
这个解析解在小样本(n=506)下非常高效,因为 X^T X 是 13×13 的矩阵,求逆的计算量几乎可以忽略。但如果特征之间存在严重共线性,X^T X 接近奇异,求逆会出现数值不稳定的问题,这是线性回归算法在真实数据集上最常见的一个坑。所以某些实现里会加入正则项,变成岭回归,目的就是让 X^T X + \lambda I 可逆且稳定。
3.2 sklearn 实现完整代码
基于线性回归实现波士顿房价预测的源码包中,核心训练文件 LinearRegression_2.py 的结构如下,这是可以直接复现的版本:
import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error # 1. 加载数据 df = pd.read_csv('boston_housing.csv') X = df.drop('MEDV', axis=1) y = df['MEDV'] # 2. 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 3. 特征标准化(只拟合训练集) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 4. 创建并训练模型 model = LinearRegression() model.fit(X_train_scaled, y_train) # 5. 预测与评估 y_pred = model.predict(X_test_scaled) print("R2 Score:", r2_score(y_test, y_pred)) print("MSE:", mean_squared_error(y_test, y_pred)) print("RMSE:", mean_squared_error(y_test, y_pred, squared=False)) print("MAE:", mean_absolute_error(y_test, y_pred)) # 6. 输出特征系数 feature_names = X.columns coef_dict = dict(zip(feature_names, model.coef_)) for name, coef in sorted(coef_dict.items(), key=lambda x: abs(x[1]), reverse=True): print(f"{name}: {coef:.4f}")3.3 关键参数与代码逻辑说明
train_test_split的random_state=42是为了固定随机种子,保证每次运行划分结果一致,期末大作业答辩时可能被问到为什么是这个数字——它的作用是让实验可复现,换成其他整数也可以,关键是固定。
StandardScaler对线性回归不是必须的,因为线性回归的系数可以吸收特征的尺度差异。但标准化有两种实际收益:一是让每个特征的系数直接反映其重要性量级,方便解释;二是当后续切换到岭回归或 Lasso 时,正则化项要求所有特征在相同尺度下才有意义。注意这里只对训练集调用fit_transform,对测试集只调用transform,这是防止数据泄露的标准做法。
LinearRegression()的核心参数只有两个:
| 参数 | 默认值 | 作用 | 何时修改 |
|---|---|---|---|
| fit_intercept | True | 是否计算截距项 b | 数据已中心化时可设为 False |
| positive | False | 是否强制所有系数为正 | 特征与目标关系先验已知为正时可用 |
模型拟合完成后,输出的特征系数排序非常直观:LSTAT 和 RM 的系数绝对值最大,对应相关系数分析中这两个特征对房价的强影响。第 3.1 节提到的共线性问题,在系数上表现为 TAX 和 RAD 的系数可能一正一负互相矛盾,这时就要做第五章的方差膨胀因子诊断。
4. 评估指标、交叉验证与正则化调优:从跑通到结果可信
4.1 回归模型四大评估指标的使用边界
波士顿房价预测的输出是连续数值,不能套用准确率,常用的四个指标分别回答了不同问题:
| 指标 | 公式要点 | 适合场景 | 本项目典型值 |
|---|---|---|---|
| R² | 1 - SS_res / SS_tot | 模型解释力,越接近 1 越好 | 0.70 ~ 0.75 |
| MSE | 残差平方的均值 | 惩罚大误差,对离群敏感 | 50 ~ 70(千美元²) |
| RMSE | MSE 开根号 | 误差尺度与 y 同单位,可解释性强 | 7 ~ 9(千美元) |
| MAE | 绝对误差均值 | 离群影响更小,稳健 | 5 ~ 6(千美元) |
RMSE 与 MAE 的差值能提示离群点的存在:如果 RMSE 明显大于 MAE(比如大 1.5 倍以上),说明测试集中存在预测误差非常大的样本。本项目预测目标 MEDV 的均值在 20 千美元左右,RMSE 达到 7~8 千美元意味着平均误差接近 40%,这个精度在教学场景下是合理的,工作场景中会考虑引入非线性模型,但大作业答辩讲清楚误差来源比盲目堆模型更值钱。
4.2 K 折交叉验证标准流程
单次 train_test_split 的结果受随机划分影响很大,尤其当样本量只有 506 条时,某次划分可能恰好把高价房都分进测试集,导致 R² 剧烈波动。所以跟导师汇报时,要补充交叉验证的结果。
from sklearn.model_selection import cross_val_score, KFold kf = KFold(n_splits=5, shuffle=True, random_state=42) cv_scores = cross_val_score(model, X_train_scaled, y_train, cv=kf, scoring='r2') print("CV R2 scores:", cv_scores) print("CV R2 mean: {:.4f}, std: {:.4f}".format(cv_scores.mean(), cv_scores.std())) cv_mse = cross_val_score( model, X_train_scaled, y_train, cv=kf, scoring='neg_mean_squared_error' ) print("CV RMSE mean: {:.4f}".format((-cv_mse.mean()) ** 0.5))这段代码的KFold设置了shuffle=True,目的同样是保证划分的随机性。输出里 R² 的均值和标准差是两个关键观察点:标准差超过 0.1 说明模型对训练集划分敏感,也就是方差偏大,需要正则化或增加数据;均值明显低于单次测试集上的 R²,说明单次划分运气好,真实泛化性能没那么强。交叉验证结果里还有一个常见坑是scoring='neg_mean_squared_error',sklearn 的cross_val_score默认遵循「越大越好」的约定,所以 MSE 被取负,手动开根号前要先加负号还原。如果直接对负数开根会在 numpy 中得到 NaN。
4.3 岭回归与 Lasso 的调参实验
当交叉验证发现模型方差大、或者特征系数不稳定时,加入 L2 正则化的岭回归(Ridge)是线性回归最直接的升级路径。泛化性能对比实验代码如下:
from sklearn.linear_model import Ridge, Lasso # 候选正则化强度 alphas = [0.001, 0.01, 0.1, 1.0, 10.0, 100.0] for alpha in alphas: ridge = Ridge(alpha=alpha) ridge_scores = cross_val_score(ridge, X_train_scaled, y_train, cv=kf, scoring='r2') lasso = Lasso(alpha=alpha) lasso_scores = cross_val_score(lasso, X_train_scaled, y_train, cv=kf, scoring='r2') print(f"alpha={alpha:.3f}, Ridge R2={ridge_scores.mean():.4f}, " f"Lasso R2={lasso_scores.mean():.4f}")在这里,alpha 控制惩罚力度:alpha 越大,系数被压缩得越狠,模型偏差增加、方差降低。调参观察到的典型现象是,alpha 从 0.001 加到 1.0 时 R² 略有波动但总体稳定;继续加大到 100 时 R² 明显下降,说明模型被过度正则化,进入了欠拟合区间。Lasso 的另一个特性是会把不重要的特征系数压缩到 0,所以还可以观察哪些特征被清零——在波士顿数据集里,通常 ZN 和 CHAS 的系数最先归零,说明它们对房价的解释力在控制其他变量后贡献很小。
5. 残差诊断与答辩技巧:用验证性分析让模型不露破绽
5.1 残差图与 Q-Q 图:以 sklearn 自带模型为对象
期末大作业拿到好分数的关键不只是跑通代码,而是能主动说出模型的缺陷和改进方向。线性回归的四个经典假设是:线性关系、误差独立、误差同方差、误差正态分布。逐一用残差验证:
import scipy.stats as stats # 计算残差 residuals = y_test - y_pred # 1. 残差 vs 预测值散点图 plt.figure(figsize=(8, 5)) plt.scatter(y_pred, residuals, alpha=0.6) plt.axhline(y=0, color='red', linestyle='--') plt.xlabel('Predicted MEDV') plt.ylabel('Residuals') plt.show() # 2. Q-Q 图检验残差正态性 stats.probplot(residuals, dist="norm", plot=plt) plt.show() # 3. 查看残差统计量 print("Residual mean:", np.mean(residuals)) print("Residual std:", np.std(residuals))残差图如果呈现明显的扇形(预测值增大时残差扩散),说明存在异方差性,此时标准误差估计和置信区间都不可信,常见补救方法是对目标变量取对数,即把 y 替换为np.log1p(y),模型的目标就变成预测房价的对数值。Q-Q 图里如果残差在两端偏离直线,说明厚尾分布,这时可以报告 MAE 而不是只依赖 RMSE。这两项验证放在项目报告里,就是预答辩时最好的防守答案。
5.2 共线性定量判断:计算方差膨胀因子 VIF
第 2.1 节已经发现 RAD 与 TAX 高度相关,这里用 VIF 定量确认:
from statsmodels.stats.outliers_influence import variance_inflation_factor X_with_const = sm.add_constant(X_train_scaled) vif_data = pd.DataFrame() vif_data['feature'] = X.columns vif_data['VIF'] = [ variance_inflation_factor(X_with_const, i + 1) for i in range(X.shape[1]) ] print(vif_data.sort_values('VIF', ascending=False))VIF 大于 10 通常认为存在严重共线性,RAD 和 TAX 会最先爆掉,通常超过 20。处理方式有三选一:直接删除其中一个特征;将两个特征合并;改用岭回归。实践中更推荐在报告中直接展示 VIF 结果,然后说明最终模型选择保留 TAX、删除 RAD,理由有两个——TAX 的物理含义更清晰,且保留 RAD 时系数不稳定。这一步体现的是对线性回归算法假设的理解程度,也是最容易被提问的环节。
5.3 手动验证一次预测,让答辩演示更完整
最后在代码里保留一个预测示例输出,方便答辩时当场演示:
sample = X_test_scaled[0].reshape(1, -1) sample_true = y_test.iloc[0] sample_pred = model.predict(sample)[0] print(f"真实房价: {sample_true:.2f} 千美元") print(f"预测房价: {sample_pred:.2f} 千美元")这段代码的作用是让答辩现场直接从测试集取一条样本验证,真实值与预测值的差距就是你要向导师解释误差来源的起点。配合上面的残差分析结果,你可以说清哪些街区被高估、哪些被低估,以及这与 LSTAT 特征的关系。这样整个基于线性回归实现波士顿房价预测的项目源码,就从「模型跑通」升级成了「模型可解释、局限可验证」的完整作业。
本文还有配套的精品资源,点击获取