☰
Boston房价预测实战:线性回归从入门到跑通的第一道坎
2026/9/26 18:01:01 网站建设 项目流程

简介:这份资源面向机器学习入门者与需要巩固回归建模基础的开发者,围绕波士顿房价预测这一经典案例,系统整理了线性回归从理论到落地的完整代码实现。压缩包共20个文件,以11个Python脚本和9个CSV数据文件为主,脚本覆盖数据加载、模型训练、测试评估与可视化等环节,CSV则承载训练集、测试集及各类曲线数据,整体约228KB,结构紧凑便于逐文件研读。内容涉及数据预处理、特征工程、多元线性回归建模、MSE与R²评估、残差与参数曲线绘制,并延伸至岭回归、Lasso、Elastic Net等正则化模型的对比思路,帮助读者理解过拟合处理与模型选择。目前已有346人学习下载,适合希望借助完整案例掌握回归建模全流程、提升数据分析与调参能力的读者参考实践。

1. Boston 房价预测实战:线性回归从入门到跑通的第一道坎

很多人第一次接触机器学习线性回归,都是从 Boston 房价数据集开始的。它足够小、特征清晰、目标变量连续,拿来练手再合适不过。但真正动手时你会发现,问题从来不是「线性回归是什么」,而是「数据怎么读、特征怎么选、模型怎么评估、结果怎么解释」。这个标题里的「实战」和「代码大全」两个词,恰恰点出了核心诉求:不是要一份教科书式的公式推导,而是要一套能直接跑、能改、能复现的代码骨架。

这篇文章面向两类人:一类是刚学完线性回归理论、想找个数据集把流程走通的新手;另一类是做过项目但想回头把 Boston 预测这套标准流程重新梳理一遍的从业者。我会把数据加载、特征工程、模型训练、评估诊断、常见翻车点全部拆开讲,每个环节都给出可抄的代码和参数说明。你跟着走一遍,就能拿到一个能跑通的基线模型,并且知道每一步为什么这么做、改哪里会出问题。

2. 数据加载与探索:先把 Boston 数据集读明白再谈建模

2.1 Boston 数据集的字段含义与加载方式

Boston 房价数据集包含 506 条样本、13 个特征,目标变量是 MEDV,即自有住房的中位数价格(单位:千美元)。13 个特征覆盖了犯罪率、住宅用地比例、非零售商业用地比例、是否临河、一氧化氮浓度、平均房间数、房龄、到就业中心距离、公路可达性、房产税税率、师生比、黑人比例、低收入人群比例。这些字段名在代码里通常是 CRIM、ZN、INDUS、CHAS、NOX、RM、AGE、DIS、RAD、TAX、PTRATIO、B、LSTAT。

加载方式有两种常见做法。一种是直接用 scikit-learn 内置的load_boston,但需要注意这个接口在新版本中因为伦理争议已被移除或标记弃用。更稳妥的做法是本地准备一份 CSV 文件,用 pandas 读取。下面给出本地 CSV 加载的完整代码:

import pandas as pd import numpy as np # 假设 boston.csv 与脚本同目录,最后一列为目标变量 MEDV df = pd.read_csv("boston.csv") # 统一列名,避免大小写或空格导致的 KeyError df.columns = [c.strip().upper() for c in df.columns] # 确认特征列与目标列 feature_cols = [c for c in df.columns if c != "MEDV"] target_col = "MEDV" print("样本数:", df.shape[0]) print("特征数:", len(feature_cols)) print(df[feature_cols + [target_col]].describe().T[["mean", "std", "min", "max"]])

这段代码做了三件事:读取 CSV、统一列名、输出描述性统计。describe().T把统计量转置后更易读,重点看 mean 和 std 判断量纲差异,看 min 和 max 判断是否有异常值。比如 CRIM 的 max 可能远大于 mean,说明存在极端高犯罪率样本,后续要考虑是否做截断或标准化。

2.2 缺失值与异常值的快速排查

Boston 数据集本身比较干净,但实战中你拿到的往往是脏数据。排查缺失值用df.isnull().sum(),排查异常值用分位数和箱线图逻辑。下面这段代码同时输出缺失情况和基于 IQR 的异常值计数:

# 缺失值统计 missing = df.isnull().sum() print("缺失值:\n", missing[missing > 0]) # 基于 IQR 的异常值计数 def count_outliers(series): q1, q3 = series.quantile(0.25), series.quantile(0.75) iqr = q3 - q1 lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr return ((series < lower) | (series > upper)).sum() outlier_counts = {c: count_outliers(df[c]) for c in feature_cols + [target_col]} print("异常值计数:", outlier_counts)

