波士顿房价数据集回归实战:解压、特征分析与建模指南
简介波士顿房价数据集是机器学习入门与回归任务中常用的经典数据集适合学生、数据分析初学者以及希望快速上手房价预测模型的开发者。压缩包体积仅15KB共包含3个文件一个CSV格式的原始数据文件、一个Python脚本以及一份Markdown说明文档其中CSV文件提供特征与目标值Python脚本可直接运行完成数据加载、划分与基础建模说明文档则帮助理解字段含义和使用流程。目前已有1361人学习下载能够帮助读者快速开展线性回归、决策树等模型对比实验节省自行整理与清洗数据的时间。从内容预览看压缩包内结构清晰核心代码与数据分离便于直接嵌入个人项目或教学演示。1. 波士顿房价数据集506 条样本凭什么当四十年回归基准第一次拿到「波士顿房价数据集.zip」的时候很多人会愣一下解压出来怎么里面还有一个同名 zip再解一层才看到 CSV 和说明文件。这个数据集是回归任务最经典的入门基准——506 条样本、13 个特征、一个连续目标 MEDV房价中位数从 1978 年采集至今被教材引用了四十多年。它的价值不在大而在小而干净你可以在几分钟内跑通数据读取、特征理解、模型训练、评估这一整条流水线又能把每一列特征掰开揉碎地讲清楚。它适合两类读者刚学 sklearn 想找第一个回归项目的初学者以及需要拿公认数据集做模型对比评估的从业者。下面从解压开始把数据格式、特征含义、基线模型和踩坑记录依次讲完。2. 解压波士顿房价数据集.zip先摸清双重压缩与文件结构这种「zip 套 zip」的题名在课程课件里很常见通常是发布者把原始数据和处理好的 CSV 分开打包再套一层壳。和 mysql 的 zip 安装包类似解压之后还得再解一层路径对不上就找不到文件。所以第一个动作不是急着读数据而是把两层都解开看清楚里面到底有什么。2.1 双重 zip 解压bash 一行解两层Python 应对乱码文件名命令行最简单。注意文件名里有中文和全角括号unzip 参数一定要用引号包住否则 shell 会把文件名拆成多个参数mkdir -p boston unzip 波士顿房价数据集波士顿房价数据集.zip.zip -d boston unzip boston/波士顿房价数据集.zip -d boston/data ls -la boston/data说明第一个 unzip 解出的是内层 zip第二个 unzip 才解出真正的数据文件。-d 指定输出目录避免文件散落在当前目录。如果 ls 之后看到 _MACOSX 目录或一堆 .开头的文件说明这个包是在 Mac 上打的可以这样排除unzip boston/波士顿房价数据集.zip -d boston/data -x __MACOSX/* ._*说明-x 后面跟要排除的通配模式把 Apple 的元数据垃圾挡在门外。反过来如果你想把解压后的目录重新打成 zip 分发常见做法是zip -r boston.zip boston/data这个命令递归压缩整个目录别人拿到后不会散一地文件。Windows 上没有 unzip 命令时用 Python 更稳还能顺手检查文件名编码import zipfile from pathlib import Path outer_zip Path(波士顿房价数据集波士顿房价数据集.zip.zip) out_dir Path(boston) out_dir.mkdir(exist_okTrue) with zipfile.ZipFile(outer_zip) as zf: for info in zf.infolist(): print(repr(info.filename)) zf.extractall(out_dir)说明extractall 之前先打印 namelist() 或 infolist()这一步能省掉很多后续麻烦——内层 zip 不一定叫「波士顿房价数据集.zip」可能带一长串乱码或多余前缀。repr() 会把不可见字符也显示出来方便你判断真实文件名。确认清单后把第二个 zip 用同样的方式再解一层。提示文件名里有空格、中文、括号时无论 bash 还是 Python 都要显式处理。bash 加引号Python 里尽量用 pathlib.Path 而不是手拼字符串路径。2.2 pandas 第一轮体检shape、info、describe 分别看什么解压完别急着训练先读进来做三轮体检import pandas as pd df pd.read_csv(boston/data/housing.csv) print(df.shape) # (506, 14) print(df.info()) print(df.describe().T) print(df.head())说明shape 确认规格506 行 14 列是波士顿房价的标准尺寸多一行少一行都要回头检查文件完整性。info() 看每列类型和缺失值——这个数据集通常没有 NaN但你拿到的版本不一定先df.isna().sum()看分布再决定怎么处理别上来就 dropna。describe().T 转置后重点看两处CHAS 的 min/max 是不是 0/1MEDV 的 max 是不是卡在 50.0。这两处分别对应哑变量和截断值问题后面都会讲到。如果你拿到的不是 CSV 而是 UCI 原始格式 housing.data读取方式完全不同column_names [ CRIM, ZN, INDUS, CHAS, NOX, RM, AGE, DIS, RAD, TAX, PTRATIO, B, LSTAT, MEDV ] df pd.read_csv( boston/data/housing.data, delim_whitespaceTrue, headerNone, namescolumn_names, ) print(df.head())说明housing.data 是空格分隔、无表头的原始文件delim_whitespaceTrue 等价于 sep\s多个连续空格当一个分隔符。headerNone 告诉 pandas 第一行不是列名names 手动指定。这里最容易翻车的是列名顺序——必须和 housing.names 文件列出的顺序一致错一位整列数据就对不上特征。我的习惯是单独维护一个 COLUMNS 列表不要手敲在 read_csv 里后面做特征选择还要复用。2.3 文件清单核对housing.data、housing.names、housing.csv 各自的作用解压后你会见到几种常见文件来历不同用途也不同文件格式内容备注housing.dataUCI 原始数据空格分隔506 行 × 14 列数值无表头老教材用的格式housing.names纯文本特征含义、单位、采集说明列名顺序的唯一权威来源housing.csvCSV 带表头整理后的表格通常就是 data 文件加了表头新教程推荐用这个其他 zip压缩包某些发行版把上述文件再套一层有的甚至套三层说明不同来源的波士顿房价数据集 zip 内容略有差异但核心就两类——数据文件.data 或 .csv和说明文件.names。如果只有 .data 没有 .names去 UCI Machine Learning Repository 查波士顿房价页面按官网顺序核对列名。我见过不止一个人拿 .data 直接 read_csv 然后报列数不对其实就是没处理无表头和无分隔符。记住.names 是数据字典不是给人读的装饰文件。3. 13 个特征逐列拆解哪些影响房价哪些藏着坑特征理解决定了后续所有建模决策。波士顿房价的 13 个特征量纲、类型、分布差异极大先做相关性分析定位主要矛盾再逐个处理离散特征和目标变量。3.1 先算相关性矩阵哪几个特征和 MEDV 强相关import matplotlib.pyplot as plt import seaborn as sns corr df.corr() print(corr[MEDV].sort_values(ascendingFalse)) plt.figure(figsize(10, 8)) sns.heatmap(corr, annotTrue, fmt.2f, cmapcoolwarm) plt.tight_layout() plt.savefig(boston_corr.png, dpi150)说明df.corr() 计算 Pearson 相关系数对全数值表格可以直接用。看 MEDV 那一行LSTAT-0.74和 RM0.70是两个最强的特征一个代表社区社会经济水平一个代表房屋本身大小正好对应房价解释里的「location, location, location」和「房子大小」。CHAS 只有 0.18和房价关系最弱。热力图里还能看到特征之间的相关RAD 和 TAX 相关系数高达 0.91这是共线性的典型信号线性回归的系数会在这两个特征之间互相拉扯。3.2 CHAS、RAD、PTRATIO离散与有序特征不能一刀切特征类型含义与 MEDV 相关性处理建议CRIM连续城镇人均犯罪率-0.39直接用可考虑 logZN连续占地超 2.5 万平方英尺住宅比例0.36直接用INDUS连续非零售商业用地比例-0.48直接用CHAS离散 0/1是否邻近查尔斯河0.18哑变量直接进模型NOX连续氮氧化物浓度-0.43直接用RM连续平均房间数0.70最重要的正相关AGE连续1940 年前建成自住房比例-0.38直接用DIS连续到波士顿就业中心加权距离0.25直接用RAD有序离散到高速公路可达性指数-0.38可转哑变量TAX连续每万美元房产税率-0.47与 RAD 强相关PTRATIO连续师生比-0.51直接用B混合历史人口统计比例0.33建议丢弃LSTAT连续低社会地位人口比例-0.74最重要的负相关MEDV目标房价中位数千美元1.00截断在 50CHAS 是 0/1 哑变量线性回归直接放进去没问题系数可以解释为「临河相对于不临河的平均房价差」。比较特殊的是 RAD它虽然取值是 1 到 24 的数字但实际只有少数几个离散值1、2、3、4、5、24 等本质是有序分类。直接当连续特征用也可以但要清楚线性模型给它的是一条直线斜率而 RAD 对房价的影响不是线性的。另一个办法是把 RAD 转成哑变量丢给模型代价是自由度变大506 条样本还撑得住。PTRATIO 是师生比分布集中在 12 到 22与房价负相关解释很直观教育资源紧张的区域房价偏低。这三个特征的处理方式体现了特征工程的常见取舍——连续特征直接用、0/1 哑变量直接用、有序特征要斟酌线性假设是否成立。3.3 MEDV 的两个事实截断在 50 和要不要取对数import numpy as np print(df[MEDV].describe()) print(MEDV 50 的样本数:, (df[MEDV] 50.0).sum()) print(MEDV 最小值:, df[MEDV].min()) df[MEDV_log] np.log1p(df[MEDV])说明MEDV 最大值 50.0 不是巧合是 1978 年采集时把超过 50千美元的房价统一记为 50这叫截断censoring。这个数据集里大约 16 条样本卡在这个上限。截断对回归的影响是系统性的模型学不到 50 以上的分布所有真实值大于等于 50 的样本残差都为正。np.log1p 对目标取 log(x1)0 值也安全。取不取对数没有标准答案如果你的模型在个别高房价样本上误差巨大log 目标能压低它们在损失函数里的权重但评估时记得 np.expm1 还原否则 RMSE 的单位变成 log(千美元)没法向业务解释。3.4 B 特征与历史包袱哪些列建议直接丢弃B 列的公式在 UCI 说明里写得很清楚B 1000(Bk - 0.63)²其中 Bk 是 1978 年采集时统计的镇内黑人居民比例。这个特征在今天有两个问题一是计算方式本身难以向业务解释平方项让系数的含义变得很绕二是其采集背景涉及敏感的人口属性主流课程已经不建议使用。scikit-learn 移除 load_boston 时官方说明也明确提到了这一点。所以对这个特征我的处理习惯很直接df df.drop(columns[B])。它对 MEDV 的相关性只有 0.33 左右丢掉对模型影响很小却能让实验少一个解释不清的变量。类似地如果你拿到的是带年份或带邮编的衍生版本先确认这些列是不是数据泄漏源再决定保留还是丢弃。4. 跑通最小回归基线线性回归与决策树的参数怎么设理解了特征之后用最小代码把基线跑出来。这一章的目标不是冲指标而是建立一套可复现的流程固定的划分方式、正确的标准化顺序、可解释的评估指标。4.1 train_test_split 参数test_size、random_state 怎么定from sklearn.model_selection import train_test_split X df.drop(columns[MEDV, MEDV_log]) y df[MEDV] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) print(X_train.shape, X_test.shape) # (404, 13) (102, 13)说明回归任务不需要 stratify 参数那是分类里按类别比例分层用的。test_size0.2 在 506 条样本上划分出 404 条训练、102 条测试。random_state42 必须固定——不是因为它有魔法而是为了实验可复现你调参后想比较两次结果划分不一样就没有可比性。但也要清醒固定 random_state 只是复现手段不是消除样本划分方差的手段单次 holdout 在 506 条样本上本身就不可靠后面交叉验证会修正这一点。4.2 标准化顺序先 fit 训练集再 transform 测试集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 再划分——测试集的信息就漏进训练过程这叫数据泄漏。线性回归不标准化也能跑但特征量纲差异大时系数解释很别扭如果后面换 Ridge 或 Lasso标准化是硬性要求因为正则项惩罚的是系数绝对值量纲大的特征系数天然偏小会被正则项误伤。4.3 线性回归基线最小代码和 RMSE 的量纲from sklearn.linear_model import LinearRegression from sklearn.metrics import root_mean_squared_error, r2_score model LinearRegression() model.fit(X_train_scaled, y_train) y_pred model.predict(X_test_scaled) rmse root_mean_squared_error(y_test, y_pred) print(fRMSE {rmse:.3f} (千美元)) print(fR2 {r2_score(y_test, y_pred):.3f})提示root_mean_squared_error 是 scikit-learn 1.4 引入的。老版本用 mean_squared_error(y_test, y_pred, squaredFalse)效果相同。很多人在这步看错量纲——squaredTrue 返回的是 MSE开根号才是 RMSE。说明RMSE 单位是千美元4.8 意味着平均误差约 4800 美元。这个基线在波士顿数据集上通常落在 4.5 到 5.5 之间R² 在 0.7 到 0.8。如果你跑出来差很多先确认 y 是 MEDV 而不是别的列再检查数据读入时有没有丢行。想看特征贡献度标准化后的线性回归系数可以直接比coefs pd.Series(model.coef_, indexX.columns) print(coefs.sort_values(keyabs, ascendingFalse))说明因为输入已经标准化系数绝对值大致反映特征对房价的影响力。通常 RM 和 LSTAT 的系数最大和相关性分析一致。这一步能帮你发现异常如果某个理论上不相关的特征系数巨大多半是共线性在作祟。4.4 决策树回归max_depth 与 min_samples_leaf 的调参实验决策树不需要标准化直接用原始特征from sklearn.tree import DecisionTreeRegressor for depth in [3, 5, 8, None]: for min_samples_leaf in [1, 5, 10]: dt DecisionTreeRegressor( max_depthdepth, min_samples_leafmin_samples_leaf, random_state42, ) dt.fit(X_train, y_train) rmse root_mean_squared_error(y_test, dt.predict(X_test)) print(fdepth{str(depth):4}, leaf{min_samples_leaf:2}, RMSE{rmse:.3f})说明两个关键参数max_depth 控制树的最大层数None 表示不限制——在 506 条样本上不限制深度会让树长到记住每个训练样本训练 RMSE 接近 0测试 RMSE 反而最高这就是过拟合。min_samples_leaf 控制叶子节点的最少样本数是比 max_depth 更有效的正则手段一般至少设 5。这种双重循环的穷举在小数据上秒出结果但要小心网格搜索选出的最优参数只是在这个测试集上最好不代表泛化最好最终要用交叉验证确认。以我的实验经验depth3、leaf10 附近通常能拿到和线性回归接近甚至略好的测试 RMSE而默认参数depthNone, leaf1几乎必然翻车。这个小实验本身就有教学价值让你亲眼看到过拟合长什么样。5. 波士顿房价数据集常用排查5 个亲测踩坑记录这一章全是实际操作中反复遇到的问题每一条都按现象、原因、解决的顺序写。5.1 坑一fetch_boston() 在 scikit-learn 1.2 之后直接报错现象照着老教材写from sklearn.datasets import load_boston运行报 AttributeError说模块里没有 load_boston。原因scikit-learn 1.0 把 load_boston 标记为弃用1.2 直接移除。移除原因官方说明写得很清楚数据集包含有争议的人口统计特征且与当代住房市场严重脱节。这不是你的环境坏了是它被删了。解决不要为了迁就旧代码去降级 sklearn直接把读取部分换成 CSV 方式import pandas as pd # 旧写法已失效 # from sklearn.datasets import load_boston # X, y load_boston(return_X_yTrue) # 新写法自己读 CSV再拆分 X 和 y df pd.read_csv(boston/data/housing.csv) X df.drop(columns[MEDV]) y df[MEDV]说明替换后X 和 y 的类型、形状和旧接口基本一致后面的建模代码不用动。这也是为什么我前面强调要保留一份 CSV——zip 里的原始文件就是你应对 sklearn 接口变动的最好后悔药。5.2 坑二双重 zip 解压后文件名乱码__MACOSX 垃圾文件干扰现象解压后出现一堆乱码文件名或者多出 _MACOSX 目录和 .开头的文件脚本按预期路径读不到 CSV。原因zip 规范没有强制统一文件名编码。Windows 上打包常见 GBKMac 上常见 UTF-8 加 AppleDouble 元数据Python 的 zipfile 模块默认按 CP437 解码文件名中文名就变成了乱码。解决先打印真实文件名再做编码修正import zipfile with zipfile.ZipFile(boston/波士顿房价数据集.zip) as zf: for info in zf.infolist(): # 常见修正cp437 - utf-8如果原编码是 GBK把 utf-8 换成 gbk fixed info.filename.encode(cp437).decode(utf-8, errorsignore) print(fixed)说明errorsignore 防止个别文件名解码失败时整个脚本抛异常。确认修正后的文件名后再决定是手动重命名还是直接按后缀匹配文件。__MACOSX 目录不影响数据读取但有洁癖就用unzip -x排除或者解压后直接删除该目录。5.3 坑三标准化放在划分之前测试集信息泄漏现象训练集和测试集 RMSE 都很低模型看起来完美可一旦接到新数据预测误差明显变大。原因先对整个数据集做 fit_transform再切训练/测试。scaler 的均值和标准差肉眼可见地用上了测试集数据这部分信息通过缩放参数泄漏进训练过程。这是回归任务里最常见的隐性泄漏比特征选择泄漏还隐蔽。解决把 StandardScaler 和模型一起放进 Pipelinefrom sklearn.pipeline import make_pipeline from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler pipe make_pipeline(StandardScaler(), LinearRegression()) pipe.fit(X_train, y_train)说明pipe.fit(X_train, y_train) 时 StandardScaler 只在训练集上拟合pipe.predict(X_test) 时用同一套参数变换测试集。如果你习惯手写缩放记住一条铁律任何预处理器的 fit 都只能看到训练集。交叉验证阶段这条铁律同样成立所以推荐把管道传给 cross_val_score。5.4 坑四MEDV 截断在 50残差长尾拉高 RMSE现象残差直方图右侧拖着一条长尾打印(y_test 50).sum()发现这些样本的预测全部偏低。原因MEDV 采集时被截断在 50超过 50 的真实房价没有被记录。模型学到的最优策略是「预测值不超过 50」因此真实值在 50 附近的样本残差系统性为正。这是数据结构性缺陷不是模型参数没调好。解决先量化截断样本数print((df[MEDV] 50.0).sum()) # 大约 16 条如果业务上高端样本重要做两阶段建模先训练一个分类器判断「是否大于等于 50」再对小于 50 的子集训练回归器。如果只是学回归流程报告 RMSE 时同时报一个去掉截断样本后的 RMSE并写明差异来源。5.5 坑五单次划分不稳定换个 random_state 结果大变现象同一个模型把 random_state 从 42 改成 2024测试 RMSE 从 4.8 变成 5.7你开始怀疑自己的代码。原因506 条样本切成 102 条测试集划分的运气成分很大。小数据集上单次 holdout 的方差高是常态不是 bug。解决不要只报单次划分的 RMSE。至少跑 3 到 5 个 random_state报告均值和波动范围更规范的做法是交叉验证下一章详细讲。记住挑最好看的那次 random_state 写进报告属于学术不端哪怕只是给自己的实验记录也不行。6. 交叉验证与模型持久化把波士顿房价做成你的回归基线6.1 KFold 交叉验证把 RMSE 报告成「均值 ± 标准差」from sklearn.model_selection import KFold, cross_val_score from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression X df.drop(columns[MEDV]) y df[MEDV] pipe make_pipeline(StandardScaler(), LinearRegression()) kfold KFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(pipe, X, y, cvkfold, scoringneg_root_mean_squared_error) print(fRMSE {-scores.mean():.3f} ± {scores.std():.3f} (千美元))说明cross_val_score 的 scoring 返回负 RMSE因为 sklearn 约定分数越大越好所以打印时取负。KFold 的 n_splits5 把数据分成 5 份每轮 4 份训练 1 份验证。shuffleTrue 很重要避免原始顺序里的系统偏差。整个管道传进 cross_val_score 后每一折内部会重新 fit scaler 和模型这是没有泄漏的评估。对比单次划分你会发现均值接近但标准差小很多这才是能写进报告的数字。6.2 用 joblib 保存基线下次不用重新训练import joblib joblib.dump(pipe, boston_baseline.joblib) # 新环境里加载 loaded_pipe joblib.load(boston_baseline.joblib) print(loaded_pipe.predict(X_test.iloc[:3]))说明joblib 比 pickle 更适合 sklearn 管道能正确处理内部数组和大对象。保存时把拟合好的整个管道存下来下次预测不用重新训练。文件名带上日期或版本比如 boston_baseline_20250601.joblib避免覆盖历史实验。我自己现在的习惯是拿到任何一个新数据集第一件事永远是跑一个最小基线——线性回归加 5 折交叉验证把 RMSE、训练耗时、特征数记在一张表里然后再谈随机森林、XGBoost、神经网络。波士顿房价数据集 506 条样本这套流程五分钟出结果非常适合练这个习惯。它另一个用途是当「数据集标尺」你在波士顿上验证过的预处理和评估流程换到加州房价或其他回归数据时只需要替换读取部分后面的管道几乎不用改。还有一点血泪经验小数据集上不要迷信复杂模型。我见过有人在这个数据集上套十几层神经网络最后效果没比线性回归好多少却多花了无数调参时间。先用简单模型把评估流程跑通再逐步加复杂度是这个数据集教会我最实用的一课。希望帮到你。本文还有配套的精品资源点击获取