☰
波士顿房价预测实战:线性回归代码与避坑指南
2026/10/6 8:16:48 网站建设 项目流程

简介:面向机器学习初学者的线性回归实战资源包,以波士顿房价预测这一经典回归任务为案例,完整覆盖从数据加载、缺失值检查、特征工程到模型训练、评估调优的端到端流程。压缩包共20个文件、约228KB,由11个Python脚本和9个CSV数据集构成;脚本按数据获取、模型训练、结果可视化等模块拆分,CSV文件则提供训练集、测试集以及MSE/参数变化曲线数据,便于与代码输出对照验证。已有346人学习下载。资源不仅实现基础线性回归,还延伸了多项式特征构造、正则化变体(岭回归、Lasso)以及误差熵分析,并通过曲线图直观展示过拟合与调参效果。代码包括梯度下降实现、训练/测试数据生成、重复与随机特征实验等脚本,可帮助读者动手实践数据标准化、损失函数计算、R²评估等关键步骤,适合希望系统掌握回归建模与参数调试技巧的数据分析初学者。

1. 波士顿房价预测没数据了?这份实战代码为什么还是值得跑

你刚拿到一份名为 boston预测实战以及线性回归基础代码大全.zip 的压缩包时,大概率和我当年一样:先解压,再找 README,然后对着满屏的load_boston()发呆。更扎心的是,如果你用的 scikit-learn 版本高于 1.2,这行代码直接报ImportError——波士顿房价数据集因为伦理和数据偏见问题,被官方移除了。但我想先说一个反直觉的结论:数据集被移除,反而让这个入门项目变得更值得跑一遍。它逼着你学会处理真实项目里最常见的“API 说没就没、接口说变就变”的迁移问题,而不是只会抄一段能跑的代码。

这份代码解决的是从“看懂线性回归公式”到“写出一套能预测、能评估、能保存的完整流程”之间的那段路。适合正在入门机器学习、想用 python 把最小二乘变成可运行代码的人;也适合准备做机器学习线性回归实验、但不想只跑通一个 demo 就交差的同学。它不适合指望靠调参就把房价预测做到排行榜前排的人——线性回归的天花板就摆在那里,它的价值不在精度,在于让你理解模型、数据和评估三者之间如何互相约束。

  1. 波士顿房价预测没数据了?这份实战代码为什么还是值得跑

2. 线性回归的数学底子:最小二乘与梯度下降,什么时候用哪个

2.1 最小二乘的闭式解:506×13 的规模为什么直接求逆就行

线性回归的目标是找一组系数 θ,让预测值 ŷ = Xθ 与真实值 y 的残差平方和最小。损失函数写成 J(θ) = (1/2m) Σ(hθ(xᵢ) - yᵢ)²,对 θ 求导并令导数为零,就能得到闭式解:

θ = (XᵀX)⁻¹Xᵀy

这就是正规方程。波士顿数据集是 506 行、13 个特征,加上偏置项之后 X 的形状是 506×14。这个规模下直接求逆矩阵的耗时完全可以忽略,连优化迭代都不需要。我一般会先给新手看这个公式,再用 numpy 手写一遍,确认和 sklearn 的LinearRegression结果一致,才算真正把线性回归吃透。

import numpy as np # 手动加一列全 1,作为偏置项(截距) X_with_bias = np.hstack([np.ones((X.shape[0], 1)), X]) # 正规方程:θ = (XᵀX)⁻¹Xᵀy theta = np.linalg.inv(X_with_bias.T @ X_with_bias) @ X_with_bias.T @ y # 预测 y_pred = X_with_bias @ theta # 和 sklearn 对比 from sklearn.linear_model import LinearRegression lr = LinearRegression().fit(X, y) print("手动求解截距:", theta[0], "斜率:", theta[1:]) print("sklearn 截距:", lr.intercept_, "系数:", lr.coef_)

这里有个前提:np.linalg.inv能稳定求逆,要求 XᵀX 可逆且条件数不过大。如果两个特征高度相关,XᵀX 会接近奇异矩阵,求逆结果会剧烈震荡——这就是下一章要讲的多重共线性问题。另外,当特征数量超过几万时,求逆的时间复杂度是 O(n³),内存和算力都撑不住,这时候就要换梯度下降。我的习惯是:特征数小于 1000 用闭式解,大于这个量级直接上梯度下降,不要纠结。

2.2 梯度下降:学习率、批量大小与收敛判断

梯度下降的思路是沿着损失函数负梯度方向反复更新 θ,直到损失不再下降。公式很简单:θ := θ - α·∇J(θ)。但实际跑起来有四个参数需要你自己定:学习率 α、批量大小、初始值、迭代次数。