参数说明:1.5 倍 IQR 是常用阈值,想更激进可以改成 3 倍。异常值不一定要删,先看它是不是真实业务场景下的合理极值。比如高犯罪率区域确实存在,删掉反而损失信息。我一般会先保留,在模型诊断阶段再看这些点是否造成残差异常。

2.3 特征与目标的相关性初筛

建模前先看哪些特征和目标变量线性相关性强,能帮你快速判断线性回归是否合适。用皮尔逊相关系数矩阵,重点看和目标变量那一列:

corr = df[feature_cols + [target_col]].corr() target_corr = corr[target_col].drop(target_col).sort_values(ascending=False) print(target_corr)

通常 LSTAT 和 RM 与 MEDV 的相关性最强,LSTAT 负相关、RM 正相关,这符合直觉:低收入人群比例越高房价越低,平均房间数越多房价越高。如果某个特征和目标几乎零相关,线性回归里它贡献很小,可以考虑剔除或做非线性变换。但注意,相关性只反映线性关系,不代表因果,也不代表该特征在多元回归中一定不显著。

3. 线性回归建模:从最小二乘到 sklearn 落地

3.1 线性回归的数学形式与损失函数

线性回归假设目标变量是特征的线性组合加噪声:y = Xw + b + ε。求解目标是让残差平方和最小,即最小化 ||y - Xw - b||²。这个优化有闭式解 w = (XᵀX)⁻¹Xᵀy,但实际中更常用梯度下降或正规方程求解。sklearn 的LinearRegression默认用 SVD 分解求解,数值稳定性比直接求逆好。

选型理由:Boston 数据量小、特征维度低,线性回归足够作为基线。如果特征间存在严重共线性,普通最小二乘的系数会不稳定,这时可以考虑 Ridge 或 Lasso。但作为第一版基线,先用普通线性回归把流程跑通,再根据诊断结果决定是否加正则。

3.2 训练集测试集划分与标准化

线性回归对特征量纲敏感,尤其是用梯度下降求解时。即使 sklearn 用 SVD,标准化也能让系数更容易比较。下面给出划分和标准化的完整代码:

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X = df[feature_cols].values y = df[target_col].values # 固定 random_state 保证可复现 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)

关键参数:test_size=0.2表示 20% 做测试,数据量小可以调到 0.15 或 0.25 看结果稳定性。random_state=42是习惯用法,保证每次划分一致。注意fit_transform只在训练集上做,测试集用transform,否则会数据泄露。这是新手最容易翻车的地方之一。

3.3 模型训练与系数解读

训练代码很短,但系数解读才是重点:

from sklearn.linear_model import LinearRegression model = LinearRegression() model.fit(X_train_scaled, y_train) coef_df = pd.DataFrame({ "feature": feature_cols, "coef": model.coef_ }).sort_values("coef", key=abs, ascending=False) print("截距:", model.intercept_) print(coef_df)

因为做了标准化,系数绝对值大小可以直接比较重要性。RM 的系数通常为正且较大,LSTAT 为负且较大,和相关性分析一致。截距是特征全为均值时预测的 MEDV。注意,标准化后的系数解释为「特征每变化一个标准差,目标变化多少个单位」,不是原始量纲下的变化。

3.4 预测与基础评估指标

评估回归模型常用 MSE、RMSE、MAE、R²。RMSE 和 MAE 单位与目标一致,R² 表示解释方差比例:

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score y_pred = model.predict(X_test_scaled) 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}") print(f"RMSE: {rmse:.2f}") print(f"MAE: {mae:.2f}") print(f"R2: {r2:.4f}")

Boston 数据集上,普通线性回归的 R² 通常在 0.6 到 0.75 之间,RMSE 在 4 到 6 千美元左右。如果你的结果远差于这个范围,先检查是否忘了标准化、是否数据泄露、是否特征列选错。R² 为负说明模型比直接用均值预测还差,通常是流程出了严重问题。

4. 模型诊断与调优:让线性回归不只是「能跑」

4.1 残差分析:判断线性假设是否成立

线性回归的核心假设是残差独立同分布且均值为零。画残差图是最直接的诊断方式:

import matplotlib.pyplot as plt residuals = y_test - y_pred plt.figure(figsize=(10, 4)) plt.subplot(1, 2, 1) plt.scatter(y_pred, residuals, alpha=0.6) plt.axhline(0, color="red", linestyle="--") plt.xlabel("Predicted") plt.ylabel("Residuals") plt.title("Residuals vs Predicted") plt.subplot(1, 2, 2) plt.hist(residuals, bins=20, edgecolor="black") plt.xlabel("Residual") plt.title("Residual Distribution") plt.tight_layout() plt.show()

