恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
波士顿房价预测实战:线性回归从数据预处理到模型评估全流程
首页
资讯中心
/
波士顿房价预测实战:线性回归从数据预处理到模型评估全流程
波士顿房价预测实战:线性回归从数据预处理到模型评估全流程
发布时间:2026/10/10 1:44:49
简介一份以波士顿房价预测为主线、线性回归从原理到实战的代码合集适合机器学习初学者与需要快速搭建回归预测流程的开发者。资源共20个文件含11个Python脚本与9个CSV数据文件脚本承担数据加载、特征工程、模型训练、评估与可视化等任务CSV主要提供训练集、测试集及中间曲线数据压缩包整体228KB结构紧凑。目前已有346人学习使用。通过学习可掌握数据标准化、多项式特征扩展与组合特征等处理技巧理解梯度下降实现还能对比线性回归、岭回归、Lasso与Elastic Net的效果和调参思路为真实房价预测或其他回归问题提供可直接复用的代码模板与排查思路。1. 波士顿预测实战为什么一份40年前的数据集还是学线性回归的第一课同一份波士顿房价数据有人把 R² 刷到 0.85有人换个随机种子直接掉到 0.7还有人连load_boston()都跑不通——这不是模型玄学是数据切分、标准化顺序和版本差异在作怪。这个经典回归任务数据量小、特征含义明确、线性关系强正好用来把「线性回归基础代码」从头到尾捋一遍损失函数怎么来的、闭式解和梯度下降各自的适用边界、调库前为什么要先手写一遍、以及预测结果好得离谱时该先怀疑哪一步。这篇笔记面向准备用回归模型入门机器学习的实践者目标是让你拿到一份可复现的完整流程而不是停留在sklearn的一行调用上。2. 线性回归基础损失函数、闭式解与梯度下降怎么选2.1 从最小二乘说起损失函数为什么选平方误差线性回归做的是这样一件事给定特征矩阵 X 和真实标签 y找到一组权重 θ让预测值 ŷ Xθ 尽量接近 y。这里的「尽量接近」需要一个可微的量化标准最常用的是均方误差。加上 1/2 的系数是为了后面求导方便J(θ) (1/2m) Σ(yᵢ - θᵀxᵢ)²为什么选平方而不是绝对值绝对值损失在零点不可导梯度下降在误差接近零时会出现锯齿状的震荡平方损失放大了大误差的惩罚让模型优先修正偏差大的样本这和大多数业务场景里「预测偏得太离谱比偏一点更不可接受」的直觉一致。缺点是它对离群点敏感个别异常样本会把回归线拉歪这个特性在后面波士顿数据里会直接体现出来。最小二乘这个名字来自几何视角我们找的是一条直线/超平面使样本点到它的竖直距离平方和最小。对这个目标函数求偏导并置零就能得到闭式解。闭式解没有迭代过程一次性算出最优权重这是它最大的优势但后面会看到它在特征维度过高或矩阵接近奇异时会失效。2.2 闭式解与梯度下降两条收敛路径的取舍对 J(θ) 关于 θ 求导并令其为零得到正规方程θ (XᵀX)⁻¹Xᵀy这个形式非常干净numpy 里几行就能算完。但它的代价藏在(XᵀX)⁻¹里矩阵求逆的时间复杂度约是 O(n³)n 是特征维度。波士顿数据集只有 13 个特征完全无所谓但特征数上万甚至几十万时求逆要么慢到无法接受要么因为特征高度相关导致 XᵀX 不可逆直接报错。梯度下降走的是另一条路不求逆而是沿着损失函数下降最快的方向迭代。参数更新规则写成向量化形式是θ : θ - α · (1/m) · Xᵀ(Xθ - y)其中 α 是学习率控制每一步迈多大。这个小批量/批量梯度下降的写法在整个训练循环里只需要矩阵乘法特征多时也能跑。它不会因为矩阵奇异而失败但引入了一个新的麻烦学习率调不好要么发散要么慢得像蜗牛。我的建议是特征数小于 1000 且数据量不大时优先用闭式解快且没有调参负担特征维度高或数据量大时转梯度下降但必须配合标准化和损失曲线监控。2.3 标准化与学习率梯度下降能收敛的前提如果不做标准化像波士顿数据里的TAX税率数百量级和CHAS是否临河0/1 取值会同时出现在特征矩阵里。梯度下降在更新参数时梯度大小和特征尺度成正比量级大的特征对应的权重会被大步长带着乱跳量级小的特征对应的权重又更新得极慢整个收敛过程会非常不稳定。标准化通常用零均值单位方差z (x - μ) / σ。做完之后所有特征落到相近的尺度损失函数的等高线从狭长椭圆变成接近正圆梯度下降的路径就不再走锯齿形学习率也更容易设置。需要特别注意的是μ 和 σ 只能从训练集计算测试集用同一组参数转换这一点在第三章会详细展开因为它是个高频踩坑点。库实现里sklearn.linear_model.LinearRegression默认不标准化也能直接算因为内部走的是最小二乘的 SVD 分解但一旦切到SGDRegressor或自己写梯度下降标准化就是刚需。理解这层关系后面调参时才知道什么时候该动学习率、什么时候该动数据。3. 取数、改表、切分做实验前先把波士顿数据搞干净3.1load_boston弃用后的正确取数方式很多人卡在第一步from sklearn.datasets import load_boston在新版 scikit-learn 里直接报AttributeError。这个数据集因为包含一些涉及敏感属性的字段存在数据伦理争议官方从较新版本开始将其移除。常见做法是直接从公开数据仓库下载boston.csv一类的现成文件或使用老版本环境。我一般建议走 CSV 路线因为后续用 pandas 处理更顺手不用和Bunch对象打交道import pandas as pd df pd.read_csv(boston.csv) print(df.shape) print(df.columns.tolist())这里df.shape应该输出(506, 14)对应 506 条样本、13 个特征加 1 个目标列。columns.tolist()可以快速确认列名是否符合预期防止下载的文件带索引列或命名异常。如果你手里的文件第一列是无名索引读取时可以加参数index_col0把它丢掉。老版本环境里的load_boston()返回的data和target也可以直接拼成 DataFrame但我不推荐为了一个数据集去锁环境版本后面装别的库容易冲突。3.2 特征含义与初步相关性观察拿到数据第一件事不是跑模型是看每列在讲什么。波士顿房价数据集的 13 个特征里RM是平均房间数LSTAT是低收入人口占比DIS是到就业中心的加权距离TAX是房产税率CHAS是是否临河的 0/1 变量。目标列MEDV是自住房屋价格中位数单位是千美元。特征含义与 MEDV 的直觉关系CRIM城镇犯罪率越高房价越低ZN大户型住宅用地比例一般正相关INDUS非零售商业用地比例一般负相关CHAS是否临河临河通常更贵NOX氮氧化物浓度越高房价越低RM平均房间数强正相关AGE老旧自住房比例一般负相关DIS到就业中心距离越远越低RAD交通可达性指数与 TAX 强相关TAX房产税率越高越低PTRATIO师生比越高教育资源越差B黑人比例相关统计量敏感字段仅作特征存在LSTAT低收入人口占比强负相关MEDV房价中位数目标列先做一轮缺失值和描述性统计用df.describe()看每列的量级和分布重点观察RM、LSTAT和MEDV的极值。再跑一个相关性矩阵把热力图打出来你会看到LSTAT和MEDV的相关系数接近 -0.7RM和MEDV接近 0.7这两个特征对房价的解释力明显强于其他列。同时注意TAX和RAD的相关系数超过 0.9这是一对典型的强共线性特征到第五章会牵扯出系数解释翻车的问题。缺失值处理在这个数据集里通常很轻松df.isnull().sum()大概率全为 0。如果有少量缺失常见做法是看列分布用中位数填充而不是均值因为房价相关的特征多为偏态分布均值容易被极端值带偏。3.3 先切分再标准化防止数据泄漏的做法数据预处理的顺序是有讲究的。最稳妥的流程是先分离 X 和 y再做训练测试切分最后只基于训练集做标准化。这里的关键点是StandardScaler的fit只能作用在训练数据上测试数据用训练集算出的均值和方差来transform。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X df.drop(MEDV, axis1) y df[MEDV] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)test_size0.2表示留出 20% 的样本做验证506 条数据里训练集约 404 条、测试集约 102 条。random_state42固定切分结果保证你每次跑出来的训练集和测试集完全一致实验可复现。fit_transform在训练集上先算均值和方差再转换transform在测试集上只用已算好的参数做转换不再重新计算。如果先在整个 X 上做标准化再切分测试集的分布信息已经混进了训练流程被称为数据泄漏。泄漏的后果是验证结果虚高模型上线后真实表现远不如实验这类问题在小数据集上尤其隐蔽。这一步做对了后面模型做得好才是真的好。4. 代码实践从 NumPy 手写闭式解到 sklearn 调参对比4.1 用 NumPy 手写闭式解验证你对公式的理解在调库之前我强烈建议先手写一遍线性回归。不是为了重复造轮子而是为了确认你对正规方程的理解加一列全 1 作为截距项、矩阵乘法顺序、转置和求逆的配合。代码做到位了后面看 sklearn 的coef_和intercept_时心里才有底。import numpy as np def linear_regression_closed_form(X, y): # 在特征矩阵左侧加一列 1用来学习截距项 X_with_bias np.c_[np.ones(X.shape[0]), X] # 正规方程θ (XᵀX)⁻¹Xᵀy theta np.linalg.inv(X_with_bias.T X_with_bias) X_with_bias.T y return theta theta linear_regression_closed_form(X_train_scaled, y_train.values) print(截距项, theta[0]) print(特征权重, theta[1:])X_train_scaled在上一章已经标准化过所以这里直接传入数值矩阵。np.c_是拼接辅助方法把一列 1 拼到特征矩阵最左边这样线性回归的偏置就被吸收进权重向量里不需要单独处理。np.linalg.inv(...)计算逆矩阵当特征维度不高时完全够用。这段代码跑出来的是一个 14 维的向量第一位是截距后 13 位是特征权重。两个容易出错的地方一是y要转成 numpy 数组pandas Series 直接参与矩阵运算有时会报维度不匹配二是X.T X的结果如果接近奇异inv会给出数值不稳定的结果此时可以试试np.linalg.pinv伪逆来兜底。这个细节在你以后处理高维共线性数据时会非常有用。4.2 梯度下降版训练循环学会监控损失曲线闭式解是一次性计算梯度下降则是一步步逼近。理解梯度下降的关键不是记公式而是观察损失值随迭代次数的变化。下面这段是批量梯度下降的完整实现训练过程中每 100 轮打印一次损失def linear_regression_gradient_descent(X, y, lr0.01, n_iter1000): m, n X.shape theta np.zeros(n 1) # 含截距项 X_with_bias np.c_[np.ones(m), X] loss_history [] for i in range(n_iter): gradient (1 / m) * X_with_bias.T (X_with_bias theta - y) theta theta - lr * gradient loss np.mean((X_with_bias theta - y) ** 2) loss_history.append(loss) if i % 100 0: print(fiter {i}, loss {loss:.4f}) return theta, loss_history theta_gd, loss_hist linear_regression_gradient_descent( X_train_scaled, y_train.values, lr0.05, n_iter500 )lr0.05在标准化后的特征上是个安全的起点。n_iter500对 400 多条样本足够因为批量梯度下降每一步都用全量数据计算梯度收敛速度通常很快。gradient那一行是核心X_with_bias.T (X_with_bias theta - y)算的是误差向量与特征矩阵的内积再除以样本数得到平均梯度。运行后你会看到 loss 从最初的几百量级快速下降到后面每轮只降零点几。如果 loss 曲线出现大幅震荡甚至上升先检查学习率是不是太大降到 0.01 再试如果下降得极慢说明学习率太小或特征没标准化。这个动手调参的过程比直接调用SGDRegressor更能建立对超参数的直觉。4.3 与 sklearn 对比并用指标评估手写版本的意义在于和标准库结果互相验证。sklearn 的LinearRegression底层默认用最小二乘的 SVD 分解数值稳定性比直接求逆更好但数学本质和我们的闭式解一致两者的系数应该几乎相同。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score model LinearRegression() model.fit(X_train_scaled, y_train) print(手写闭式解权重, theta[1:]) print(sklearn 权重, model.coef_) print(截距对比, theta[0], model.intercept_) 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(fMSE: {mse:.4f}, RMSE: {rmse:.4f}, MAE: {mae:.4f}, R²: {r2:.4f})跑完你会发现手写权重和 sklearn 的coef_在小数点后三四位内有细微差别这是数值计算精度问题不影响结论。评估指标里RMSE 和房价的单位一致千美元波士顿测试集上通常落在 4 到 6 之间R² 在 0.7 到 0.85 之间都算正常——如果你想继续提升后面还能加正则化和特征变换。需要注意的是R² 超过 0.9 时先别高兴回去检查是不是把测试集信息泄漏到训练里了。5. 波士顿预测的 5 个坑从加载失败到只看 R²5.1 数据集加载失败版本差异是第一道坎现象from sklearn.datasets import load_boston报错AttributeError或者程序在 import 阶段就崩了。原因scikit-learn 较新版本已经移除了这个数据集老代码直接迁移过来必然翻车。很多教程没有注明版本约束导致照着抄的人卡在环境搭建这一步。解决先检查 sklearn 版本再决定走哪条路。我一般直接用 CSV 读取绕开版本问题import sklearn print(sklearn.__version__) # 方案一直接读 CSV适用于任何版本 df pd.read_csv(boston.csv) # 方案二老版本环境下的兼容写法 try: from sklearn.datasets import load_boston data load_boston() except ImportError: print(当前 sklearn 版本已移除 load_boston请改用 CSV 方式读取)这个坑本身不难但它提醒了一件事跑别人的代码前先看环境版本尤其是涉及到已弃用 API 的机器学习项目。5.2 标准化顺序颠倒结果好得可疑时先查这里现象模型在训练集上 R² 高达 0.95测试集却只有 0.5或者反过来测试集结果异常地好好到你不敢相信。原因先对整个 X 做标准化再切分训练测试集测试集的均值和方差已经参与过训练数据的转换。模型在测试时「见过」数据分布评估结果失去意义。这是典型的泄漏表现往往是验证集指标虚高。解决严格执行「先切分、后标准化」的流程。scaler.fit(X_train)算参数scaler.transform(X_test)用同一套参数转换。检查代码时不只要看有没有调用fit_transform还要看它作用在哪个数据上。小数据集上这一步的判断失误后面所有调参都是白费。5.3 随机种子不固定R² 波动大不是模型问题现象代码完全一样只是没设随机种子重跑几次 R² 在 0.7 到 0.82 之间来回跳。原因train_test_split默认每次随机切分波士顿数据只有 506 条测试集 100 条左右的样本分布差异足以让评估指标产生明显波动。这不是模型不稳定是采样随机性。解决固定random_state42让切分结果可复现。更进一步的做法是放弃单次切分改用 K 折交叉验证取多折的平均分和标准差来判断模型真实水平。对小数据集而言交叉验证比单次 holdout 更能反映泛化能力。5.4 多重共线性系数方向不对时的排查思路现象跑出来的TAX系数是正数直觉上税率越高房价应该越低为什么模型给出正相关或者某个特征单独和房价负相关放进模型后系数变成正的。原因波士顿数据里TAX和RAD相关系数超过 0.9高度共线。当两个特征携带相似信息时模型会把权重在它们之间任意分配单个系数不再代表该特征的独立影响。这个现象在线性回归里非常常见也是新手最容易误解的地方。解决先看相关系数矩阵或方差膨胀因子 VIF确认哪些特征强相关。常见的处理是二选一删掉其中一个或者改用岭回归用 L2 正则化把系数压缩、缓解共线性带来的方差膨胀。如果目标是预测而非解释共线性问题可以适当放宽但如果你想跟别人解释每个特征的影响方向就必须处理它。5.5 只看 R²这个指标在小样本上会骗人现象模型 R² 看起来不错但你把预测值和真实值摆在一起看发现价格在 20 万以上的房子全都预测偏低。R² 没能暴露这个问题。原因R² 是整体拟合优度对高值区的系统性偏差不敏感。波士顿数据里房价中位数在 20 以上时样本明显变少模型倾向用均值回归来降低整体损失于是高房价区域的预测被压低。解决把y_pred和y_test画成散点图配合 MAE 和 RMSE 一起看。RMSE 对偏离大的样本更敏感MAE 则反映平均误差水平。如果高值区预测总是偏低考虑对目标列做log1p变换把偏态分布拉正再跑回归并比较变换前后的指标。记住 R² 是结论之一不是结论本身。6. 进阶用残差图和学习曲线给模型做体检6.1 残差图该长什么样残差是真实值与预测值的差residual y_test - y_pred。把它画在预测值旁边理想情况是残差随机分布在 0 水平线上下没有明显形状。如果出现漏斗形——预测值越大残差越分散说明存在异方差模型在高值区的不确定性更大这时候单纯调参已经不够得考虑对目标做变换。import matplotlib.pyplot as plt residuals y_test - y_pred plt.scatter(y_pred, residuals, alpha0.6) plt.axhline(0, colorred, linestyle--) plt.xlabel(Predicted MEDV) plt.ylabel(Residuals) plt.title(Residual Plot) plt.show()这张图不需要高级技巧但它能在你被指标迷惑时给出直观判断。我自己的习惯是评估完模型先画残差图再回头看数字因为数字会掩盖局部模式。6.2 用学习曲线判断偏差方差学习曲线是判断模型处于欠拟合还是过拟合的最直接工具不断增大训练集规模分别记录模型在训练集和验证集上的分数画成两条曲线。训练分高、验证分低且两线差距大是高方差模型记忆了训练数据两线都低且贴在一起是高偏差模型本身表达力不够。from sklearn.model_selection import learning_curve train_sizes, train_scores, val_scores learning_curve( LinearRegression(), X_train_scaled, y_train, cv5, train_sizesnp.linspace(0.1, 1.0, 5) ) train_mean train_scores.mean(axis1) val_mean val_scores.mean(axis1) for size, tm, vm in zip(train_sizes, train_mean, val_mean): print(f训练样本数 {size:.0f}训练 R² {tm:.3f}验证 R² {vm:.3f})波士顿数据上你会看到验证分数随训练样本增多缓慢上升训练分数始终在验证分数上方一些这是小数据集的常态。真正厉害的做法是把这个过程用在特征工程之后——比如加了多项式特征再画一次看曲线是否改善。这套「先指标、再残差图、最后学习曲线」的诊断顺序也是我处理其他回归问题时沿用的流程。有一次在模拟项目X上我靠着残差图发现预测误差在目标值两端都偏大回去查才发现是目标变量没做对数变换指标虚高骗了我两天。从那以后我拿到任何回归模型都会先画这两张图再谈结论希望你也能少走这一步弯路。本文还有配套的精品资源点击获取