简介:波士顿房价数据集是机器学习中最经典的回归数据集之一,广泛用于价格预测模型的入门练习。这份资源面向数据科学初学者、高校学生以及需要快速验证回归算法的开发者,提供一套可直接使用的精简数据集和配套脚本。压缩包共3个文件,包含 CSV 格式的房价样本数据、Python 脚本和说明文档,整体大小仅 15KB,轻量易用,省去了自行收集和清洗数据的步骤。目前已有 1360 人学习下载,常用于课程作业、算法对比和教学演示。通过该数据包,读者可以快速掌握数据加载、特征探索、回归建模及误差评估的完整流程,同时可结合说明文档理解每个字段的含义,并基于脚本修改特征或更换模型继续扩展,是新手熟悉机器学习项目流程的理想起点。
1. 波士顿房价数据集.zip:一个老数据集为什么值得认真对待
大多数人的第一个回归模型,跑的就是波士顿房价数据集.zip。它只有 506 条记录、13 个特征,压缩包本身不到 200KB,却是 scikit-learn 自带示例数据集里被引用最多的一份。很多人只是load_boston()一下然后跑个线性回归就丢到一边,实际上这份数据在课程之外有更高的价值:它同时暴露了缺失值、变量截断、多重共线性和特征语义错位四类经典问题,正好是做回归建模和特征工程的入门标本。
这篇文章要做的不是帮你再敲一遍load_boston,而是从拿到的波士顿房价数据集.zip开始,完整走一遍:解压和确认文件身份 → 数据清洗与特征关系分析 → 建立回归基线与调参 → 排掉那些让新手和熟手都翻过车的坑。整个过程用 Python 实现,每一步都有可直接复制的代码和参数说明,读完你可以用自己的数据替换整个流程。
2. 先从 zip 包里取出真身:解压、伪加密与文件确认
2.1 用 zipfile 安全解压,别直接双击
拿到波士顿房价数据集(波士顿房价数据集.zip).zip这种套娃命名,第一反应应该是警惕而不是兴奋。很多网盘资源和课程附件都是二次打包,外层 zip 解出来可能还是一个 zip,也可能混进了同名不同内容的文件。我一般习惯用 Python 的zipfile模块统一处理,比 7-Zip 和右键解压在可复现性上强得多,而且对伪加密文件有更明确的报错。
import zipfile from pathlib import Path zpath = Path("波士顿房价数据集(波士顿房价数据集.zip).zip") outdir = Path("./boston_data") with zipfile.ZipFile(zpath, "r") as zf: for name in zf.namelist(): print(repr(name), zf.getinfo(name).file_size) zf.extractall(outdir)这段代码先把 zip 内所有文件名打印出来,再解压到指定目录。namelist()返回的是压缩包内部路径列表,打印出来的每个 entry 名字和压缩前大小,能让你在解压前就判断这个包是不是正经文件。extractall在 Zip Slip 这类路径穿越攻击上已内建防护,但要注意:如果文件名里带中文且解压后出现乱码,那是 Windows 下 zipfile 对 GBK 编码的兼容问题,后面避坑章会专门处理。
参数上,ZipFile第二个参数"r"表示只读,这是日常工作里足够安全的模式;如果解压几十个 GB 的大包,建议改用zf.open(name)逐个流式读取,避免一次性撑爆内存。
2.2 伪加密 zip 的识别与绕过思路
伪加密是热词里出现频率很高的一个词,在数据包场景里更是常见。某些分享者为了防盗用,会把 zip 的加密标志位手动改掉——文件看起来有密码、解压工具会弹窗要求输密码,但实际上文件内容完全没有加密,只是general purpose bit flag的 bit 0 被置位了。
import zipfile zpath = "波士顿房价数据集(波士顿房价数据集.zip).zip" with zipfile.ZipFile(zpath, "r") as zf: for info in zf.infolist(): flag = info.flag_bits encrypted = bool(flag & 0x1) print(f"{info.filename}: flag_bits={flag:#06x}, encrypted={encrypted}")这段代码读flag_bits的低位。0x1是加密标志位;如果输出encrypted=False,但解压时依然弹密码框,那就是典型的伪加密。常见做法是用 7-Zip 打开文件看「加密」列是否显示+:显示了但 flag 位为 0,可以直接用 Python 按无密码方式解出来。注意,真加密的文件这里会显示True,不要去尝试任何密码爆破——大部分课程数据包不会故意设密码,设了密码的十有八九是营销引流,直接放弃比花时间改造更划算。
2.3 解压后的标准文件清单与身份核对
解压完成后,正确的波士顿房价数据集一般包含以下几个文件,你可以对照自己手上这份做身份确认:
| 文件 | 类型 | 说明 |
|---|---|---|
housing.data | 原始数据 | 经典 UCI 格式,每行是一条样本,只有数值没有表头 |
housing.names | 特征说明 | 给出每个特征的含义、单位和缺失值标记方式 |
data.csv | 课程版 | 很多人第一步会转成 CSV 方便读取 |
description.txt | 说明文档 | sklearn 数据描述文本,包含数据集背景、特征列表和引用来源 |
拿到文件后第一件事不是pd.read_csv,而是核对housing.names里的特征列表。经典波士顿房价数据的特征顺序是固定的:CRIM, ZN, INDUS, CHAS, NOX, RM, AGE, DIS, RAD, TAX, PTRATIO, B, LSTAT,目标值是MEDV。如果你手上的 CSV 列名和这个顺序不一致,后续所有模型解读都会错位。
3. 数据清洗:缺失值、截断值与列名错位
3.1 读入数据,确认形状与缺失值分布
波士顿房价数据经常被人忽略的一点是:它本身没有显式的NA值,却有实际缺失。housing.names里明确写了ZN(住宅用地比例)、CHAS(是否临河)、B(黑人比例相关)等变量在某些样本里是不存在的,缺失标记用的不是NaN,而是-1或0这种业务上的「无效值」。直接用pd.read_csv读进来,isnull()查不到任何缺失,这是最容易踩的第一个暗坑。
import pandas as pd df = pd.read_csv("boston_data/data.csv") print("shape:", df.shape) print("columns:", df.columns.tolist()) for col in df.columns: n_valid = df[col].notna().sum() n_zero = (df[col] == 0).sum() n_neg = (df[col] < 0).sum() print(f"{col:8s}: non-null={n_valid:4d}, zero={n_zero:4d}, negative={n_neg:4d}")shape应该输出(506, 14),13 个特征加 1 个目标变量。如果多一列或者少一列,先回到 2.3 的列顺序核对。n_neg统计负值,ZN和CHAS会有大量 0 属正常;但如果MEDV出现 50,说明样本是从截断分布里抽出来的——1980 年代初期的数据采集把房价大于 50 的样本统一记录成 50,这是数据采集方做的「右截断」,不是随机缺失。
3.2 缺失值策略:不是所有 0 都能当缺失
处理缺失之前要分清三种情况。第一类:CRIM(犯罪率)、RM(平均房间数)、DIS(到就业中心距离)这些连续特征存在个别样本为 0 或缺失,可以用中位数填充;第二类:ZN的 0 是「非住宅用地」的真实语义,不能删也不能填;第三类:CHAS的 0/1 是分类变量,填中位数会引入假的分布形态,只能用众数或直接modeling时保留类别。
from sklearn.impute import SimpleImputer import numpy as np medians = df.median(numeric_only=True) df_clean = df.copy() for col in ["CRIM", "ZN", "INDUS", "NOX", "RM", "AGE", "DIS", "RAD", "TAX", "PTRATIO", "B", "LSTAT"]: df_clean[col] = df_clean[col].replace(0, np.nan) if df_clean[col].isnull().sum() > 0: imputer = SimpleImputer(strategy="median") df_clean[col] = imputer.fit_transform(df_clean[[col]]).ravel()这里的replace(0, np.nan)是刻意把所有 0 都视为缺失,但在做之前必须看 3.1 的统计输出:ZN的 0 是真实语义,不应该被填充;CHAS的 0 也是真实类别。所以更稳妥的做法是先把ZN、CHAS、RAD这三个列从替换名单里移出去,只对连续数值列做缺失填补。SimpleImputer的strategy="median"比均值更抗离群值,比如TAX列有666这种极端值,均值会被拉偏,中位数更稳。
3.3 特征间的共线性与目标变量关系
波士顿房价数据最有教学价值的是它的特征之间高度相关。比如RAD(高速公路可达性)和TAX(房产税率)相关系数超过 0.9,INDUS与NOX、DIS也有强耦合。对线性回归这是灾难,对树模型反而不敏感,这会影响你后面模型选型的判断。
import matplotlib.pyplot as plt import seaborn as sns corr = df_clean.drop(columns=["MEDV"]).corr() plt.figure(figsize=(10, 8)) sns.heatmap(corr, annot=False, cmap="RdBu_r", vmin=-1, vmax=1) plt.title("Feature Correlation Matrix") plt.show()一般做法是画完热力图后,找与MEDV相关性最高的几个特征——LSTAT(低收入人口比例)通常负相关最明显,RM则正相关最明显。这两个特征单独建模就能解释大部分方差,适合你在后面做基准时先跑一版「双特征模型」对比效果。还要注意:RAD与TAX这种共线性对线性回归的系数解释影响很大,后面要用岭回归对比普通最小二乘。
4. 用波士顿房价做基线模型:线性回归到随机森林
4.1 划分训练集与测试集,设置随机种子
波士顿房价数据只有 506 条,划分上要特别小心。课程里常见的train_test_split(test_size=0.3)对 506 条来说测试集只有约 152 条,单次划分的方差非常大。我一般至少用 5 折交叉验证,并在划分前先对特征做标准化。
from sklearn.datasets import load_boston from sklearn.model_selection import train_test_split X = df_clean.drop(columns=["MEDV"]) y = df_clean["MEDV"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) print(f"train: {X_train.shape}, test: {X_test.shape}")random_state=42是行业默认值,方便复现。实际业务里建议把random_state做成参数,扫多个值(42/0/2024)看模型指标波动,如果波动超过 5%,说明数据量不够,单次划分不可信,要靠交叉验证。test_size我很少用 0.3,在这个样本量下 0.2 已经是上限,再大会让训练集不足 400 条,模型方差变大。
4.2 线性回归+Ridge:处理共线性问题
用普通线性回归跑一遍,然后立刻换 Ridge 对比。普通最小二乘在RAD与TAX的强共线性上会出现系数方差放大,Ridge 的 L2 惩罚可以收缩系数,让模型更稳定。
from sklearn.linear_model import LinearRegression, Ridge from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline scaler = StandardScaler() lin = make_pipeline(scaler, LinearRegression()) ridge = make_pipeline(scaler, Ridge(alpha=1.0)) for name, model in [("linear", lin), ("ridge", ridge)]: model.fit(X_train, y_train) r2_train = model.score(X_train, y_train) r2_test = model.score(X_test, y_test) print(f"{name}: R2 train={r2_train:.4f}, test={r2_test:.4f}")make_pipeline把标准化和回归连在一起,防止你在训练集上学到StandardScaler参数后直接用在测试集——那是在数据泄露的边缘试探。Ridge(alpha=1.0)的alpha是正则强度,1.0 是中等强度;可以扫[0.1, 1, 10]三个量级,观察测试集 R2 变化。若alpha=10时 R2 明显下降,说明正则过强,数据本身共线性不严重。
这一节跑完,你手上就有了两个对比基准。普通线性回归的 R2 一般是 0.7 左右,Ridge 稍微好一点或持平,但系数更合理。如果线性回归 R2 比随机森林还高,多半是问题设定有误。
4.3 随机森林:处理非线性关系与特征交互
随机森林在波士顿房价数据上通常跑得比线性回归好一截,原因在于LSTAT与MEDV的关系不是线性的,低收入比例低于 5% 时房价上升速率明显加快。随机森林不需要特征标准化,可以直接在原始值上训练。
from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import cross_val_score rf = RandomForestRegressor( n_estimators=300, max_depth=8, min_samples_leaf=3, random_state=42, n_jobs=-1, ) scores = cross_val_score(rf, X_train, y_train, cv=5, scoring="r2") print(f"RF CV R2: {scores.mean():.4f} ± {scores.std():.4f}")n_estimators=300在 506 条样本上训练时间不到 10 秒,没必要用 100 以下。max_depth=8限制树深度,防止小样本上过拟合;min_samples_leaf=3确保叶子节点至少 3 条样本,对波士顿这种小数据集是合理的默认值。跑完交叉验证后,把 RF 在测试集上的 R2 和 4.2 的线性回归对比,通常 RF 能到 0.85 附近,线性回归 0.7 左右,差距主要来自非线性项。
4.4 超参数粗调:n_estimators 与 max_depth
小数据集的超参数调参不像大数据那样讲究,全网格搜索反而容易过拟合。我一般只扫两个维度:n_estimators看收敛曲线,max_depth看偏差方差权衡。
import numpy as np results = [] for depth in [4, 6, 8, 10, None]: rf = RandomForestRegressor( n_estimators=300, max_depth=depth, random_state=42, n_jobs=-1 ) scores = cross_val_score(rf, X_train, y_train, cv=5, scoring="r2") results.append((depth, scores.mean(), scores.std())) for depth, mean, std in results: print(f"max_depth={str(depth):4s}: R2={mean:.4f} ± {std:.4f}")输出的趋势一般是随着max_depth从 4 升到 8,R2 上升;到 10 或None(完全展开)时,训练集 R2 继续升但测试集 R2 开始下降。曲线的拐点就是最佳深度。n_estimators的收敛性判断类似:取 100、200、400、800,把每次的scores.mean()打点,看 400 之后是否还在明显增长,如果没有,就固定到 400 以下,避免无意义训练时间。
5. 避坑集中营:五个高频踩坑与排查思路
5.1 伪加密导致的「有密码但没加密」
现象:下载的 zip 在 7-Zip 里显示有锁,输入常见密码都失败,但文件是从正规课程平台下载的。
原因:分享者为防随手转发,用工具修改了加密标志位,文件实际没有加密,但常规解压工具会强制弹密码框。
解决:用 2.2 节的代码读flag_bits,确认加密位为 0 后,用 Pythonzipfile直接以无密方式解压。不要尝试爆破,更不要花钱去买「zip 密码移除工具」——真加密你搞不定,伪加密这招就够。
5.2 解压出来中文名乱码
现象:extractall 解压后,描述文件.txt变成乱码文件名,内容也打不开。
原因:zipfile在 Python 3.6 之前默认用 cp437 解码文件名,Windows 下生成的 zip(文件名编码 GBK)会被错误解码。Python 3.6+ 部分解决了 UTF-8 带标志位的情况,但对 GBK 旧包仍会翻车。
解决:先用raw.decode("gbk")手动转码再重命名文件。代码逻辑:解压到临时目录,遍历namelist(),逐一判断文件名是否为乱码,如果是就用name.encode("cp437").decode("gbk")修正,再os.rename。
5.3 数据集被命名为boston.csv,但列顺序不一样
现象:网上有人分享的boston.csv第一列是median_value,最后一列是crime_rate,跑完模型 R2 只有 0.3。
原因:该文件其实是自己生成的,列顺序打乱,或者把目标变量放在了第一列。初学者直接df["MEDV"]会取到犯罪率,分析全错。
解决:建模前先核对列名与 2.3 节的标准顺序。最可靠的做法是直接用pd.read_csv("housing.data", header=None, delim_whitespace=True)读原始.data文件,再按自己的列名列表赋值,绕过一切第三方转好的 CSV。
5.4 MEDV 截断值 50,导致高房价样本被系统性低估
现象:模型的残差在MEDV大于 45 的样本上全是负的,预测值最高死活到不了 45 以上。
原因:1980 年代采集数据时,房价超过 50k 的统一记 50,你在真实世界看到的 $50k 其实是多个不同价位样本的合并值,模型不可能学出 50 以上的分布。
解决:一种是直接删掉MEDV == 50的样本,再看模型表现;另一种是把MEDV做对数变换np.log1p(MEDV),压缩右尾。课程作业里第一种更常用,但做业务时要考虑截断机制,必要时改为分类模型不预测连续值。
5.5 交叉验证分数高但测试集低,单次划分骗人
现象:5 折交叉验证 R2=0.85,但随机抽一次test_size=0.2的测试集 R2=0.6。
原因:506 条样本太少,单次划分受随机性影响大。某一次测试集可能恰好抽到一批高LSTAT样本,模型没见过这种分布。
解决:把 4.4 节的cross_val_score当作主要评估,单次train_test_split只做参考。或者用KFold(n_splits=10, shuffle=True, random_state=42)自己构建多次划分,评估指标取 10 次均值和标准差。
6. 最后一公里的验证:残差分析、特征重要性与模型的业务解释
模型跑通只是第一步。把 R2 报出来谁都会,但能让这份数据集真正变成你自己方法论的是验证环节。残差分析是我的习惯性操作:预测值与真实值之差如果呈现喇叭形(方差随预测值增大而增大),说明模型在尾部分布上不够好;如果残差图左侧或右侧有明显的弯曲,说明漏了非线性项。
import matplotlib.pyplot as plt y_pred = rf.predict(X_test) residual = y_test - y_pred plt.figure(figsize=(8, 5)) plt.scatter(y_pred, residual, alpha=0.6) plt.axhline(0, color="red", linestyle="--") plt.xlabel("Predicted MEDV") plt.ylabel("Residual") plt.show()残差图的解读规则很简单:点均匀分布在零线两侧、没有明显结构,模型可信;如果出现 U 形或漏斗形,需要考虑对MEDV做对数变换或加入特征交互项。在我自己的项目里,波士顿房价数据残差图最常见的问题就是右上角一撮点全在零线以下——这是我前面 5.4 说过的截断导致的。
特征重要性方面,随机森林的feature_importances_可以直接输出。预期排序中LSTAT和RM一定排前两名,CHAS这种二分类变量通常排最后。但要注意该指标基于节点不纯度下降,存在高基数偏好,对连续特征有放大效应的嫌疑;更严谨的做法是permutation_importance,它把特征列打乱后观察模型误差变化,不受树结构偏好影响。
from sklearn.inspection import permutation_importance perm = permutation_importance( rf, X_test, y_test, n_repeats=30, random_state=42, scoring="r2" ) sorted_idx = perm.importances_mean.argsort()[::-1] for i in sorted_idx[:5]: print(f"{X_test.columns[i]:8s}: {perm.importances_mean[i]:.4f} ± {perm.importances_std[i]:.4f}")n_repeats=30是重复打乱次数,越大标准差越平滑,但单特征 30 次 × 5 折效率不高;小数据集可以减到 10 次。跑通这套流程之后你会发现,波士顿房价数据真正的价值不是让你拿一个好看的 R2 出去炫耀,而是让你在 506 条样本上把「数据质量 → 特征工程 → 模型选择 → 验证与解释」的完整链路建立起来。我自己的习惯是把这份基准代码保存成模板,换业务数据时只改data_path和feature_list,其余全部复用。数据小有小的好处,迭代快、变量可控,适合把每一步都做扎实。希望帮到你。
本文还有配套的精品资源,点击获取