简介:面向机器学习和数据科学入门学习者的波士顿房价数据集压缩包,适用于需要开展回归预测练习、特征工程或基础模型训练的开发者与学生。该数据集围绕经典房价预测场景展开,在压缩包内整合了数据说明、处理脚本和原始数据,便于快速搭建实验环境,节省查找与整理数据的时间。压缩包共包含3个文件,分别为Markdown说明文档、Python数据处理脚本和CSV格式原始数据集;说明文档用于描述字段含义与使用注意事项,Python脚本示范了从数据读取、特征查看、训练集拆分到简单回归建模的完整流程,CSV文件则提供包含多条区域样本记录和多个特征字段的规范数据,整体体积约15KB,结构轻量、上手门槛低。目前已有1361人学习或下载,具备较好的通用参考价值。借助该资源,读者可以完成从数据加载、特征观察到基础模型构建的闭环练习,既能用于课程作业和毕业设计实验,也能作为算法对比与数据分析报告的支撑素材。
1. 波士顿房价数据集.zip:一个套娃压缩包里藏着的回归入门样本
刚接触机器学习的开发者,十有八九都下过这个文件:波士顿房价数据集(波士顿房价数据集.zip).zip。名字看着像压缩包套压缩包,解压后里面还真是一个同名 zip。这套数据在 UCI、教学课件和各类网盘里流传了二十多年:506 条样本、14 个字段,13 个特征加一个目标列 MEDV,数据来自 1978 年波士顿地区的房屋普查。它的价值不在“新”,而在“小而全”:单机秒跑完线性回归、决策树和随机森林,特征含义清楚,适合用来搞懂回归任务的完整链路。如果你正在找一份能快速验证环境、练习特征工程或跑通模型流程的数据集,它比人工构造的 toy data 更真实,也比大规模业务数据更省心。不过动手前有个坑要先知道:sklearn 早就把load_boston()移除了,直接调 API 会报错。这篇笔记就从这个双层 zip 讲起,一路拆到建模、评估和踩坑。
2. 从双层 zip 到干净的 CSV:解压、编码与表头确认
2.1 为什么会出现“套娃 zip”这种结构
这套数据在中文技术社区里流传时,经常被二次打包。原发布者一般传一个波士顿房价数据集.zip,下载站或网盘转存时又在外面套一层同名压缩包,就成了标题里这种“(波士顿房价数据集.zip).zip”。其实里面就是一份 CSV,偶尔还会附带 README 或 PDF 说明。所以第一步别急着写代码,先把文件老老实实解压出来,确认里面到底有几个文件、多大、是不是 CSV。
在 Linux 或 macOS 终端里,我习惯先把外层解压:
unzip "波士顿房价数据集(波士顿房价数据集.zip).zip" ls -la file 波士顿房价数据集.zipfile命令用于确认内层文件真实类型。如果输出显示Zip archive data,说明确实还有一个 zip。接着解内层:
unzip "波士顿房价数据集.zip" ls -la解压后预期得到一个.csv文件,行数约 507 行(506 条样本加一行表头)。Windows 下如果双击解压遇到中文名乱码或路径过长报错,用 7-Zip 打开后选择“解压到当前文件夹”通常能绕过去;乱码多由文件名编码不一致引起,后面读 CSV 时再处理。我一般会顺手跑一下md5sum把文件指纹记下来,防止后续换机器时弄混版本。
2.2 先看表头再训练:列名、行数与编码的一次性确认
不要急着建模,先确认 CSV 的列名和行数。不同渠道流传的版本列名不完全一样:常见的有CRIM, ZN, INDUS, CHAS, NOX, RM, AGE, DIS, RAD, TAX, PTRATIO, B, LSTAT, MEDV,有些版本第一列是序号,还有些版本末尾多了一列文本DATA,这一列在pandas里会被读成 object 类型,直接用它算相关性会报错或算出一堆 NaN。
先用系统命令扫一眼,再进 Python 确认结构:
head -5 波士顿房价数据集.csv wc -l 波士顿房价数据集.csv如果表头里出现DATA或在head输出里看到整行数据被重复放在最后一列,说明是“带文本列的教学版”。接下用pandas读进来,先打印形状和列名,不急于建模:
import pandas as pd df = pd.read_csv("波士顿房价数据集.csv") print(df.shape) print(df.columns.tolist()) print(df.head())shape应显示(506, 14)或(506, 15)——多出来的一列往往是序号或DATA。如果df.head()里最后一列是一长串文本,那是把原始行数据原样复制了一份,需要裁掉。列名带首尾空格也很常见,比如"MEDV ",不清理的话df["MEDV"]会直接 KeyError。此时执行:
df.columns = df.columns.str.strip() df = df.loc[:, ~df.columns.duplicated()]第一句去掉列名首尾空白,第二句去掉重复列。读 CSV 时如果中文路径报编码错误,加encoding="gbk"或encoding="utf-8"试一遍。这一阶段的目标只有一个:让df.shape、df.dtypes和列名都符合预期,再进入预处理。
3. 把 506 条样本装进 DataFrame:13 个特征的含义与预处理
3.1 加载并强制数值化:去掉文本列,统一列名
这套数据最容易被忽略的坑是:CSV 版本里除了 14 个字段,经常混进一列文本DATA,导致df.info()里所有列都显示为 object。直接用df.corr()会得到一堆 NaN。按前面说的把列名清理干净之后,第二步是只保留我们需要的 14 列,并把它们全部转成数值类型。
cols = ["CRIM", "ZN", "INDUS", "CHAS", "NOX", "RM", "AGE", "DIS", "RAD", "TAX", "PTRATIO", "B", "LSTAT", "MEDV"] df = df[cols].copy() for c in cols: df[c] = pd.to_numeric(df[c], errors="coerce") print(df.dtypes) print(df.isna().sum())errors="coerce"的作用是:任何无法解析的字符串都变成 NaN,而不是让整列升格成 object。这样做的代价是引入了缺失值,所以紧接着要用isna().sum()看每一列的缺失数量。正常版本应该没有缺失;如果发现某列大面积 NaN,多半是表头对不上或该列原本就是文本列被误留了。此时有两种处理:一是删除该列,二是用df[c].fillna(df[c].median())填充中位数。506 条样本本身不大,缺一两个值用中位数填充可以接受;缺几十个,就要回去检查列名映射是否错位。
3.2 13 个特征的中文含义和三个必看边界
这个数据集里的特征来自 1978 年波士顿标准都市统计区的普查数据。列名是缩写,建模前最好把含义写出来,否则后面做特征筛选时只能靠猜。
| 列名 | 含义 | 典型范围 | 备注 |
|---|---|---|---|
| CRIM | 城镇人均犯罪率 | 0.006–89 | 右偏严重 |
| ZN | 占地面积超过 25000 平方英尺的住宅用地比例 | 0–100 | 大量 0 |
| INDUS | 城镇非零售商业用地比例 | 0.46–27.74 | |
| CHAS | 是否邻近查尔斯河 | 0 或 1 | 哑变量 |
| NOX | 一氧化氮浓度(每千万分之一) | 0.38–0.87 | 与 INDUS 相关性高 |
| RM | 平均每栋住宅的房间数 | 3.56–8.78 | 与 MEDV 强正相关 |
| AGE | 自住房屋中建于 1940 年前的比例 | 2.9–100 | |
| DIS | 到波士顿五个就业中心的加权距离 | 1.13–12.13 | |
| RAD | 高速公路可达性指数 | 1–24 | 是索引值,不是连续度量 |
| TAX | 每 10000 美元的不动产税率 | 188–711 | 与 RAD 高度相关 |
| PTRATIO | 城镇学生教师比 | 12.6–22.0 | |
| B | 修正后的黑人比例指数 | 0.32–396.9 | 教学常用,解读需谨慎 |
| LSTAT | 低地位人口百分比 | 1.98–37.97 | 与 MEDV 强负相关 |
| MEDV | 自住房屋房价中位数(千美元) | 5–50 | 目标列,上限被截断 |
三个必看边界:第一,CHAS 是哑变量,只有 0 和 1,不要对它做标准化;第二,RAD 虽然看起来是数值,实际是分组索引,1 到 24 不代表“辐射强度”,线性模型把它当连续变量用会引入假关联;第三,MEDV 被截断在 50 千美元,也就是说超过 50 的样本都被记成了 50,这让模型残差天然右偏,预测值逼近 50 时会压缩。还有一个争议点:B 列是“修正后的黑人比例指数”,来自原始论文,教学代码里沿用了这个缩写。如今用它做特征,建议概念上把它当作“人口构成比例”处理,同时在报告中注明数据年代,避免误读。
预处理上,我一般会做两件事:对线性模型用StandardScaler标准化全部数值特征(CHAS 除外),对树模型则不标准化。量纲差异在岭回归和线性回归里影响很大,TAX是几百、RM是个位数,不缩放会让梯度下降和正则化都偏向大数值列。标准化要在切分训练集和测试集之后做,只对训练集fit_transform,再对测试集transform,这一条放到第 5 章详细说。
4. 先跑线性回归再用交叉验证:最适合接手这套数据的第一步
4.1 切分训练集与测试集,跑一个最小线性回归
这套数据是回归任务的基准样本,第一个模型没必要上神经网络。先用线性回归建立基线,如果线性回归表现正常,后续再考虑决策树、随机森林或带正则化的岭回归。切分时固定random_state,保证结果可复现。
from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score import numpy as np 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 ) model = LinearRegression() model.fit(X_train, y_train) y_pred = model.predict(X_test) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) print("RMSE:", round(rmse, 3)) print("R2:", round(r2_score(y_test, y_pred), 3))test_size=0.2表示留出 101 条样本做测试;random_state=42不是玄学,是为了让多次运行的结果完全一致。RMSE的单位是千美元,跑出来的典型值在 4.5 到 6 之间;R2在 0.7 上下波动。如果 R2 低于 0.6,先检查预处理:有没有文本列混进 X,有没有对全量数据做标准化。此处的LinearRegression默认带截距,不需要手动加常数项,但特征列如果有单位矩阵问题,OLS 会直接报“singular matrix”警告,一般由完全线性相关的两列引起(如 RAD 与 TAX),后面会用岭回归处理。
4.2 用 10 折交叉验证看稳定性,别被一次切分骗了
单次切分的结果受随机种子影响很大。同样是random_state=42,换成random_state=0可能 R2 就掉了 0.05。因此我习惯紧接着跑交叉验证:把数据切成 10 份,轮流拿 9 份训练、1 份验证,最终得到 10 个 RMSE,看均值和方差。
from sklearn.model_selection import KFold, cross_val_score kf = KFold(n_splits=10, shuffle=True, random_state=42) neg_mse = cross_val_score( LinearRegression(), X, y, cv=kf, scoring="neg_mean_squared_error" ) cv_rmse = np.sqrt(-neg_mse) print("CV RMSE mean:", round(cv_rmse.mean(), 3)) print("CV RMSE std:", round(cv_rmse.std(), 3))cross_val_score默认返回负的均方误差,因为 sklearn 的评分函数遵循“越大越好”,所以取负号之后再开方得到 RMSE。对照组实现中,10 折 RMSE 的均值通常比单次切分的 RMSE 略高,这是数据量小导致的正常现象;如果 std 超过 1.5,说明模型对数据划分非常敏感,这时更适合用KFold(shuffle=True)的多次重复来平滑波动。n_splits在 506 条样本上用 5 或 10 均可,10 折每折只有 50 条验证样本,会稍微放大方差;5 折跑起来更快,但验证集更大、结果更稳。做基线验证时我一般先用 10 折,后续调参再切回 5 折省时间。
真正执行时还有一层常见问题:如果对全量 X 先做了标准化再切分,交叉验证就产生了信息泄露。正确做法是把标准化放进Pipeline,让每一折单独 fit 标准化器:
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler pipe = Pipeline([ ("scaler", StandardScaler()), ("lr", LinearRegression()) ]) neg_mse_pipe = cross_val_score( pipe, X, y, cv=kf, scoring="neg_mean_squared_error" )这样每一折只在本折的训练数据上计算均值和方差,测试数据不会被“偷看”。很多人在 Kaggle 新手阶段都犯过这个错:先scaler.fit(X)再train_test_split,结果是交叉验证分数虚高,上线就翻车。Pipeline 的写法把顺序锁死在流程里,是最稳妥的选择。
5. 避坑:加载、预处理和评估中最常见的 5 个翻车现场
5.1 现象:load_boston()直接抛 AttributeError
原因:scikit-learn 1.2 起移除了load_boston()数据集接口。网上大量 2021 年前的教程还在用from sklearn.datasets import load_boston,照抄必翻车。
解决:改用pandas.read_csv读取你解压出来的 CSV,这也是标题里这个 zip 存在的意义——数据文件远比 API 长寿。如果确实想体验从 sklearn 拉取同源数据,用fetch_openml("boston", parser="auto", as_frame=True),但注意 OpenML 版本的列名和 CSV 版不完全一致,接口参数也需要适配。别跟load_boston较劲,一条read_csv就绕过了整个坑。
5.2 现象:df.info()显示所有列都是 object,df.corr()输出全是 NaN
原因:CSV 版本里混入了文本列DATA,或表头与数据行之间有空行,导致 pandas 把整张表都推断为字符串。
解决:先按第 2 章的usecols或手动cols列表把 14 列选出来,再用pd.to_numeric(errors="coerce")强制转换。转换后立即执行df.isna().sum()排查缺失:如果某一列全 NaN,多半是列名映射错位(例如原表头末尾有看不见的\r),用df.columns.tolist()打印真实列名,手动修正后再转换。另一个有效手段是pd.read_csv(..., skip_blank_lines=True),它能自动忽略空行,避免表头后多一行空行导致类型推断失败。
5.3 现象:随机森林跑出来的 R2 反而比线性回归低
原因:506 条样本对随机森林来说太少,默认超参下每棵树都深到叶节点逼近个位数,训练集拟合得很夸张、测试集直接过拟合。线性回归只有 14 个系数,反而限制了方差。
解决:限制树模型复杂度。随机森林里调max_depth=4, min_samples_leaf=5,让模型学大趋势而不是背样本。用交叉验证对比默认参数和受限参数,你会发现受限后测试集 RMSE 明显下降。这个现象本身就是很好的课堂:数据集规模决定模型复杂度的上限,小数据上别迷信“越复杂越强”。
5.4 现象:交叉验证第一折就报Input X contains NaN
原因:预处理阶段没有处理缺失值,或CHAS列在某个版本里全是空字符串,pd.to_numeric(errors="coerce")把它们全部转成了 NaN。
解决:df.isna().sum()逐列确认缺失位置。对于 CHAS,正确的做法是检查df["CHAS"].value_counts():正常应该输出 0 和 1 两类,比例大约 9:1。如果发现该列只有 0 没有 1,那是流传版本丢失了少数样本,处理方式是直接删除这一列,而不是用中位数把它填成全是 0;如果缺失集中在某几列,用中位数填充即可。记住:先把缺失问题解决干净,再进交叉验证,否则每一折报错位置还不一样,看起来非常像“玄学”。
5.5 现象:预测出来的房价出现负数,或者全部挤在 50 附近
原因:MEDV 本身被截断在 50,线性模型在特征外推时很容易预测出 50 以上的值;某些极端特征组合(例如高犯罪率、高 TAX)会推出负房价。这不是模型“坏了”,而是线性模型没有边界约束。
解决:第一,查看预测值分布,如果大量堆在 50 附近,说明数据里超过 50 的真实值都被截成了 50,模型在学“封顶”。此时可以把目标列做对数变换,即对np.log1p(y)建模,评估时再用np.expm1还原;第二,改用带 L2 正则化的Ridge,系数被压缩后极端预测会变少;第三,如果业务上只关心相对排序而不是绝对值,可以用 RMSE 之外再算一个秩相关指标,避免被几个极端样本带偏评估结论。
6. 让它更可信:相关性排序、残差诊断与一次合格的模型验收
跑完基线模型后,先别急着调参。我会先看特征与目标的相关性排序,这一步能快速暴露数据版本问题:如果RM与MEDV的相关性不是最高之一,说明列名映射或预处理有错。执行:
corr = df.corr()["MEDV"].sort_values(ascending=False) print(corr)正常版本里,RM是最大的正相关(约 0.7),LSTAT是最大的负相关(约 -0.74)。这两个特征基本主导了模型预测走向。基于此做一次精简实验:只用RM、LSTAT、PTRATIO三个特征重新跑线性回归,R2 通常能保住全特征版本的八成以上。这既是特征选择练习,也是理解模型机制的最快路径——多特征版本很多系数只是互相抵消,不具解释价值。
然后是残差诊断。用测试集画出预测值与真实值的散点图,理想情况是点均匀落在y=x对角线两侧;如果散点呈喇叭状,即预测值越大残差越分散,说明模型存在异方差,常见对策是对目标列做对数变换。因为 MEDV 在 5 到 50 之间取值,低房价区间样本多且波动大,高房价区间被截断,残差图天然不会太干净。我习惯把预测值大于 48 的样本单独打印出来,看它们对应的RM、LSTAT原始值——这些往往是原始论文里被截断的“超 50 千美元”房屋,模型无法还原真实价格不是缺陷,而是数据本身的边界。
这套 506 条样本的数据集,虽然房价数字早已不符合当下波士顿市场,但回归建模的流程到今天一点没过时。我到现在接一个新环境、换一套新库,都会先拿它当“冒烟测试”:能在一个小时里跑通加载、预处理、建模、交叉验证、残差检查全流程,才敢把代码搬到大项目上。早年我也迷信过更大更新的数据集,后来发现小数据能把每个步骤的黑匣子都拆开看清楚。希望帮到你。
本文还有配套的精品资源,点击获取