☰
Kaggle房价预测:数据预处理与特征工程完整实战指南
2026/10/21 3:15:45 网站建设 项目流程

说实话,房价预测(House Prices)是我见过最适合入门 Kaggle 的题目,没有之一。它的数据量不大,表格结构清晰,回归目标明确,但它的坑一点都不少,尤其是数据预处理部分,几乎把真实项目中会遇到的基础问题都囊括了一遍,比如缺失值、异常值、偏态分布、类别编码、特征构造。这篇我准备只讲预处理这一段,因为这一段的决策会直接影响你后面模型的上限。我不打算堆概念,我会直接把我实际跑通的这套流程和每一步的判断逻辑,按原样拆给你看。

1. 动手之前:先弄清楚这题到底考什么

1.1 这份数据长什么样

这个比赛给的是房价预测的数据集,训练集大概一千四百多条样本,属性列有接近八十个,测试集也差不多这个规模。特征涵盖的维度很杂:有地段分区、房屋面积、建造年份、地下室情况、车库情况、装修质量评级、销售条件等等。目标变量就是 SalePrice,也就是销售价格。

我拿到数据的第一反应是:这不像工业界的真实数据,更像一个把真实情况做了系统化整理的样例,每个特征都带语义,很多特征之间还有强关联,比如总面积和地面居住面积、卫生间数量和房间数量之间明显相互影响。这正是适合练习特征工程的地方,因为你可以根据常识去构造特征,而不是盲目地在黑盒里拼特征。

从建模角度看,这是一个标准的回归问题,评估指标是 RMSLE,就是均方根对数误差。这个指标的意思是,预测结果和真实值都先取对数再算误差,这样一来,小误差会被放大,大误差会被压缩,模型会更关注那些价格不太高的房子的相对准确度。这个细节直接决定了我们后面处理目标变量的方式,不是简单做个标准化就可以。

1.2 预处理的目标不是“填满数据”

很多人第一次做数据预处理的时候,容易陷入一个误区,觉得预处理就是把空值填上、把字符变成数字,完事。但真正决定预处理质量的,是你对业务语义的理解。

举个例子,有个特征叫 PoolQC,代表泳池质量。这个字段在网上有一千四百多条样本里只有两三条有值,其余全是空的。你会怎么填?如果你盲目用众数去填,那等于告诉模型“绝大部分房子都有泳池”,这是灾难性的错误。实际上,这个字段的缺失应该理解成“没有泳池”,所以正确的填充值是 None,而不是某个数值。

类似的情况在数据里非常多,比如 Alley 是巷子类型,缺失就表示没有巷子;Fence 是栅栏类型,缺失就表示没有栅栏。这就是为什么我说,预处理的第一原则是:每一个缺失值都要问一句“它为什么缺失”,而不是直接调用一个填充函数。

另一个容易忽视的问题是分布形态。很多特征不是正态分布,右偏严重,比如 LotArea 地块面积、GrLivArea 地面居住面积。如果你不做处理,直接丢给线性模型,这些超大的值会像杠杆一样,把模型的结果撬歪。所以我们需要做对数变换或者 Box-Cox 变换,把这部分非对称性拉回来。

总结一下,我理解的预处理目标有四个:

  • 把缺失值按照业务语义变成有意义的取值;
  • 把异常值识别出来,避免极端个案主导模型;
  • 把偏态分布拉向正态,或者至少让模型更好处理;
  • 把类别变量变成模型能消化的数值,同时尽量保留原有的顺序和层级信息。

2. 数据初探:第一眼该看什么

2.1 读取与合并:训练集和测试集必须一起处理

我通常会先把训练集和测试集读进来,然后用一个 key 字段把它们纵向合并成一个全集。这样做有两个好处:第一,特征的编码和填充逻辑只写一遍,两边不会出现不一致;第二,测试集里的信息也能参与填充统计量的计算,比如众数、中位数这些,这在业界属于合理做法,因为我用的是无标签的分布信息。

这里要特别提醒:合并前一定要先把标签列 SalePrice 摘出来存好,千万不能和特征一起填充。我曾经见过有人把整张表丢进填充函数,然后发现 SalePrice 也被“优化”了,后面模型结果彻底没法看。

