简介:波士顿房价数据集是机器学习最经典的回归数据集之一,包含犯罪率、房间数、人口等特征,以及对应的房价中位数标签,适用于线性回归、决策树、随机森林等模型训练与评估,特别适合刚接触数据科学的学生和初学者作为入门实践。压缩包共3个文件,其中csv文件存放整理好的原始数据,py脚本提供快速加载与建模的示例代码,md文档对字段含义和使用方法作简要说明,整体仅15KB,轻量且易于分发。该资源已有1360人学习下载,是CSDN上人气较高的公开数据包。通过这份资料,读者可直接跳过繁琐的数据搜集与清洗环节,聚焦特征分析、模型搭建与预测效果调优,也可参照脚本和说明快速复现一次完整的回归建模流程,适合作为课程作业、教学案例或自学项目的起步素材。
1. 二十年前的房价数据,为什么今天还要下:波士顿房价数据集与它的应用边界
2025 年还在下一份 1978 年收集、1993 年发布的波士顿房价数据集,听起来像考古,但不管 Kaggle 上新数据集怎么卷,这个 506 样本、13 特征的小表格依然是回归入门绕不开的基准。它的价值不在“新”,而在“小且完整”:特征含义清楚、量纲差异大、存在明显的非线性关系和截断问题,正好把特征工程、数据泄漏、模型评估这些基本功都练一遍。适合刚学完 pandas 和 sklearn 的从业者拿它跑通全流程,也适合老手在五分钟内验证一个回归思路是否靠谱。
2. 拆包到首跑:zip 嵌套解压、字段识别与数据分布体检
2.1 压缩包到底装了什么:两层 zip 与文件格式差异
很多平台在上传数据集时会做二次压缩,下载下来经常是“波士顿房价数据集.zip”里面再套一层同名的 zip,解压完发现还有一层。不要急着双击,先在命令行里确认包结构。Linux 下我习惯先跑zipinfo看一眼,再决定解压路径:
zipinfo "波士顿房价数据集.zip" | head -20 unzip "波士顿房价数据集.zip" -d boston_data cd boston_data ls -la第一行zipinfo列出压缩包内的文件清单,head -20限制输出行数,避免文件多的时候刷屏;unzip -d boston_data指定解压到独立目录,防止文件散落到当前目录。解压后看到的可能是一个.data文件、.csv文件,也可能是另一个 zip。
这里有个很容易翻车的点:如果外层 zip 解压后出现“输入密码”的提示,先别急着找密码。很多二次打包工具只是把 zip 的 general purpose bit flag 里的加密位置成了 1,文件并没有真正加密,这就是常见的“zip 伪加密”。现象是双击就弹密码框,但用 7-Zip 直接拖拽文件出来往往能成功,或者用unzip -o强制覆盖也能绕过去。后面第四章展开说。
拿到文件后,不要盲信 README。下载来的版本可能被重新整理过,列名可能从全大写变成小写,甚至多出索引列。先识别字段再谈建模。
2.2 用 pandas 完成首次加载:最小可复现代码
不管原始文件后缀是.data还是.csv,先用 pandas 读进来,再统一检查结构。.data是旧格式,列之间用空格分隔,不是逗号,所以read_csv要指定sep:
import pandas as pd df = pd.read_csv("boston_housing.csv") # 如果解压出来是 .data 文件,用下面这行 # df = pd.read_csv("boston_housing.data", sep=r"\s+", header=None) print(df.shape) print(df.dtypes) print(df.isnull().sum())sep=r"\s+"表示按一个或多个空白字符分隔,适配老式.data文件;header=None是因为这种文件通常没有列名,列名要在读完之后手动指定。输出结果里,df.shape判断样本量和特征量是否 506×14,dtypes确认哪些特征是数值型、哪些是离散型,isnull().sum()检查缺失值——原版数据没有缺失,但二次整理的版本不一定,这一步不能省。
常见做法是先打印前五行,再跟网上资料核对列名顺序。波士顿房价的 13 个特征顺序是固定的:CRIM(犯罪率)、ZN(住宅用地比例)、INDUS(非商业用地比例)、CHAS(是否邻近查尔斯河)、NOX(一氧化氮浓度)、RM(平均房间数)、AGE(老房子比例)、DIS(到就业中心距离)、RAD(公路可达性)、TAX(房产税率)、PTRATIO(师生比)、B(社区构成相关指数)、LSTAT(低收入人群比例),最后一个是目标 MEDV(房价中位数,单位千美元)。
columns = ["CRIM", "ZN", "INDUS", "CHAS", "NOX", "RM", "AGE", "DIS", "RAD", "TAX", "PTRATIO", "B", "LSTAT", "MEDV"] df.columns = columns print(df.describe().T)describe().T会输出每个特征的均值、标准差、最小值、四分位数和最大值。这一步能立刻看出量纲差异:RM 均值 6 左右,TAX 均值几百,B 特征直接到几百,差异巨大,后续标准化跑不掉。
2.3 数据分布第一眼:相关性热力图与目标变量分布
建模前先看相关性和目标分布,这个习惯救过我很多次。波士顿房价这套数据最明显的特点是 MEDV 在 50 的位置有一排样本堆积,这是因为数据收集时对超过五万美元的房价做了截断处理,只记录为 50。如果不认识这个特征,后面所有模型的误差都会被这堆样本带偏。
import matplotlib.pyplot as plt import seaborn as sns plt.figure(figsize=(12, 10)) sns.heatmap(df.corr(), annot=True, cmap="coolwarm", fmt=".2f", linewidths=0.5) plt.show() df["MEDV"].hist(bins=30, edgecolor="black") plt.xlabel("MEDV (千美元)") plt.show()annot=True把相关系数直接标在格子里,fmt=".2f"控制数值保留两位小数。热力图里重点看两列:MEDV 与 RM 的相关系数在 0.7 左右,正相关;MEDV 与 LSTAT 在 -0.7 左右,强负相关。这两个特征基本决定了线性回归的上限。hist看 MEDV 分布时,注意最右边 50 那根柱子是不是明显偏高,那是截断样本,不是正常长尾。
提示:如果热力图里某个特征和 MEDV 的相关性出奇地高或者符号反了,先回 2.2 核对列名和读取方式,不要急着调模型。
3. 特征工程与回归基线:从 13 个原始特征到一套可复现流水线
3.1 特征逐个过:哪些直接能喂模型,哪些要动手脚
波士顿房价的 13 个特征不是都能直接丢进线性模型。下面这张表是我每次拿到这套数据都会过一遍的清单,按处理方式分组:
| 特征 | 含义 | 处理建议 |
|---|---|---|
| RM | 平均房间数 | 连续特征,保留原值,线性模型主力 |
| LSTAT | 低收入人群占比 | 连续特征,和 MEDV 近似负线性,保留 |
| DIS | 到就业中心加权距离 | 连续特征,保留 |
| CRIM | 人均犯罪率 | 右偏严重,可先取 log 再看分布 |
| ZN / INDUS | 用地比例 | 很多样本为 0,分布接近双峰,可保留也可分箱 |
| NOX | 一氧化氮浓度 | 连续特征,和 DIS 相关性高,注意多重共线性 |
| AGE | 老房子比例 | 连续特征,信息量偏弱,可保留 |
| RAD / TAX | 公路可达性 / 税率 | 等级数据,TAX 与 RAD 强相关,选一个即可 |
| PTRATIO | 师生比 | 连续特征,保留 |
| B | 社区构成相关指数 | 数值很大且分布集中,建议去掉对比效果 |
| CHAS | 是否邻近查尔斯河 | 二分类 0/1,别做标准化,当分类特征处理 |
这里特别说两个容易忽略的特征。CHAS 是 0/1 二值变量,逻辑上不应该进 StandardScaler,标准化后它只会变成两个固定值,虽然不影响线性回归的预测结果,但会让系数解释变得很别扭。B 特征历史上和社区构成相关,现在看带有明显的时代遗留问题,实际工程里我一般直接删掉,或者至少做一次“带 B”和“不带 B”的对比实验,你会发现它对模型影响很小,却会给评审留下不必要的质疑点。
Z世代的从业者可能还会犯另一个错:把 RAD 和 TAX 同时留下来。两个特征的相关系数超过 0.9,线性回归里会出现系数震荡,训练集上分数好看,测试集上翻车。处理办法是两者选其一,优先保留 TAX,因为 TAX 的语义更直观。
3.2 划分训练集与标准化:一个不会泄漏的流水线
数据泄漏是回归任务里最常见也最隐蔽的问题。很多人刚上手时先把整个数据集标准化,再切训练集和测试集,看起来没毛病,实际上测试集的信息已经通过均值和方法差混进了训练过程。正确的顺序永远是先切分,再对训练集 fit,对测试集只 transform:
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X = df.drop(columns=["MEDV"]) y = df["MEDV"] 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% 的样本做测试,506 个样本里大概 101 个;random_state=42固定随机种子,保证每次跑出来结果一致,这对后续调参对比非常重要。fit_transform先计算训练集的均值和标准差,再用同一套参数转换;transform只做转换,不重新计算,这就堵住了泄漏口。
如果想把流程再收紧一点,可以直接用 Pipeline 把标准化和模型串起来,训练时只 fit 一次:
from sklearn.pipeline import Pipeline from sklearn.linear_model import LinearRegression pipe = Pipeline([ ("scaler", StandardScaler()), ("reg", LinearRegression()) ]) pipe.fit(X_train, y_train) y_pred = pipe.predict(X_test)Pipeline 的好处是调参时不会忘记缩放,交叉验证也能自动处理每一折的标准化,不用手动维护训练集和测试集两套状态。
3.3 跑通线性回归基线:R²、MAE 的预期范围
线性回归是这套数据集的第一个合理基线。虽然 13 个特征里存在非线性关系,但线性模型能跑出一个不算难看的分数,给后面上树模型留出对比空间:
from sklearn.metrics import mean_absolute_error, r2_score from sklearn.metrics import mean_squared_error y_pred = pipe.predict(X_test) print("R2:", r2_score(y_test, y_pred)) print("MAE:", mean_absolute_error(y_test, y_pred)) print("RMSE:", mean_squared_error(y_test, y_pred, squared=False))预期范围我直接给你:简单线性回归在随机种子 42、测试集 20% 的情况下,R² 大概在 0.70 到 0.80 之间,MAE 约 3.2 到 3.8 千美元,RMSE 约 4.5 到 5.5 千美元。如果你跑出来 R² 低于 0.5,先检查是不是把 CHAS 也标准化了,或者 B 特征没删,或者把 MEDV 截断样本全留在了测试集里。
mean_squared_error的新版参数squared=False直接返回 RMSE,老版本没有这个参数,需要自己开根号。如果你用的 sklearn 版本较旧,改成np.sqrt(mean_squared_error(y_test, y_pred))即可。RMSE 比 MAE 大是正常的,因为平方项会让离群点的影响被放大,这里 MEDV=50 的截断样本就是推高 RMSE 的主力。
提示:这一步跑出来的 R² 不要发出去当结论,它只是基线。真正要看的不是绝对值,而是后面随机森林、XGBoost 相比这个基线能提升多少。
4. 避坑记录:load_boston 失效、zip 伪加密与数据泄漏的五个真实现场
4.1 现象:load_boston()直接报 ImportError,老教程全部失效
很多旧代码第一行是from sklearn.datasets import load_boston,现在跑会直接告诉你找不到这个函数。原因是从 sklearn 1.2 开始,官方因为数据集本身的伦理争议把它移除了,这是一个不可逆的决策,不要试图通过降级 sklearn 来绕过。解决方法是改用本地文件加载,也就是第二章的方式。如果你坚持要体验一键加载,可以用fetch_openml("boston", parser="pandas"),但需要联网且数据格式和原版有差异。我的习惯是下载好的 zip 解压后直接read_csv,一劳永逸。
4.2 现象:zip 解压要求输入密码,但压缩包介绍里没提密码
这是“zip 伪加密”的典型表现。很多二次打包工具只是把 zip 的加密标志位置为 1,并没有真正加密文件内容。解决路径分三步:先用 7-Zip 打开压缩包,尝试直接把文件拖拽到文件夹;不行就用命令行unzip -o强制覆盖;还不行就用zipinfo查看加密标志位。第二步还有一个副作用:解压出来的文件名可能乱码,这是 zip 格式在中文环境下的历史问题,解压后用ls -lb看真实文件名,再用mv改名即可。不要把时间耗在找密码上。
4.3 现象:模型在训练集上 R² 0.95,测试集上直接掉到 0.4
这是典型的数据泄漏。最常见的原因是先对整个数据集做了StandardScaler().fit_transform,再切分训练测试集。标准化时计算出的均值和方差已经看了全部样本,相当于把测试集的信息提前告诉了模型。解决方法是把切分放在最前面,或者用 3.2 的 Pipeline。还有一个隐蔽场景:有些教程让你用df.corr()筛选特征后再切分,这同样属于泄漏,因为相关性也是从全量数据算的。正确的做法是先用训练集算相关性、选特征,再去套测试集。
4.4 现象:CHAS 特征标准化后,模型系数解释变得很奇怪
CHAS 是 0/1 分类变量,放进 StandardScaler 后会变成两个固定数值,线性回归照样能跑,但输出的系数已经不能直接解释为“邻近查尔斯河对房价的影响”。而且当数据里有分类变量时,标准化会让后续的树模型分裂点变得难以调试。解决方法是把 CHAS 单独留在特征矩阵里,不参与标准化;如果是 Pipeline,可以给 CHAS 单独用ColumnTransformer处理。更简单粗暴的做法是直接去掉这个特征跑一遍对比,你会发现对 R² 影响很小。
4.5 现象:R² 很高但预测的房价明显偏贵,对不上业务认知
R² 只反映模型解释了多大比例的方法差,不反映误差的绝对大小。波士顿数据里有一批 MEDV=50 的截断样本,模型看到这些样本会把所有高房价都往 50 靠,结果就是 MAE 勉强能看,但 RMSE 被顶得很难看。解决方法是画一张“预测值 vs 实际值”的散点图,检查右上角是不是有一排横着的点;另一个做法是做一个“MEDV 是否等于 50”的标记列,看看模型在这部分样本上的表现。如果你要做真实房价预测,应该把截断样本单独拿出来分析,而不是硬塞进训练集。
5. 进阶玩法:把回归改成三分类,再用 SHAP 验证特征贡献
线性回归只能告诉你“LSTAT 越低房价越高”,但说不出这个特征到底在哪些样本上起作用。把 MEDV 切成低中高三档,转成分类任务,再用 SHAP 看特征贡献,才能把这张老数据表榨出新的信息。
df["MEDV_cat"] = pd.qcut(df["MEDV"], q=3, labels=["low", "mid", "high"]) from sklearn.ensemble import RandomForestClassifier X = df[["RM", "LSTAT", "DIS", "TAX", "PTRATIO", "NOX"]] y = df["MEDV_cat"] X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.2, random_state=42 ) model = RandomForestClassifier(n_estimators=300, max_depth=6, random_state=42) model.fit(X_tr, y_tr) print(model.score(X_te, y_te))pd.qcut按分位数切分,三档样本量接近,避免某一类太少导致评估失真。随机森林的分类准确率预期在 0.75 到 0.85 之间,比回归难看到一点,但对异常值更稳。特征只保留六个核心变量,是为了让 SHAP 图更清爽。
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_te) shap.summary_plot(shap_values, X_te, class_names=["low", "mid", "high"])SHAP 输出里,每个点代表一个样本,颜色代表特征值大小。你会看到 LSTAT 和 RM 在三种房价档位上的分布模式完全不同:低档房主要由 LSTAT 拉升贡献,高档房主要由 RM 决定。这个结论比线性回归的系数表直观得多,也是面试时展示理解深度的好素材。
玩到这里,这套 1978 年的数据才算真正被吃透。从那以后我每次拿到陌生数据集,都强制自己走一遍shape、dtypes、缺失值、分布、相关性到基线模型这条固定流水线,再谈花活。波士顿房价数据集足够老,但这条肌肉记忆是新的,希望帮到你。
本文还有配套的精品资源,点击获取