import numpy as np def batch_gradient_descent(X, y, alpha=0.01, epochs=1000): m, n = X.shape theta = np.zeros(n) losses = [] for _ in range(epochs): grad = (1 / m) * X.T @ (X @ theta - y) theta -= alpha * grad loss = (1 / (2 * m)) * np.sum((X @ theta - y) ** 2) losses.append(loss) # 收敛判断:损失变化小于阈值就提前停 if len(losses) > 1 and abs(losses[-2] - loss) < 1e-6: break return theta, losses

学习率是最大的玄学点。α 太大,损失会震荡甚至发散;α 太小,跑几百轮还在半山腰磨蹭。我手上的经验值:标准化之后的数据,α 从 0.01 起步,观察 loss 曲线,如果前 50 轮震荡剧烈就除以 10,如果曲线平得像一条直线就乘以 10。批量大小则是另一种取舍:批量梯度下降用全部数据算梯度,方向准但每轮都慢;随机梯度下降每轮只用一个样本,快但噪声大;实践中更常用小批量梯度下降,batch 取 16 或 32,兼顾稳定性和速度。波士顿这种 506 行的数据集,三者差别不大,但你得知道这个权衡,因为换到视频流量预测、金融时序预测那种上万行的数据时,批量大小直接决定训练能不能收敛。

2.3 评估指标:R²、RMSE、MAE 各自在看什么

模型训练完,最怕只盯一个指标。波士顿房价预测这个场景里,我习惯同时看四个数:

指标公式含义关注点波士顿场景参考
R²1 - SS_res / SS_tot模型解释了百分之多少的方差0.7 左右算及格,0.8 以上不错
RMSEsqrt(Σ(ŷ-y)²/n)大误差会被平方放大4~7(千美元)都算合理
MAEΣ|ŷ-y|/n平均误差,不受离群点过度影响3~5(千美元)
MAPEmean(|ŷ-y|/y)相对误差,适合汇报给业务10%~15% 可接受

RMSE 和 MAE 的差值很有信息量:如果 RMSE 远大于 MAE,说明存在少量预测误差极大的样本在拉高整体误差,这是离群点的典型信号。R² 则要小心假象,它在测试集上可能因为样本量小波动很大。我的习惯是训练集和测试集都打印一份,放在一起对比:训练 R² 高、测试 R² 低,是过拟合;两个都低,是欠拟合或特征没选好。

3. 把 boston 预测跑起来:加载数据到模型保存的完整代码

3.1 先看 zip 里的文件与 load_boston 的兼容写法

解压这份 zip 后,常见的文件组织形式是:一个 Jupyter Notebook 或 python 脚本、一份数据文件(csv 或自带数据的加载代码)、可能还有一个说明文档。不要急着从头到尾跑一遍,先确认里面是直接用load_boston()还是从 csv 读取。如果是前者,你要先处理 scikit-learn 1.2 移除波士顿数据集的兼容问题。

try: from sklearn.datasets import load_boston data = load_boston() X, y = data.data, data.target print("旧版 API 加载成功,特征名:", data.feature_names) except ImportError: import pandas as pd # 从本地 csv 读取,csv 可以由 UCI 波士顿房价原数据整理而来 df = pd.read_csv("boston_housing.csv") X = df.drop(columns=["MEDV"]).values y = df["MEDV"].values print("新版 sklearn 已移除 load_boston,改用本地 csv 加载")

这段兼容代码的意义不只是“让它能跑”,而是给你一个通用套路:任何项目从旧版本迁移到新版本时,入口 API 变了,数据源本身不会消失。把数据落成 csv,就是给自己留后悔药。另外注意,csv 里的MEDV是目标列,特征列包括 CRIM、ZN、INDUS、CHAS、NOX、RM、AGE、DIS、RAD、TAX、PTRATIO、B、LSTAT 这 13 个,顺序最好和原数据集保持一致,否则之后对照系数含义时会混乱。

3.2 房价预测的最小闭环:训练与评估一次走完

拿到 X 和 y 之后,完整的训练流程包括:切分训练测试集、标准化、训练、预测、评估。这里最关键的一个顺序问题是:标准化必须在切分之后拟合,绝对不能先对全量数据做标准化再切分,否则会造成数据泄漏。数据泄漏会让你的测试分数虚高,部署到真实场景立刻翻车。为了从流程上堵住这个坑,我一般直接用 Pipeline。

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression from sklearn.pipeline import Pipeline from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error # 切分:固定随机种子,保证结果可复现 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 用 Pipeline 串起标准化和线性回归 pipe = Pipeline([ ("scaler", StandardScaler()), # 先对训练集拟合均值方差,再变换 ("lr", LinearRegression()) # 在标准化后的特征上训练 ]) pipe.fit(X_train, y_train) y_pred = pipe.predict(X_test) # 评估 print("R2:", r2_score(y_test, y_pred)) print("RMSE:", mean_squared_error(y_test, y_pred, squared=False)) print("MAE:", mean_absolute_error(y_test, y_pred))

