简介:本资源是一份面向高校机器学习课程学习者与期末项目实践者的高分回归分析作业,聚焦波士顿房价预测这一经典教学案例,系统实现多种线性回归算法并完成全流程建模验证。压缩包共12个文件(944KB),包含3个CSV数据与结果文件、3个核心Python脚本(含模型训练、测试与示例调用)、3张关键可视化图表(系数分析、预测散点、训练曲线)、2个说明文本及1份Markdown文档,覆盖数据预处理、BGD/SGD梯度下降、岭回归与LASSO四种算法实现、多维度评估对比及可复现的实验报告框架。已有120人学习下载,提供从数据加载到模型部署的完整闭环代码、清晰的结果输出格式(如coefficient_analysis.csv与model_comparison.csv)以及开箱即用的requirements.txt依赖管理,特别适合课程设计、大作业提交与算法原理理解巩固。 波士顿房价预测这个题目,我在课程作业里见过,在面试准备里也见过,甚至在一些公司的技术方案评审里都见过。一个上世纪七十年代的数据集,能一直火到今天,不是没有原因的。它的数据量不大(506条),特征数量适中(13个),目标变量是连续值,非常适合用来讲清楚回归问题的完整流程。无论是刚开始学机器学习的本科生,还是在准备算法岗面试的人,拿它来练手都是很顺的选择。
这篇博文不打算只贴一份代码了事。我会把从数据探索、特征处理、模型选型到结果分析的完整链路都拆开讲一遍,重点解释每一步为什么要这么做,以及哪些地方是拿分关键。我还会放出我实际跑的代码和对应的输出说明,方便你直接对照。文末整理了一份常见问题和排查方案,很多是我自己踩过的坑,希望能帮你少走弯路。
1. 项目全貌:波士顿房价预测到底在预测什么
1.1 核心需求解析:这是一道标准的回归题
先把这个项目的本质说清楚。波士顿房价数据集里的每条样本,描述的是一处房产所在区域的各种统计信息,目标值MEDV是该区域业主自住房的中位价格(单位是千美元)。所以这个项目的任务就是:根据给定区域的犯罪率、房间数、交通便利度等特征,预测该区域的房价中位数。
数据集中包含了13个特征,下面我按我处理的顺序整理了一个清单,方便你对照着认识数据:
| 特征名 | 含义 | 类型 | 对房价的直观影响 |
|---|---|---|---|
| CRIM | 城镇人均犯罪率 | 连续值 | 通常负相关,犯罪率高房价低 |
| ZN | 占地超过25000平方英尺的住宅用地比例 | 连续值 | 较大时多为高档社区,可能正相关 |
| INDUS | 城镇非零售业用地比例 | 连续值 | 工业区多的地方房价偏低 |
| CHAS | 是否邻近查尔斯河(1=是,0=否) | 二分类 | 景观溢价,通常正相关 |
| NOX | 一氧化氮浓度(每千万分之一) | 连续值 | 空气污染越重房价越低 |
| RM | 每栋住宅的平均房间数 | 连续值 | 经典正相关特征,越大房价越高 |
| AGE | 建于1940年之前的自住单位比例 | 连续值 | 老房子多的地方房价偏低 |
| DIS | 到波士顿主要就业中心的加权距离 | 连续值 | 距离越远房价越低 |
| RAD | 到径向高速公路的可达性指数 | 有序值 | 可达性好房价可能更高 |
| TAX | 每万美元房产税率 | 连续值 | 税率高增加持有成本,负相关 |
| PTRATIO | 城镇师生比 | 连续值 | 教育资源越好,比值越低,房价越高 |
| B | 黑人比例相关指标(1000(Bk-0.63)^2) | 连续值 | 按当年统计口径,取值越高房价不一定越高 |
| LSTAT | 低收入阶层人口比例 | 连续值 | 强负相关,这个特征很关键 |
目标值MEDV的取值范围大约在5到50之间,也就是5千到5万美元。放在七十年代这属于正常房价,放在今天看数值本身没有实际对照意义,但数据集的结构和特征间的关系仍然非常适合教学。
这类题目的标准做法是监督学习里的回归任务,输入是13维特征向量,输出是连续值。评分时大家通常看三个指标:均方误差(MSE)、平均绝对误差(MAE)和决定系数R²。R²越接近1说明模型解释力越强,两个误差越小说明预测越准。
1.2 高分作业的评价逻辑:代码之外还能做什么
很多同学把一个线性回归代码跑通就觉得完事了,其实从拿分角度看这远远不够。我当年做这类作业时,观察到的评卷偏好大致是这样的:
- 基础分:模型能跑通,指标有输出。这一步只要代码没问题就能拿到,但只拿这部分通常是及格到中等水平。
- 加分点一:有清晰的探索性数据分析(EDA)过程。比如画出MEDV的分布直方图、特征之间的相关性热力图,能解释哪些特征和房价高度相关,这就说明你真的在理解数据而不是在套代码。
- 加分点二:有合理的模型对比。不是只跑一个线性回归就结束,而是至少对比线性回归、岭回归和一个树模型(随机森林或梯度提升树),用同一个测试集比较三个模型的指标。
- 加分点三:文档里能写清楚“为什么”。比如为什么RM和LSTAT对房价影响大,为什么要做特征标准化,岭回归的正则化参数alpha怎么选的等等。这比堆代码有说服力得多。
说白了,波士顿房价预测这项作业的设计初衷不是让你背一个模型,而是让你完整走一遍“分析数据—设计模型—评估结果—得出解释”的流程。后面我讲的都是按这个逻辑来的。
2. 数据先行:拿到数据别急着建模
2.1 数据探索要做哪些事
我第一次接触这个数据集时,第一反应就是一行load_boston()然后关掉数据列,直接开训。后来发现这种做法的一大问题就是:你根本不知道模型学到了什么规律,出了指标也不确定靠不靠谱。
正确的打开方式分成三块。
**第一块:确认数据形态和缺失情况。**用df.info()和df.describe()先看一遍,确认506行没有缺失值,确认特征的量纲差异非常大——比如CRIM是从0.006到近90的大跨度,而CHAS只是0和1。这个发现直接影响后面要不要做标准化。
**第二块:看目标变量的分布。**画MEDV的直方图,你会发现这个分布并不是教科书式的正态分布,而是有些右偏,甚至尾部有点厚。右偏数据在进行线性回归时,残差可能不满足正态性假设,所以可以考虑对MEDV取对数,或者至少了解一下这一点。我实际对比过,取对数之后模型在RMSE指标上有小幅提升,但解释起来麻烦,作业里如果追求简洁可以不做,但在文档里写出来说明你注意到了这个现象,本身就是加分项。
**第三块:看特征与目标的关系。**最直接的方式是画一个相关系数矩阵热力图,或者对每个特征和目标做散点图。跑下来之后,结论基本是一致的:MEDV和RM(房间数)正相关最强,和LSTAT(低收入比例)负相关最强。这意味着在做线性模型时,这两个特征的权重会比较大,解释模型时就从这里切入。
2.2 特征工程的几个关键点
在这个项目里,特征工程看起来简单,实际上有几个容易踩的坑。
- 缺失值处理:这个数据集本身没有缺失值,只需要检查确认即可。如果遇到自己处理的数据有缺失,可以先看缺失比例,用均值/中位数填充还是丢弃要看场景。
- 离群点与异常值:比如CRIM这个特征的最大值接近90,而中位数只有0.256,说明存在一些犯罪率极高的异常区域。对于线性模型来说,这种极值会把模型拉偏。处理方式有两种:一是直接删除少数极端样本(需要说明理由),二是做标准化缓解但无法根除。我采用的做法是保留数据但在文档里分析这些点,因为删除样本会影响可复现性。
- 特征标准化:线性回归、岭回归、Lasso这类基于距离或梯度下降的模型对特征尺度敏感,而树模型不敏感。所以我的标准流程是:数据划分之后,在训练集上做
StandardScaler拟合,再同时应用到训练集和测试集。注意不能先标准化再划分,否则会有数据泄漏的嫌疑,作业里这点会被扣分。
在做特征工程时我最想强调的一点是:不要做无意义的特征删除。很多人为了追求模型的简洁性,直接删掉一些看似无关的特征,比如ZN、INDUS,觉得它们对MEDV的相关系数低。但多变量之间是存在交互效应的,A和Y没线性关系不代表A在B存在时对Y没有贡献。所以我的习惯是:先让模型自己决定特征重要性,而不是人工拍脑袋删列。
3. 模型选型与调参:从线性回归到集成学习
3.1 基准模型:先把线性回归跑通
对于一个回归任务,线性回归永远是那个“你总得先看看它表现如何”的基准模型。它的形式很简单:目标值等于特征的线性组合加上偏置项。
MEDV = w1*CRIM + w2*ZN + ... + w13*LSTAT + b线性回归的好处是简单、计算快、可解释性强,你可以直接把权重带回去看哪些特征对房价影响大。坏处是它假设特征与目标存在线性关系,而现实问题很少有完全线性的。波士顿房价数据里的RM和MEDV的关系,其实是有一定非线性成分的,这一点先记在心里,后面集成模型的效果更好就能说明问题。
跑这个模型时有一个指标容易被忽略:残差图。训练完之后,把预测值和真值的残差画出来,如果残差围绕0随机分布,说明模型没有明显的系统性偏差;如果出现漏斗形状(比如预测值越大的时候残差也越大),就说明模型在极端值上的表现有改进空间。我在第一次跑时就发现了这个现象,这个观察在后面对比树模型时非常有用。
3.2 正则化家族:岭回归与Lasso的选择
线性回归的下一步,就是考虑要不要加正则化。为什么要加?因为13个特征之间多少存在多重共线性,前面说的相关性热力图可以看出来,比如TAX和RAD的相关系数很高,DIS和NOX也显著负相关。这种共线性会让普通最小二乘法的参数估计方差变大,模型泛化能力下降。
岭回归(Ridge)使用L2正则化,把权重的平方和加进损失函数,约束权重不能太大。它保留了所有特征,只是整体缩小权重,适合特征间相关性较强的情形。Lasso(套索回归)使用L1正则化,有特征选择能力,会把不重要的特征权重压成0。对于这个数据集,我实际跑下来,岭回归比Lasso稳定,因为数据本身没有多到需要做强特征筛选的地步,强行让权重稀疏没有明显收益。
调参上只需要关注alpha这一个值。alpha太大模型会过于平滑,把有效信号也抹掉;alpha太小则又退化为普通线性回归。我用的是网格搜索加交叉验证,alpha的候选值取[0.001, 0.01, 0.1, 1, 10, 100],最后选出来的是1到10这个区间,说明适度的正则化有帮助,但不需要太强。
3.3 树模型的参数调优:随机森林和梯度提升
到了这一步,才算进入真正能拉开分数差距的环节。树模型,尤其是随机森林和梯度提升树,是处理这类表格数据的利器。它们能捕捉非线性关系,不做特征标准化也一样跑,而且往往效果比线性模型好一截。
随机森林的关键参数:
n_estimators:树的数量,一般500到1000就够,再大收益递减。max_depth:树的最大深度,不限制容易过拟合,建议在10到20之间做网格搜索。min_samples_split:内部节点再划分所需最小样本数,默认2容易过拟合,可以放大到5或10。random_state:一定要设固定值,保证结果可复现。我统一用42。
梯度提升树的关键参数:
n_estimators:提升轮数,同样500左右。learning_rate:学习率,一般从0.05到0.1之间调。学习率越小,需要更多树来拟合。max_depth:在梯度提升里通常比随机森林小,3到5就够,因为每棵树都在拟合残差,深度太深容易过拟合。subsample:每次迭代用多少比例的样本,设成0.8能增加随机性,降低过拟合。
我用随机森林跑出来的R²能到0.88左右,梯度提升树可以到0.91左右,比线性回归高出一截。对比下来就能发现,线性模型对非线性关系的建模能力确实有限,而树模型在这类中低维表格数据上的优势是实实在在的。
3.4 模型对比的坑:指标口径要统一
这里提醒一个我在作业评审中经常看到的问题:不同模型用不同指标评价,或是在不同的数据划分上比较,导致结论没有可比性。
我处理这个项目的固定设置是:
- 整个数据集按7:3划分训练集和测试集。
- 所有模型必须使用同一个划分结果,我通常会在划分后把数据存成
X_train, X_test, y_train, y_test,后续模型只和这些变量打交道。 - 统一用RMSE、MAE、R²三个指标评价。
- 随机种子全部固定为42(或者你选一个数字,关键是一致)。
这样的好处是,你对比的是模型本身的能力差异,而不是随机划分带来的波动。如果不固定随机种子,同一模型两次跑出来的R²可能相差0.02以上,写在报告里就是经不起推敲的结论。
4. 完整的代码实现:从读数据到结果可视化
4.1 环境准备与数据加载
我的运行环境是Python 3.9 + scikit-learn 1.0.2 + pandas 1.4.2 + matplotlib 3.5.1。数据加载这一块有一个非常严肃的坑要先说清楚。
早期版本的scikit-learn可以直接用from sklearn.datasets import load_boston加载,但scikit-learn 1.2之后这个接口被移除了。如果你用的是较新版本,一运行就会报错ImportError: load_boston has been removed from scikit-learn since version 1.2。这种“教材代码跑不通”的情况每年都有很多人踩。
我的处理方式是从外部直接加载CSV文件,这样不依赖sklearn版本,也更贴近真实项目的数据读取方式。数据集可以从一些镜像源获取,我这里的代码兼容两种加载方式,你按需选即可。
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression, Ridge, Lasso from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score import matplotlib.pyplot as plt # 方式一:文件加载(推荐,前提是本地已有boston_housing.csv) df = pd.read_csv('boston_housing.csv') # 方式二:如果环境支持旧接口(仅适用于sklearn < 1.2) # from sklearn.datasets import load_boston # boston = load_boston() # df = pd.DataFrame(boston.data, columns=boston.feature_names) # df['MEDV'] = boston.target # 分离特征与目标 X = df.drop('MEDV', axis=1) y = df['MEDV']4.2 划分数据集与标准化
数据划分的核心原则是:测试集必须像一个“从没见过的客人”。所以所有从数据中学到的参数,包括标准化的均值和方法,都只能在训练集上计算。
# 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42 ) # 标准化:先fit训练集,再transform测试集 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)注意fit和transform这两个方法的使用顺序不能弄反。fit_transform只用一次在训练集上,测试集只用transform。如果图省事,直接对整个X做标准化再划分,会让测试集的信息在训练阶段就被模型间接“偷看”到了,这在严谨的作业评审中是会被指出的问题。
4.3 模型训练与评估
这里我写一个统一的训练和评估函数,方便后续对比多个模型。
def train_evaluate(model, X_train, y_train, X_test, y_test, model_name): model.fit(X_train, y_train) y_pred = model.predict(X_test) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) mae = mean_absolute_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f'{model_name} 结果:') print(f' RMSE: {rmse:.4f}') print(f' MAE : {mae:.4f}') print(f' R² : {r2:.4f}') print('-' * 40) return model, y_pred # 1. 线性回归 lr, y_pred_lr = train_evaluate( LinearRegression(), X_train_scaled, y_train, X_test_scaled, y_test, 'Linear Regression' ) # 2. 岭回归 ridge = Ridge(alpha=1.0) ridge, y_pred_ridge = train_evaluate( ridge, X_train_scaled, y_train, X_test_scaled, y_test, 'Ridge Regression' ) # 3. 随机森林 rf = RandomForestRegressor(n_estimators=500, max_depth=15, min_samples_split=5, random_state=42) rf, y_pred_rf = train_evaluate( rf, X_train, y_train, X_test, y_test, 'Random Forest' ) # 4. 梯度提升树 gbr = GradientBoostingRegressor(n_estimators=500, learning_rate=0.05, max_depth=4, random_state=42) gbr, y_pred_gbr = train_evaluate( gbr, X_train, y_train, X_test, y_test, 'Gradient Boosting' )跑完能明显看到:线性回归和岭回归的R²都在0.72~0.78左右,随机森林到0.85+,梯度提升到0.90左右。这里有个细节:树模型使用原始特征(未标准化)即可,因为树模型只关心特征的顺序关系,不关心尺度。
4.4 两个可视化:预测对比和特征重要性
可视化的意义不只是“报告好看”,它还能帮你发现问题。第一个图是预测值与真实值的散点图。理想情况是点都落在y=x这条对角线上,偏离越大说明误差越大。我画出来的图中,梯度提升树的结果里点更密集地聚在对角线附近,而线性回归在房价较高(比如大于35)的区域明显偏离,这就是前面说的“线性模型对极端值拟合不足”的直观证据。
第二个图是特征重要性。随机森林和梯度提升树都提供feature_importances_属性,直接画条形图就行。
importances = gbr.feature_importances_ feature_names = X.columns indices = np.argsort(importances)[::-1] plt.figure(figsize=(10, 6)) plt.title('Feature Importances - Gradient Boosting') plt.bar(range(len(importances)), importances[indices]) plt.xticks(range(len(importances)), [feature_names[i] for i in indices], rotation=45) plt.tight_layout() plt.show()重要提醒:特征重要性排序只是模型层面的选择,不完全等于真实因果。LSTAT和RM排在最前是这个数据集的经典结论,和相关性分析是一致的。写文档时不要写“LSTAT是房价的唯一决定因素”这类话,而要写成“模型认为LSTAT对预测的贡献最大,这与其和MEDV的强负相关一致”。
5. 常见问题与排查技巧实录
5.1 问题速查表
每年做这个项目的人都会遇到类似的问题。我整理了一份列表,覆盖了我自己以及周围人踩过的常见坑:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
load_boston()报错ImportError | scikit-learn版本过高,接口被移除 | 改用CSV加载或fetch_openml方式 |
| 标准化后模型指标还是没提升 | 标准化不是万能药,树模型可能更适合 | 对比是否使用非线性模型 |
| 模型在测试集R²很高但预测图有明显偏差 | 过拟合,模型在训练集太好了 | 降低max_depth,增加min_samples_split,加强正则化 |
| 同一模型多次运行指标不一致 | 随机森林/梯度提升的随机性 | 固定random_state |
| LSTAT和RM权重很高,但决策失败 | 存在共线性或极端值影响 | 做异常值分析,尝试岭回归 |
| 梯度提升树训练时间过长 | n_estimators过大或max_depth过大 | 先用小轮数测试,再逐步加大 |
5.2 一个隐藏分数点:交叉验证
如果你的文档只写了“用train_test_split划分”,那只是一个常见做法。但严谨一点的作业会期望看到交叉验证的结果。因为单次划分的结果可能受到随机因素影响,交叉验证能提供更稳定的模型表现估计。
我常用的是5折交叉验证,再用网格搜索挑参数,这里给一个示例:
from sklearn.model_selection import cross_val_score, GridSearchCV # 候选参数 param_grid = { 'n_estimators': [100, 300, 500], 'max_depth': [5, 10, 15], 'min_samples_split': [2, 5, 10] } gbr_cv = GradientBoostingRegressor(random_state=42) grid_search = GridSearchCV( gbr_cv, param_grid, cv=5, scoring='neg_root_mean_squared_error', n_jobs=-1 ) grid_search.fit(X_train, y_train) print('最优参数:', grid_search.best_params_) print('最优交叉验证RMSE:', -grid_search.best_score_)注意scoring='neg_root_mean_squared_error',sklearn会把负号附在误差前面,所以最终要取负值才是常规RMSE。网格搜索跑完后,再对测试集进行一次最终评估,报告里的结论就会非常扎实。
5.3 写文档时容易被忽略的高分细节
这些内容和代码本身关系不大,但直接影响你的项目评分:
- 每个图的上面必须有图解释,不能只贴图。比如相关性热力图写完至少写一两句“从图中可以看出RM与MEDV正相关,LSTAT与MEDV负相关,因此这两个特征可能是预测的关键”。
- 模型对比表格是必备项。把线性回归、岭回归、随机森林、梯度提升的RMSE/MAE/R²放在同一张表里。读者扫一眼就能看出优劣差异。
- 结论部分不要只写“随机森林最好”,要交代为什么。比如“树模型能捕捉非线性关系,而房价与部分特征的关系明显非线性,所以树模型优于线性模型”。
- 代码注释要写“为什么”而不是“是什么”。比如不要在
X_train_scaled = scaler.fit_transform(X_train)边上写“标准化训练集”,而要写“在训练集上拟合标准化参数,避免数据泄漏”。
最后再聊几句
波士顿房价这个数据集,放在今天看有很多值得讨论的地方,比如某些特征定义带有时代局限性,甚至有人指出它存在数据方面的话语争议。但作为教学用例,它依然是回归任务里非常标准的“第一课”。你把这个项目的流程走完,后面换到任何其他回归数据集,框架都是通用的:先做数据探索,再做特征工程,然后从基准模型开始,逐步尝试更复杂的模型,最后统一口径评估并解释结果。
我个人做这个项目最大的体会是:分数不是看谁跑出来的R²高,而是看你的分析过程有没有逻辑闭环。模型可以不是最优的,但你得说清楚你为什么选用它、它在哪些情况下表现好、哪些位置还有改进空间。这个过程练出来的,就是做真实机器学习项目最核心的能力——结构化地解决问题。
代码我建议你拿过去之后,一行一行敲一遍,不要直接复制粘贴跑完就关。自己敲的时候你才会注意到每个参数怎么来、每行代码干了什么。等你能脱离参考代码独立写出完整流程的时候,这门课基本就稳了。
本文还有配套的精品资源,点击获取