如果残差图呈现喇叭形,说明存在异方差,可以考虑对目标变量取对数。如果残差有弯曲趋势,说明线性假设不成立,需要加多项式特征或换模型。残差直方图接近正态是理想情况,偏离太远会影响置信区间,但对预测点估计影响有限。

4.2 共线性检查与 VIF 计算

特征间高度共线会让系数估计不稳定,表现为系数符号和直觉相反、换一组样本系数大幅变化。用方差膨胀因子 VIF 量化:

from statsmodels.stats.outliers_influence import variance_inflation_factor X_with_const = np.column_stack([np.ones(X_train_scaled.shape[0]), X_train_scaled]) vif_data = pd.DataFrame({ "feature": ["const"] + feature_cols, "VIF": [variance_inflation_factor(X_with_const, i) for i in range(X_with_const.shape[1])] }) print(vif_data.sort_values("VIF", ascending=False))

VIF 大于 10 通常认为共线性严重。Boston 数据里 RAD 和 TAX 往往 VIF 较高,因为它们都反映公路可达性和税收,信息重叠。处理方式:删掉其中一个、做 PCA 降维、或改用 Ridge 回归。我一般先看业务含义,如果两个特征确实冗余,直接删一个更简单。

4.3 正则化对比:Ridge 与 Lasso 的适用场景

当共线性存在或特征数多时,加正则能提升泛化。Ridge 做 L2 惩罚,系数收缩但不为零;Lasso 做 L1 惩罚,能把不重要特征系数压到零,自带特征选择:

from sklearn.linear_model import Ridge, Lasso ridge = Ridge(alpha=1.0) ridge.fit(X_train_scaled, y_train) ridge_r2 = r2_score(y_test, ridge.predict(X_test_scaled)) lasso = Lasso(alpha=0.1, max_iter=10000) lasso.fit(X_train_scaled, y_train) lasso_r2 = r2_score(y_test, lasso.predict(X_test_scaled)) print(f"Ridge R2: {ridge_r2:.4f}") print(f"Lasso R2: {lasso_r2:.4f}") print("Lasso 非零系数个数:", np.sum(lasso.coef_ != 0))

alpha 是正则强度,越大惩罚越重。Ridge 的 alpha 一般从 0.1 到 10 调,Lasso 从 0.001 到 1 调。用交叉验证选 alpha 更稳:

from sklearn.linear_model import RidgeCV alphas = np.logspace(-2, 2, 50) ridge_cv = RidgeCV(alphas=alphas, cv=5) ridge_cv.fit(X_train_scaled, y_train) print("最佳 alpha:", ridge_cv.alpha_) print("CV R2:", ridge_cv.score(X_test_scaled, y_test))

4.4 交叉验证:比单次划分更可靠的评估

单次 train_test_split 的结果受随机性影响大,交叉验证能给出更稳定的评估:

from sklearn.model_selection import cross_val_score cv_scores = cross_val_score( LinearRegression(), X_train_scaled, y_train, cv=5, scoring="r2" ) print("每折 R2:", cv_scores) print("平均 R2: %.4f (+/- %.4f)" % (cv_scores.mean(), cv_scores.std()))

cv=5 表示五折交叉验证,数据量小可以用 10 折。标准差大说明模型对数据划分敏感,可能需要更多数据或更简单的模型。我一般会把交叉验证分数和测试集分数对比,如果测试集远低于 CV 均值,说明测试集分布和训练集差异大,或者有过拟合。

5. 避坑与排查:Boston 线性回归实战中最容易翻车的 5 个点

5.1 现象:R² 为负或极低 → 原因:忘了标准化或数据泄露 → 解决:检查 fit_transform 调用顺序

新手常见错误是在划分训练测试集之前就对全量数据做标准化,导致测试集信息泄露到训练过程。正确顺序永远是先划分、再在训练集上 fit、然后 transform 测试集。另一个原因是忘了标准化,导致梯度下降不收敛或 SVD 数值不稳定。排查方法:打印训练集和测试集的均值方差,确认标准化后训练集均值接近 0、方差接近 1。

5.2 现象:系数符号和业务直觉相反 → 原因:特征共线性 → 解决:算 VIF 并删冗余特征

比如 RM 的系数变成负数,直觉上房间越多房价越高,出现负号说明共线性把系数估计带偏了。先算 VIF,把大于 10 的特征逐个删掉再看系数变化。也可以改用 Ridge,L2 惩罚能缓解共线性导致的系数震荡。注意,不要为了符号好看强行删特征,要看删掉后模型评估指标是否下降。