处理逻辑大致是这样一个开头:

import pandas as pd import numpy as np train = pd.read_csv('train.csv') test = pd.read_csv('test.csv') y = train['SalePrice'] all_data = pd.concat([train.drop('SalePrice', axis=1), test], axis=0, ignore_index=True)

注意,这里的 ignore_index=True 很重要,它能保证后面做 One-Hot 编码或者索引对齐的时候不会因为行号重复而混乱。

2.2 特征大盘点:按数据类型分三路处理

拿到 combined 之后,我不会急着填充,我的习惯是先按特征类型分三堆:数值型连续特征、数值型离散特征、类别特征。

类别特征里面还要再细分为两种:有顺序的和没顺序的。有顺序的比如 OverallQual,这是综合评分,数值从 1 到 10,10 分是顶级豪宅质量,你说它能不能直接当数值用?可以,但更好的做法是保留它的顺序信息,转换成等价的数值映射,而不是拆成哑变量丢掉顺序。没顺序的比如 MSZoning,这是区域划分,A、B、C 之间没有任何谁高谁低的关系,这种就适合 One-Hot 编码。

连续特征和离散特征我就直接看 describe() 了,重点关注标准差、最大值和分位数。一个直觉上的判断是:如果某个特征的标准差非常大,或者最大值远远超过 75% 分位数,那它大概率是重尾分布或者有异常值,需要进一步处理。

我会把特征结构列一下,因为后面每一步都要用到这个清单:

类型特征示例处理方式
连续数值GrLivArea, LotArea, TotalBsmtSF缺失值填充 + 偏度处理 + 标准化
离散数值YearBuilt, OverallQual, OverallCond保留顺序,缺失值填充
顺序类别ExterQual, KitchenQual, HeatingQC映射为 1~5 的数值
名义类别MSZoning, Neighborhood, SaleTypeOne-Hot 编码

2.3 目标变量 SalePrice 为什么要做对数变换

前面提到评估指标是 RMSLE,这意味着我们天然应该对 SalePrice 取对数,让模型的优化目标和评估指标保持一致。另外,从分布图上看,SalePrice 是明显右偏的,房子越贵,样本越少,长尾巴拖在右边,如果不做对数变换,模型会对高价格样本过于敏感,损失函数会被极少数千万级豪宅主导。

我处理的代码如下:

y_log = np.log1p(y)

这里的 log1p 是 log(x+1),目的就是为了防止 x=0 的情况出现,虽然在房价这个场景里不会出现 0,但这是个好习惯。训练结束后,如果你要提交预测结果,记得再走一遍逆变换np.expm1(pred),把预测值还原成真实价格,不然你的预测值是对数尺度,Kaggle 会让你得到一份完全不对的结果。

3. 缺失值处理:这块最考验对业务的理解

3.1 按缺失率给特征分梯队

我统计了一遍缺失情况,发现缺失率差异极大,从几乎全空到只有零星几个空值都有。针对不同的缺失率,处理策略完全不同。

先把缺失率列表打出来:

missing_ratio = all_data.isnull().mean().sort_values(ascending=False) missing_ratio = missing_ratio[missing_ratio > 0]

我的梯队划分标准是这样的:

  • 缺失率大于 50% 的特征:基本可以断定是“有就是有,没有就是没有”的语义,大概率要填 None;
  • 缺失率在 10%~50% 之间:要结合业务语义具体分析,可能是条件缺失,也可能是统计缺失;
  • 缺失率小于 5% 的:通常对整体影响有限,可以用众数或中位数兜底。

3.2 高缺失率字段:多数情况下“缺失”本身就是一种取值

我们先看最极端的一批:PoolQC、MiscFeature、Alley、Fence、FireplaceQu。这几个字段的含义分别是泳池质量、杂项特征、巷子类型、围栏类型、壁炉质量。你想想,普通住宅有几个有泳池?有几个有巷子?数据缺失完全正常,因为那些房子压根没有这些配套设施。

