简介:波士顿房价数据集是机器学习与数据分析领域的经典入门数据,常被用于回归预测、特征工程与模型评估练习。资源适合正在学习Python机器学习、希望用真实数据完成房价预测实践的学习者,也适用于教学演示与快速原型验证。压缩包共3个文件,分别为csv格式的房屋属性数据、py格式的模型训练脚本以及md格式的使用说明,整体大小仅15KB,结构精简,便于下载后即刻查看。其中数据文件包含犯罪率、房屋面积、房龄等关键特征,脚本覆盖数据加载、切分与常用回归模型训练过程,说明文档则梳理了字段含义与运行方式。已有1361人学习下载,对初学者而言,该资源能有效降低上手门槛,免去自行整理数据的麻烦,可直接依托脚本理解波士顿房价预测的完整流程,并为后续改进模型、尝试不同算法提供基础。
1. 波士顿房价数据集:最容易被低估的回归入门数据集
很多人在入坑机器学习时,第一份拿来练手的就是波士顿房价数据集。它不大,506 条样本,13 个特征,一个连续型目标变量 MEDV,恰好覆盖了回归任务里最常见的一整套动作:数据清洗、特征选择、标准化、模型训练、结果评估。我见过不少人在 Kaggle 上跑完几个竞赛项目后,反而回头用这份数据补基础,因为它的特征之间有明显相关性、有缺失值、还有少数异常样本,这些恰恰是真实业务数据里最常见的三种情况。这份 zip 里装的就是这个经典数据集的 CSV 文件包,适合刚接触回归的新手照着走一遍完整流程,也适合带学生的讲师拿来做课堂样例。接下来我会从文件结构、字段含义、加载方式、训练流程和常见坑五个层面把它拆开,让你下载后能直接复现一整套回归建模流程。
2. 拆开 zip 先看数据:文件结构、字段字典与数值单位
2.1 压缩包里装了什么文件
下载后解压,里面通常是一个 CSV 文件加一份简短说明文档。CSV 是这类数据集最通用的分发格式,既能用 Excel 打开,也能用 pandas、R 或者 SQL 直接导入。有的打包版本会把描述文件单独放一个README.txt,里面写明了字段来源和数据说明,建议解压后先把这份文档打开看一遍。
我在处理这类打包数据时,习惯先建立一个工作目录,把数据和脚本分开:
mkdir boston-housing && cd boston-housing unzip -o 波士顿房价数据集.zip ls -la-o参数表示覆盖同名文件,防止重复解压时提示交互。执行完ls -la后你会看到.csv文件和说明文档,文件名通常会带年份或版本标记。这一步骤本身没有技术难度,但能帮你确认解压是否完整——如果 CSV 文件只有几 KB,说明可能解压不完整,需要重新下载。
2.2 十四个字段逐个过:含义、边界值与用途
数据集包含 13 个特征和 1 个目标变量,所有字段全部是数值型,没有字符串类型。下面是完整的字段字典,单位标注清楚,训练前最好对照这个表格检查一遍数据范围。
| 字段名 | 含义 | 单位 | 边界情况 |
|---|---|---|---|
| CRIM | 城镇人均犯罪率 | 每千人犯罪数 | 最大值接近 89,严重右偏 |
| ZN | 占地超过 25,000 平方英尺的住宅用地比例 | 比例 | 大量样本为 0 |
| INDUS | 城镇非零售商业用地比例 | 比例 | 与 NOX 高度相关 |
| CHAS | 是否临查尔斯河 | 0/1 | 只有 35 条为 1,类别极度不平衡 |
| NOX | 一氧化氮浓度 | 每千万分之一 | 范围 0.385 到 0.871 |
| RM | 平均房间数 | 间 | 最小 3.561,最大 8.78 |
| AGE | 自住单位中建于 1940 年前的比例 | 比例 | 大量分布在 50 到 100 |
| DIS | 到五大就业中心的加权距离 | 英里 | 与 NOX 呈负相关 |
| RAD | 径向公路可达性指数 | 指数 | 取值离散,1 到 24 |
| TAX | 房产税率 | 每 1 万美元 | 与 RAD 存在强相关 |
| PTRATIO | 城镇师生比 | 比值 | 范围 12.6 到 22 |
| B | 城镇黑人比例相关的综合指标 | 指数 | 按公式 1000 * (Bk - 0.63)^2 计算 |
| LSTAT | 低收入人群比例 | 百分比 | 与 MEDV 负相关最强 |
| MEDV | 自住房屋价格中位数 | 千美元 | 最大 50,疑似截断 |
这里最容易被新手忽略的是 MEDV 的上限问题。数据集源自 1978 年,当时统计时对超过 50,000 美元的房价做了统一处理,全部按 50 记录,所以目标变量在 50 处有一个明显的平台。这不是数据损坏,而是采集时的截断行为。如果你做后续的回归分析,要注意模型可能在这个位置出现系统性的低估。
2.3 数据口径与版本差异
波士顿房价数据集最早由 Harrison 和 Rubinfeld 在 1978 年整理,后来被卡内基梅隆大学维护的 UCI 机器学习仓库收录,再后来被 sklearn 直接内置。这份 zip 里的 CSV 和 sklearn 内置版本仅有一个字段名的差异:sklearn 内置版把目标变量命名成target,而原始 CSV 版本里叫MEDV。其他字段名完全一致,都是小写。
这带来一个实际问题:如果你在网上搜到的大部分教程都是基于 sklearn 内置版本写的,直接套用到 CSV 版本上会出现列名报错。解决办法很简单,读取之后把列名改一下就行。
import pandas as pd df = pd.read_csv("housing.csv") df.rename(columns={"MEDV": "target"}, inplace=True)我一般不建议只依赖 sklearn 的load_boston()接口,因为它的数据经过了标准化预处理,新手容易误以为原始数据就是这样干净整洁的。用 CSV 版本才能看到真实的脏数据形态,这对理解什么叫做缺失值处理更有帮助。
3. 用 pandas 把 CSV 读进 DataFrame:最小可用代码与关键参数
3.1 读文件前先搞清楚的三个参数
CSV 读取看起来简单,但有几个参数直接决定你能不能拿到一份干净的数据。第一个是表头header,这份数据的 CSV 第一行就是字段名,所以header=0即可,不需要额外设置;第二个是缺失值标记na_values,早期版本的数据里空缺位置可能填的是字符串"NA"或空字符串,读取时要告诉 pandas 这两种情况都算缺失;第三个是数据类型dtype,保险起见用float64统一读入,CHAS 列虽然是 0/1 整数,但存成浮点数对建模没有影响。
import pandas as pd import numpy as np df = pd.read_csv( "housing.csv", header=0, na_values=["NA", "", "N/A"], dtype=np.float64 ) print(f"原始样本数: {len(df)}") print(f"缺失值统计:\n{df.isnull().sum()}")这段逻辑里最关键的是na_values。如果你不指定它,"NA"会被当成普通字符串读成一个 object 列,后面做df.dtypes检查时会发现 14 列里有 3 列是object类型,模型直接报错。指定为np.float64之后,所有特征列统一为浮点数,后续才能直接喂给 sklearn 的接口。
3.2 缺失值处理:MEDV 的缺失与填充策略
原始数据集本身缺失值不多,但如果用的是网络流传的修正版本,可能包含人工删除或添加过的样本,导致某些特征出现少量缺失。处理缺失值时,我一般先看缺失比例。少于 1% 就删除该行,超过 5% 就得认真考虑用均值或中位数填充。
# 删除缺失值占比过高的行 before_count = len(df) df = df.replace([np.inf, -np.inf], np.nan).dropna() after_count = len(df) print(f"删除前后样本数变化: {before_count} -> {after_count}")注意代码里的replace([np.inf, -np.inf], np.nan)这行,它把正负无穷也统一转成缺失值。CSV 文件里偶尔会出现INF字符串,pandas 默认不会把它解析成NaN,而是读成一个字符串对象,这行代码就是为了堵住这个漏洞。
如果缺失值发生在 MEDV 上,我倾向于直接删掉对应行,而不是填充,因为目标变量的填充会引入错误的监督信号,哪怕只填一条,也可能让模型学到一个根本不存在的标签分布。
3.3 划分训练集与测试集:随机种子决定能不能复现
数据划分是回归任务里最容易出问题也是最少被重视的一步。很多人在同一个数据集上反复调参,测试集成绩越调越好,但模型换到新数据上效果惨淡,就是因为测试集泄漏了信息。正确的做法是划分一次之后固定住,后续所有操作都在训练集内完成。
from sklearn.model_selection import train_test_split feature_cols = [ "CRIM", "ZN", "INDUS", "CHAS", "NOX", "RM", "AGE", "DIS", "RAD", "TAX", "PTRATIO", "B", "LSTAT" ] X = df[feature_cols] y = df["MEDV"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) print(f"训练集样本数: {len(X_train)}, 测试集样本数: {len(X_test)}")random_state=42是让每次运行得到同一个切分结果的关键参数。如果你不设置,系统会取当前时间作为随机种子,跑两次代码得到的训练集不一样,模型结果自然也不一样,后续任何调参对比都失去参考价值。至于这个值本身,42 只是个习惯约定,换成其他任意整数都可以,只要固定住就行。
4. 特征工程与训练:从线性回归到随机森林的完整流程
4.1 特征尺度差异与标准化
波士顿房价数据集的 13 个特征尺度差异极大。CRIM 的均值是 3.6,方差接近 74,而 CHAS 只有 0 和 1 两个取值。如果你直接拿原始数据训练线性模型,模型会把更多权重放在数值大的特征上,这并不代表这些特征对房价的贡献更大,纯粹是数值尺度的偏差。
svm、线性回归和 knn 这类基于距离的模型对尺度敏感,必须做标准化。树模型不受尺度影响,可以跳过这一步。常见做法是只对训练集做标准化,测试集用训练集的统计结果做变换,防止信息从测试集流向训练过程。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)这里fit_transform和transform的用法是很多教程会少讲一笔的地方。fit_transform先计算训练集的均值和方差,再用它完成标准化;测试集上只能用transform,不能重新计算统计量。如果对测试集单独fit,训练过程和测试过程使用的特征分布基准不一致,在线性模型上会导致预测结果偏移。
4.2 训练线性回归模型与评估指标
标准化之后就可以上模型了。线性回归是第一个必须跑的模型,它不是用来拿高分的,而是用来做基线的。如果线性回归的 R² 就达到了 0.7,说明特征和目标的关系接近线性,用复杂模型提升的空间有限;如果线性回归只有 0.4 而随机森林能到 0.8,说明特征之间明显存在非线性交互,这时候才值得上更复杂的模型。
from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score, mean_squared_error lr_model = LinearRegression() lr_model.fit(X_train_scaled, y_train) y_pred_lr = lr_model.predict(X_test_scaled) mse = mean_squared_error(y_test, y_pred_lr) rmse = mse ** 0.5 r2 = r2_score(y_test, y_pred_lr) print(f"线性回归 RMSE: {rmse:.3f}") print(f"线性回归 R²: {r2:.4f}")RMSE 的单位和 MEDV 相同,都是千美元,所以 3.5 意味着模型预测的平均误差在 3500 美元左右。只看 RMSE 不够,还要结合 R² 看解释度。如果 RMSE 低但 R² 也低,可能只是数据本身方差小,模型并没有真正抓住特征和目标的规律。
4.3 树模型的优势与交叉验证
随机森林在中小型表格数据上几乎总是能超越线性回归,因为它的特征交互不需要人为构造。不用先做特征交叉,直接把 13 个特征丢进去就行,树分裂过程会自动搜索交互关系。
from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import cross_val_score rf_model = RandomForestRegressor( n_estimators=200, max_depth=8, min_samples_leaf=4, random_state=42 ) scores = cross_val_score( rf_model, X_train_scaled, y_train, cv=5, scoring="r2" ) print(f"交叉验证 R²: {scores.mean():.4f} (± {scores.std():.4f})") rf_model.fit(X_train_scaled, y_train) y_pred_rf = rf_model.predict(X_test_scaled) print(f"随机森林测试集 R²: {r2_score(y_test, y_pred_rf):.4f}")n_estimators=200是树的棵数,200 棵对 404 条训练样本来说足够,再大收益很小;max_depth=8限制树的最大深度,防止单棵树完全记住训练样本;min_samples_leaf=4要求每个叶子节点至少 4 条样本,进一步抑制过拟合。交叉验证的cv=5代表 5 折,全部 404 条训练数据被切成 5 份,每份轮流做验证集。
R² 达到 0.85 以上属于正常水平,这份数据本身噪声不大,再往上压需要做特征筛选或尝试梯度提升类模型,但对练手来说到这里已经完成闭环了。
5. 排查与避坑:从加载数据到复现结果之间的 6 个高频问题
5.1 不同来源的版本混用导致特征列错位
现象:代码是从 sklearn 内置示例改装来的,直接把它作用于 CSV 版本,读取时报错或者模型训练结果明显偏离教程数值。原因:sklearn 内置版目标变量名为target,而 CSV 版是MEDV,两个版本在内部存储上也有细微差别。解决:统一以 CSV 文件字段为准,用df.columns打印字段列表,逐一核对后再决定是否重命名。
5.2 缺失值被当成了数值 0
现象:模型能跑通,但系数里 CHAS 的权重异常高,交叉验证结果忽高忽低。原因:READCSV 时na_values未配置,"NA"被强制转成了 NaN,但某些填充逻辑里用了fillna(0),把 0 当作了一个有效取值输入模型。解决:缺失值的填充策略要分列处理,CHAS 列的缺失应填众数或直接删行,不能统一填 0。
5.3 标准化时把训练集和测试集一起 fit
现象:训练集评估指标很好,测试集指标差了一截,而且每次跑结果漂移。原因:对 X 全量数据做了一次fit_transform,再切分训练和测试,测试集的统计信息提前进入了归一化参数,等价于轻微的数据泄漏。解决:先切分,再在训练集上fit_transform,在测试集上只transform。
5.4 目标变量被选成了特征
现象:feature_cols列表里多写了MEDV,训练时特征矩阵和目标向量包含同一个字段,模型 R² 接近 1,看起来完美但完全不可用。原因:列名筛选时没做排除,直接把全部列丢进特征矩阵。解决:用feature_cols = [col for col in df.columns if col != "MEDV"]过滤,或者打印df.columns.tolist()人工核对列表。
5.5 RMSE 计算时单位混乱
现象:算出来的 RMSE 是几千甚至几万,但教程里都是个位数,怀疑是数据或模型出了问题。原因:MEDV 原始单位是千美元,如果你的代码在某个环节乘了 1000,比如试图把单位转成美元,RMSE 会同步放大。解决:建模全流程保持原始单位,仅在结果展示时用文字说明等价美元。RMSE 的对比必须在同一单位下进行。
5.6 随机划分导致相同代码得到不同结果
现象:同一个模型,同一个参数,两次运行 R² 分别是 0.82 和 0.86,差距明显且无法解释。原因:train_test_split没有设置random_state,两次划分的测试集组成不同。解决:所有涉及随机过程的接口,包括模型本身的random_state,都设置成同一个整数。这份数据总共只有 506 条样本,测试集只有 100 条左右,一次划分的差异足以造成 0.04 以上的 R² 波动。
6. 最后一步:把完整流程合并成一键脚本
6.1 全流程脚本
前面的步骤拆开讲是方便理解,实际使用时我需要一个能反复执行的脚本,输入 CSV,输出模型评估报告。下面给出一份可直接运行的完整流程,包含数据加载、清洗、切分、标准化、三模型对比和结果汇总。
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score, mean_squared_error # 1. 数据加载 df = pd.read_csv("housing.csv", na_values=["NA", "", "N/A"]) df = df.replace([np.inf, -np.inf], np.nan).dropna() # 2. 特征与目标分离 feature_cols = [ "CRIM", "ZN", "INDUS", "CHAS", "NOX", "RM", "AGE", "DIS", "RAD", "TAX", "PTRATIO", "B", "LSTAT" ] X = df[feature_cols] y = df["MEDV"] # 3. 数据划分 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 4. 标准化 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 5. 模型训练与评估 models = { "LinearRegression": LinearRegression(), "RandomForest": RandomForestRegressor( n_estimators=200, max_depth=8, min_samples_leaf=4, random_state=42 ), } for name, model in models.items(): cv_r2 = cross_val_score( model, X_train_scaled, y_train, cv=5, scoring="r2" ).mean() model.fit(X_train_scaled, y_train) y_pred = model.predict(X_test_scaled) test_r2 = r2_score(y_test, y_pred) rmse = mean_squared_error(y_test, y_pred) ** 0.5 print(f"{name:>16s} | CV R²={cv_r2:.4f} | Test R²={test_r2:.4f} | RMSE={rmse:.3f}")这段脚本覆盖了前面提到的所有关键点:缺失值统一处理、切分固定随机种子、标准化区分训练和测试、交叉验证与测试集双重评估。输出里同时给出交叉验证和测试集两个指标,交叉验证反映模型稳定性,测试集反映最终泛化能力,两个数值差距越小越好。如果相差超过 0.1,基本可以判断训练集和测试集的分布不够一致,需要检查切分代码。
6.2 自检清单
脚本跑完之后,我习惯用四个条件核对结果是否符合预期。
| 检查项 | 预期值 | 偏差说明 |
|---|---|---|
| 样本数 | 506 行 x 14 列 | 小于 500 行说明有丢数据 |
| 特征数 | 13 个 | 少于 13 个说明列名有误 |
| 线性回归 R² | 0.65 - 0.75 | 低于 0.6 检查标准化是否漏做 |
| 随机森林 R² | 0.80 - 0.88 | 高于 0.95 怀疑数据泄漏 |
这份数据我拆过很多次,每次跑到随机森林这一步都能稳定得到 0.82 以上的 R²,线性回归稳定在 0.7 左右。如果你跑出来的数值偏差很大,优先回查随机种子和缺失值处理这两个环节,九成问题出在这两处。从那以后我每次拿到一份新的数据集,都会强制走一遍「加载 — 检查缺失 — 固定随机种子 — 训练基线 — 交叉验证」这套流程,再去碰参数调优,才没再被那些莫名其妙的指标波动带偏过。希望这份笔记对你有帮助。
本文还有配套的精品资源,点击获取