test_size=0.2意味着 506 个样本里留约 101 个做测试,剩下的 405 个训练,这个比例对波士顿这种小数据集是合理的,测试集再小评估波动会很大。random_state=42的作用是固定切分结果,你换机器、重跑脚本,得到的训练测试划分完全一致,方便排查问题。Pipeline 在这里的核心价值是:StandardScaler只在训练集上执行fit,拿到均值和方差后,在测试集和未来新数据上只做transform。如果你手动写scaler.fit_transform(X)再切分,测试集的统计信息早就混进 scaler 里了,相当于考试时偷看了答案。

正式场合不推荐分开写。虽然scaler = StandardScaler()再X_train_scaled = scaler.fit_transform(X_train)也行,但后续做交叉验证时容易忘记对每一折重新 fit,Pipeline 逼着你用统一流程,这是我认为线性回归 python 实战里最值得养成的习惯。

3.3 可视化与模型保存:残差图、joblib 与持久化

训练评估结束后,不要急着关电脑。先画两张图:一张是预测值和真实值的散点图,另一张是残差图。残差图的横轴是拟合值,纵轴是残差(y - ŷ),它能暴露线性回归是否遗漏了非线性结构——这一点下一章会详细讲。确认模型没大问题后,再把模型保存到磁盘,这样下次预测不用重新训练。

import matplotlib.pyplot as plt import joblib # 画残差图 plt.scatter(y_pred, y_test - y_pred, alpha=0.6) plt.axhline(y=0, color="red", linestyle="--") plt.xlabel("预测房价(千美元)") plt.ylabel("残差") plt.title("残差图") plt.show() # 保存整个 Pipeline,而不是只保存模型 joblib.dump(pipe, "boston_lr_pipeline.pkl", compress=3)

compress=3是压缩级别,取值范围 0 到 9,数字越大压缩率越高但保存和加载越慢。for 一个几十 KB 的模型来说取 3 就够用,没必要拉满。保存整个 Pipeline 而不是只保存LinearRegression模型,是为了让新数据预测时自动走标准化流程。如果你只保存了lr而丢了scaler,预测时还得手动拿着训练集的均值和方差去变换新样本,一旦忘记,预测结果会偏到离谱。加载模型用joblib.load("boston_lr_pipeline.pkl"),然后直接调用.predict(new_data)即可。

4. 线性回归实战避坑:数据泄漏、共线性与回归假象的 5 条踩坑记录

4.1 数据泄漏:先标准化再切分,测试集分数虚高

现象:训练集 R² 只有 0.75,测试集 R² 却高达 0.82,甚至比训练集还好。不懂行的会以为是模型泛化能力超强,实际上十有八九是数据泄漏。原因:你在切分之前就对全量数据做了标准化,StandardScaler用了测试集的均值和方差来做变换,测试集信息提前进入了模型。解决:把标准化放进 Pipeline,或者严格按“先切分,再在训练集上 fit,再 transform 测试集”的顺序执行。验证方法也很简单:把random_state换成另一个值比如 7,再跑一遍,如果测试分数波动巨大,说明流程有问题。

4.2 多重共线性:删掉一个特征,系数符号直接反转

现象:LSTAT 的系数明明是负数(贫民比例越高房价越低),但加了某个特征后它变成正数,逻辑上说不通。原因:波士顿数据里 B、INDUS、TAX、RAD 这几个特征之间相关性极高,XᵀX 接近奇异,系数估计的方差被放大,细微的样本变化就会让系数大幅摆动。解决:先看相关系数矩阵,再决定要不要删特征或做正则化。

import pandas as pd df = pd.DataFrame(X, columns=["CRIM","ZN","INDUS","CHAS","NOX","RM","AGE","DIS","RAD","TAX","PTRATIO","B","LSTAT"]) corr = df.corr() # 找出和某个特征相关系数超过 0.7 的特征对 for i in range(len(corr.columns)): for j in range(i+1, len(corr.columns)): if abs(corr.iloc[i, j]) > 0.7: print(f"{corr.columns[i]} ~ {corr.columns[j]}: {corr.iloc[i, j]:.2f}")

输出里你会看到 TAX 和 RAD 的相关系数在 0.9 以上,DIS 和 INDUS、NOX 也高度相关。处理方式有两种:一是人工删掉业务上不太重要的那个,比如 RAD 和 TAX 留一个;二是改用 Ridge 回归,通过 L2 正则让系数稳定下来。真实项目里我更推荐 Ridge,因为它不需要你手动做特征取舍,代价是损失一点可解释性。

4.3 离群值与截断值:MEDV=50 的样本在干扰拟合