所以我给它们的处理方式是:类别型缺失全部填成字符串'None',表示“没有这个物件”。这比填 0 要好,因为填 0 在某些树模型里会被解释成有意义的数值大小,填'None'则是一个独立类别,模型自己会去判断这个类别和目标变量的关系。

我也试过把这些特征直接删除,结果分数量化出来反而掉了,说明这些“有没有”的信息确实是有预测力的,毕竟泳池和壁炉这种东西直接跟房价挂钩。

3.3 中低缺失率字段:逐字段判断才是正路

缺失率在中等水平的特征,比如 LotFrontage 街面宽度,约 17% 缺失;GarageYrBlt 车库建造年份、MasVnrArea 砖石面积,也有不少空值;还有一部分是地下室相关特征,比如 BsmtQual、BsmtCond 之类。

这些字段就需要逐个分析语义了。

以 LotFrontage 为例,它是房子临街面的宽度。数据里有不少空值,但你对门牌号一想就知道,每条街的面宽是有相似分布规律的,所以这里用中位数填充是合理的。我还特意按 Neighborhood 分组去填,因为不同地段的街宽差异挺大的,全局中位数不如分组中位数精细。

GarageYrBlt 的处理就更有意思了,它的缺失通常意味着这栋房子没有车库。那我直接先建一个 HasGarage 标志位,然后把 GarageYrBlt 缺失的部分填成一个合理的值。但这又引出另一个问题:如果直接填充一个年份,模型可能会认为所有缺失车库的房子都是那一年盖的,产生误导。更合理的做法是把 GarageYrBlt 这种连续变量在缺失时填 0,同时配套 HaveGarage 这个 0/1 标志来对冲。

地下室特征 BsmtQual、BsmtCond 也是同理,这些是类别字段,缺失代表“无地下室”,所以我填'None'字符串。

3.4 为什么不能一个 fillna 走天下

有人会用这样的代码图省事:

all_data = all_data.fillna(all_data.median())

这种写法放在数据竞赛里基本能帮你拿到一个保底分数,但绝对拿不到高分。原因很简单:它会把所有缺失情况强行拉成同一个统计值,忽略了“缺失”这个状态本身可能携带的信息。举例说,MasVnrArea 缺失了,可能意味着外墙没有砖石贴面,这种房子往往价格偏低,如果你用中位数去填,模型就识别不到“贴面缺失”这个信号了。

正确做法是把所有特征分成几类,每类各用各的策略再批量执行。虽然代码看起来长了点,但它有两个好处:第一,可复现;第二,每个字段的填充理由随时能追溯,这对于后面调模型、跟别人交流特征思路都很重要。

4. 异常值与分布修复:给数据“祛痘”和“正骨”

4.1 房价预测里那几个必须清掉的极端点

很多人忽略异常值,但这题里有个出了名的个案。训练集里有一套房,地面居住面积 GrLivArea 超过四千平方英尺,但 SalePrice 却连三十万都不到。你想想,一个又大又便宜的房子,是不是哪里不对劲?要么是数据录入错误,要么是特殊产权情况,总之它不是正常市场的代表样本。这种点如果留在训练集里,线性模型会被它强行拽出一条不合理的斜率,树模型也会专门为它分出一个奇葩的叶子节点,纯属浪费。

我的处理方法是用散点图先定位,然后用 IQR 或者简单阈值把这类样本挑出来,看它们是否符合常识。如果不符合,稳妥起见就删掉。

train = train.drop(train[(train['GrLivArea'] > 4000) & (train['SalePrice'] < 300000)].index)

这里有个原则需要强调:异常值的判断不能只看单变量分布,一定要看多变量关系。GrLivArea 大本身不是异常,但 GrLivArea 大加 SalePrice 异常低才是异常组合。所以做异常值剔除前,先画图、再验证、最后动手,顺序别乱。

4.2 数值特征的偏度与 Box-Cox/log1p 变换

数值特征分布不均的问题,比缺失值更隐蔽。我用偏度系数扫了一遍,结果相当吓人:有些特征偏度超过 5,说明右尾极重,样本大多集中在小数值区,但尾巴上挂着几个巨值。