5.3 现象:测试集 RMSE 远大于交叉验证 RMSE → 原因:测试集分布偏移或样本太少 → 解决:换随机种子多次划分

Boston 只有 506 条样本,单次划分的测试集可能恰好包含较多极端值。解决办法是用不同 random_state 多跑几次,看 RMSE 的波动范围。如果波动很大,说明模型稳定性不足,考虑用交叉验证的均值作为最终评估,或者收集更多数据。也可以检查测试集的目标变量分布是否和训练集接近。

5.4 现象:Lasso 把所有系数压成零 → 原因:alpha 过大 → 解决:用 LassoCV 自动选 alpha

Lasso 的 alpha 控制惩罚强度,alpha 太大会把所有系数压到零,模型退化成只预测均值。手动调 alpha 很费时,直接用 LassoCV 在训练集上交叉验证选最优:

from sklearn.linear_model import LassoCV lasso_cv = LassoCV(alphas=np.logspace(-3, 1, 50), cv=5, max_iter=10000) lasso_cv.fit(X_train_scaled, y_train) print("最佳 alpha:", lasso_cv.alpha_) print("非零系数个数:", np.sum(lasso_cv.coef_ != 0))

如果最佳 alpha 仍然导致全零系数,说明特征和目标之间线性关系确实弱,需要考虑非线性模型或特征工程。

5.5 现象:预测值出现负数房价 → 原因:线性模型无输出范围约束 → 解决:后处理截断或换模型

线性回归输出范围是负无穷到正无穷,但房价不可能为负。如果预测出负数,说明模型在某些特征组合下外推了。简单做法是对预测值做截断np.maximum(y_pred, 0),但这只是掩盖问题。更根本的解决是检查是否有异常特征值导致外推,或者改用对数目标变量做变换,预测后再指数还原。我一般先截断保证业务可用,同时记录有多少预测被截断,如果比例高说明模型需要重构。

6. 从基线到进阶:用特征工程和诊断插件把 Boston 预测再推一步

基线跑通后,想再提升 R²,最有效的方向是特征工程。Boston 数据里 LSTAT 和 RM 与目标关系最强,但它们和目标未必是纯线性。可以尝试对 LSTAT 做对数变换、对 RM 做分箱、或者加 RM 和 LSTAT 的交互项。下面这段代码演示如何用PolynomialFeatures自动生成二次项和交互项,再用 Ridge 控制过拟合:

from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import Pipeline pipe = Pipeline([ ("poly", PolynomialFeatures(degree=2, include_bias=False)), ("scaler", StandardScaler()), ("ridge", RidgeCV(alphas=np.logspace(-2, 3, 50), cv=5)) ]) pipe.fit(X_train, y_train) y_pred_poly = pipe.predict(X_test) print("多项式 Ridge R2: %.4f" % r2_score(y_test, y_pred_poly)) print("RMSE: %.2f" % np.sqrt(mean_squared_error(y_test, y_pred_poly)))

注意这里没有手动标准化,因为 Pipeline 里已经包含。PolynomialFeatures(degree=2)会把 13 个特征扩展到 104 个(含交互项),特征数暴增后必须加正则,否则必然过拟合。RidgeCV 会自动选 alpha,比手动调省事。如果 R² 提升不明显,说明线性模型加二次项已经到瓶颈,该考虑树模型或神经网络了。

另一个实用技巧是代码诊断。写线性回归时最常见的隐性 bug 是特征列顺序在训练和预测时不一致。我习惯在训练后立刻保存特征列顺序,预测前做一次校验:

import json # 训练后保存 with open("feature_order.json", "w") as f: json.dump(feature_cols, f) # 预测前校验 with open("feature_order.json") as f: saved_cols = json.load(f) assert saved_cols == feature_cols, "特征列顺序不一致,检查数据读取逻辑"

这个习惯帮我省过很多次后悔药。线上推理时特征顺序错位不会报错,但预测结果会完全离谱,而且很难排查。加一行 assert 成本极低,收益极高。

最后说一个验证方法:把模型预测结果按目标值分箱,看每个箱内的平均预测偏差。如果高房价区间系统性低估、低房价区间系统性高估,说明模型对极端值拟合不足,可以考虑分位数回归或对目标做变换。这个诊断比单看 R² 更能暴露问题。

我自己做回归项目的习惯是:先跑通基线拿到 R²,再画残差图,再算 VIF,最后才动特征工程。顺序反了容易在错误的方向上浪费时间。线性回归虽然简单,但把诊断流程走扎实,后面换任何模型都能复用这套方法论。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询