现象:残差图右侧出现一排几乎水平的点,预测值在 50 附近堆成一团。原因:波士顿数据里 MEDV 大于等于 50 的样本被统一记录为 50,这是当年数据采集时的截断操作(censoring),导致大量真实房价超过 50 的样本被压成一个平台。解决:先别急着删。用y_train[y_train >= 50]看一眼数量,如果只有几条,可以单独分析;如果占比不小,说明模型天然无法拟合这个截断,要么对目标变量做 log 变换压缩高端值,要么明确告诉业务方“模型对 50 千美元以上的预测不可信”。我见过有人把 MEDV>=50 的样本全部删掉后 R² 升到 0.85,但那是自欺欺人——现实预测里你根本不知道新样本会不会超过 50。

4.4 残差图呈扇形或 U 形:模型缺了非线性项

现象:残差图不是均匀分布在零线两侧,而是呈喇叭口(扇形)或明显弯曲。原因:线性回归假设特征和目标之间是直线关系,但波士顿数据里 RM(平均房间数)和 MEDV 的关系更接近曲线,房间数超过 6 之后房价增速变缓,一条直线根本描述不了这种形态。解决:对特征做多项式扩展,比如加 RM² 项,再重新训练。这也解释了为什么很多人在跑 LSTM 预测时序数据、或者想用 Transformer 预测正弦数据之前,连线性回归的残差图都没看过——先把简单模型的残差诊断做扎实,再上复杂模型,你会少走很多弯路。

4.5 版本迁移:scikit-learn 1.2 把 load_boston 移除了

现象:from sklearn.datasets import load_boston直接报错,错误信息写着 “load_boston has been removed from scikit-learn since version 1.2”。原因:官方认为该数据集存在伦理问题和数据偏见,决定移除。解决:不要硬改代码去兼容旧 API,而是按 3.1 的做法,把数据源换成本地 csv 或者用fetch_openml读取替代版本。我的建议是本地 csv 最稳,不依赖网络、不受 sklearn 版本迭代影响,这份 zip 里的数据文件就是为这个准备的。这个坑真正教会你的,是任何机器学习项目都要把“数据获取”和“模型代码”解耦开。

5. 模型诊断进阶:学习曲线与交叉验证,确认模型不是瞎拟合

5.1 学习曲线:用一条图区分欠拟合与过拟合

模型训练完、R² 看着还行,不代表真的没问题。我会再画一张学习曲线:横轴是训练样本数量,纵轴是 R²,分别画出训练集分数和验证集分数随样本量变化的曲线。如果两条线最终都收敛在 0.7 附近且差距很小,说明模型已经用足了数据;如果训练线很高但验证线一直上不去,说明过拟合;两条线都贴着 0.5 附近拉不上去,则是欠拟合。画法很简单,用learning_curve一行就能出数据,再交给 matplotlib 渲染。

5.2 交叉验证:5 折是性价比最高的默认值

如果你是在做机器学习线性回归实验,或者要给模型一个更可信的评估数字,单次 train_test_split 的分数说服力不够。我一般会加一个cross_val_score:

from sklearn.model_selection import cross_val_score scores = cross_val_score( pipe, X, y, cv=5, scoring="neg_mean_squared_error" ) rmse_scores = np.sqrt(-scores) print("5折RMSE:", rmse_scores, "均值:", rmse_scores.mean())

这里有个容易踩的坑:sklearn 的 scoring 约定是“越大越好”,所以 MSE 要写成neg_mean_squared_error,取负号后计算。cv=5 表示把 506 个样本分成 5 份,轮流拿一份做验证、其余训练,平均分数比单次切分稳健得多。如果数据量更少,可以用 cv=10 或留一法,但波士顿这个量级 5 折足够。

5.3 部署前最后一个动作:用保存的 Pipeline 预测新样本

模型确认没问题后,最终要落到“拿到新数据能出预测结果”这一步。由于我们保存的是完整 Pipeline,预测代码非常短:

import joblib pipe = joblib.load("boston_lr_pipeline.pkl") new_sample = [[0.02, 18, 2.3, 0, 0.5, 6.5, 60, 2.5, 4, 300, 17, 380, 8.5]] pred = pipe.predict(new_sample) print("预测房价(千美元):", pred[0])

注意传入的new_sample必须是 13 个特征、排列顺序和训练时一致的二维数组。Pipeline 会在内部自动完成标准化,你不用手动减均值除标准差——这也是当初坚持保存整条 Pipeline 的原因。这几年我摸爬滚打下来最深的教训就是:单次测试分数漂亮不算数,能稳定复现、能在新数据上不走样,才算真跑通。这套流程你完整走一遍,比搜二十篇教程都有用,希望帮到你。

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

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

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

立即咨询