处理策略是:对偏度大于 0.75 的正偏特征做 log1p 变换。记住,这个操作有个前提条件,就是特征值必须非负,而且最好没有太多 0,否则变换后 0 值会被压到同一个点上,造成信息丢失。对偏度特别严重的,我更推荐 Box-Cox 变换,它的优势在于可以自动寻找最优的变换参数,效果通常优于固定的 log。

简单记录一下当时的处理片段:

from scipy import stats skewness = all_data[numeric_feats].apply(lambda x: stats.skew(x)) skew_features = skewness[skewness > 0.75].index for feat in skew_features: all_data[feat] = np.log1p(all_data[feat])

要注意的是,Box-Cox 变换要求数据严格为正,如果特征里有 0 值,可以先加一个很小的偏移,或者直接退回到 log1p。我个人的建议是优先 log1p,够用且稳定。

4.3 标准化与归一化:两个容易搞混的概念

做完偏度修复后,数据主体已经比较圆润了。接下来才是规模问题。不同特征的量纲差异很大,有的特征在几千量级,有的在个位数。这对线性模型和神经网络很不友好,因为梯度下降的步长受量纲影响,大数值特征会主导权重更新的方向。

常见的做法有两种:StandardScaler 标准化为均值为 0、方差为 1,以及 MinMaxScaler 归一化到 0-1 区间。我倾向 StandardScaler,因为它能保留异常值和处理后的分布形态,而且对后续的正则化项更友好。

但我要提醒一句:对于树模型来说,标准化基本没有意义,因为树的切分不依赖距离计算。所以这一步做不做,取决于你后面打算用什么模型。如果你准备后续做模型融合,我会建议做标准化,反正对树模型无害,对线性部分有益,一箭双雕。

5. 特征工程实操:这步最容易拉开差距

5.1 有序类别特征怎么映射数值

回到前面提到的有序类别,比如 ExterQual 这列,它表示外部装修质量,取值有 Ex(优秀)、Gd(良好)、TA(一般)、Fa(差)、Po(很差)。这些取值之间是有高下之分的,所以不能当作普通字符串直接 One-Hot,否则模型会认为 Ex、Gd、TA 是三个互相独立的平行类别,彻底丢失优劣顺序。

我手动建立一个映射字典,让它们变成 5, 4, 3, 2, 1 这样有序的数值,放到模型里它就能理解 5 比 4 好、4 比 3 好这个顺序了。

qual_map = {'Ex': 5, 'Gd': 4, 'TA': 3, 'Fa': 2, 'Po': 1}

像 KitchenQual、HeatingQC、PoolQC(填充之后)这类字段都可以走这个映射。OverallQual 和 OverallCond 本身就是从 1 到 10 的分数,直接保留数值即可。这一步做完,我再回头查看缺失值,发现已经清得七七八八了。

5.2 名义类别特征 One-Hot 编码的坑

遇到 Neighborhood、MSZoning、SaleType 这类没有顺序的类别,常规做法是 One-Hot 编码。但这里有一个坑:如果训练集和测试集在某些类别上的分布不一致,直接对合并后的数据做 Pandas 的 get_dummies,会导致两边维度不一致。解决这个问题的标准做法是:先合并、再编码、最后重新切分。

all_data = pd.get_dummies(all_data)

这一步之后,特征数量会暴增,从原来的八十个左右变成两百多个,这是正常现象。但要注意的是,每个哑变量列上如果还有缺失值,需要统一兜底填 0,因为哑变量本来就是 0/1 体系,缺失会被解读成 NaN,很多模型不接受。

5.3 构造新特征:用生活常识给模型递线索

模型自己虽然能学习特征组合,但如果你能用业务常识直接构造出关键组合特征,它的学习负担会小很多,精度也会提升。我在这个项目里构造的几个特征不是拍脑袋,是实实在在有意义的:

  • 总面积:把地面居住面积、地下室面积、车库面积等加起来,让模型直接看到房子的总体规模;
  • 翻修标记:如果 YearRemodAdd 和 YearBuilt 不一致,说明房子经历过大改造,这类房子通常价值更高;
  • 卫生间总数:把各层卫生间加总成一个值,减少模型需要自己组合的难度。

这些特征的作用,举个例子你就明白:原本模型要自己学出 “地下室面积和地面面积共同影响价格” 这个组合关系,现在你直接给它一个 TotalSF,它只需要学一个系数就行。

5.4 最终核验与保存

特征工程做完,我最后会跑一遍all_data.isnull().sum()确认全表无缺失。这一步不能跳,因为哪怕一个小字段遗漏,后面模型都会直接报错,或者更糟,模型跑通了但结果莫名失真。

确认无误后,把合并数据重新切回训练集和测试集,目标是得到一份干净的、无缺失的、维度对齐的 X_train 和 X_test,然后就可以放心交给模型了。

X_train = all_data[:len(y_log)] X_test = all_data[len(y_log):]

6. 踩坑实录:那些没人提醒你的事

这一节专门给你总结我在这个项目里遇到过的几个典型问题,全部是真实发生的,不是理论推断。

6.1 标签泄露:预处理时不小心把 y 也处理了

这是我第一次做这个比赛时犯过的错,当时我把训练集整体做了一个 fillna,没把 SalePrice 单独摘出来。结果 SalePrice 也被填充了,模型训练的时候特征和标签之间出现信息交叉,评估分数看起来很高,但提交到平台上分数直接崩了。正确做法我前面已经提过,先y = train['SalePrice']摘出来,再drop('SalePrice', axis=1),确保标签不参与任何特征工程操作。

6.2 填充统计量用了全局而不是分组

LotFrontage 这个特征就是个典型教训。刚开始我用全局中位数填充,结果按 Neighborhood 分组填完之后分数提升了大概万分之几。别看提升幅度小,在 Kaggle 这种靠千分位定排名的比赛里,任何一个合理增益都值得争取。

我现在习惯在填充前先画一个分组分布图,看看有没有明显的组间差异。有的话就分组填,没有就用全局统计量。

6.3 缺失值全填 0 导致特征失真

有几个布尔或者数量型特征,比如有没有车库、有没有地下室,我一开始嫌麻烦,统一用 0 去填。但效果很差,原因是:0 对于某些特征是合理的,比如面积,但对于桩基础类型或者装修质量这类已经有语义的字段,0 会引入虚假的数值关系。后来我改成了分特征定制填充策略,再加上配对构造 HasGarage 之类的标志位,分数才算恢复正常。

6.4 偏度检查做晚了,模型被“长尾”拖累

我记得早期跑基线模型时,分数一直卡在 0.15 左右上不去。后来把偏度大于 0.75 的特征清单打出来,才发现 LotArea、MasVnrArea 等一批特征右偏极其严重。做完整套偏度修复后,分数立刻往下走了一个台阶。现在我的习惯是:任何数值特征进入模型之前,先扫一遍偏度,再决定要不要变换。

6.5 一句话速查表

为了方便你对照排查问题,我把常见的异常、原因和解决方案整理成了一张表:

症状可能原因解决方案
训练集分数高,提交分数低标签被预处理污染先摘出 y,再处理特征
模型报错“Input contains NaN”某列填充遗漏提交前跑一遍全局缺失检查
线性模型系数异常大特征量纲不一致对数值特征做标准化
特征多但模型无提升One-Hot 分得太碎考虑合并低频类别,或改用顺序编码
预测值全是负数或很小做了 log 变换但没逆变换提交前用 expm1 还原

最后再分享一个我个人的心得,其实并不复杂:数据预处理表面上是一堆编程操作,但它真正考验的是你对这个业务场景的理解深度。很多人以为拿到高分靠的是模型选得多好、参数调得多准,实际上在这个房价预测的比赛里,前百分之八十的分数都在数据预处理里就注定了。你愿不愿意在缺失值上多问一个“为什么”,愿不愿意对每一个连续特征都检查一次分布,愿不愿意花时间去构造那些看起来笨但很有解释力的组合特征,这决定你后面模型的天花板。

如果你也是第一次跑这个比赛,我的建议是不要急着上 XGBoost 或者深度学习模型,先踏踏实实把这条预处理流程过一遍。等你把数据收拾到 “干净、无缺失、有语义、分布合理” 的状态,再回来看模型,你会发现很多事情都顺了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询