☰
Kaggle房价预测实战:从缺失值处理到模型集成全解析
2026/10/3 11:39:52 网站建设 项目流程

打开这个比赛的 Notebook 时,我做了三件蠢事:先去看了别人的高分代码,然后尝试原样运行,最后对着完全不同的输出一脸茫然。后来我才明白,问题根本不是模型跑不起来,而是我压根没搞懂这个数据集在表达什么。

这个比赛就是 Kaggle 上经典的《房价预测:高级回归技巧》(House Prices: Advanced Regression Techniques)。你一听名字可能觉得普通,但它常年霸占 Kaggle 入门推荐榜第一名,靠的不是花哨的模型,而是它以极小的数据量覆盖了回归建模里几乎所有关键环节:缺失值处理、偏态分布、类别特征编码、交叉验证、正则化、集成学习。网上一搜这个比赛能搜出大量图文教程,光标题叫“三万字深度解析”的就不止一篇。但很多内容是把别人的代码流程重新排列组合了一遍,真正把“每一步为什么要这样做”讲清楚的少。这篇我想换一个角度,不帮你逐行背诵别人的 Notebook,而是带着你把整条解题链路拆开,看看每一步后面到底是什么逻辑,以及复现过程中你在哪几个环节最容易翻车。无论你是刚注册完 Kaggle 账号准备跑第一个比赛,还是已经跑通 Baseline 想继续涨分,这篇应该都能给你一些参考。

先回答一个大家反复在问的问题:参加 Kaggle 竞赛对求职到底有没有帮助?我的看法是,有,但帮助的大小取决于你怎么参赛。如果你是照着别人的 Notebook 一键运行然后提交,那充其量是熟悉了平台操作;但如果你真的把这个比赛从头到尾推导过一遍,能够解释清楚你填的每个缺失值都用了什么策略、为什么交叉验证要用 10 折而不用 5 折、集成模型比单一模型好在哪,那这种经历才是面试里拿得出手的东西。所以这篇文章的目标很清楚:不只要让你“跑通”,更要让你“讲得清”。

1. 为什么我在入门 Kaggle 时死磕这个比赛

1.1 这个比赛看起来是预测房价,实际上是在训练建模全局观

房价预测这个比赛的赛题描述很简单:给你 79 个解释变量和 1460 条训练数据,让你预测测试集里 1459 套房子的 SalePrice。数据集来自美国爱荷华州 Ames 市,所以你也经常看到有人管它叫 Ames 房价数据集。它不像很多企业里的真实数据那样存在大量乱码、重复记录、字段错位,但缺失值、偏态分布、异常值、高相关特征这些该有的“教学素材”一样不少,非常适合用来练手。

我个人复盘这个比赛时最大的感受是:它其实不是让你去发明什么高深算法,而是逼你在一套完整的建模流水线里做一百个选择题。比如某个列缺失了 80%,你选择直接删掉还是构造一个“是否缺失”的标志位?某个数值特征严重右偏,你选择 Log 变换还是 Yeo-Johnson 变换?整体质量 OverallQual 明明是 1 到 10 的离散打分,你是当连续值用还是当有序类别用?这些选择单独看都不难,但把它们串在一起,就决定了你的模型最终能摸到多高的分数。

1.2 和波士顿房价数据集相比,Ames 数据集好在哪

很多人在搜“波士顿房价预测”时会被引导到这个比赛。波士顿房价确实是教科书级数据集,13 个特征,506 条数据,做线性回归演示非常合适。但放到现代机器学习竞赛的语境下,它有一个致命缺点:太干净了。没有缺失值,没有类别特征,没有明显的偏态特征,不需要你做任何特征工程。这导致你用它只能学到“调用 LinearRegression 再算个 R2”,学不到真正工程化的建模流程。

Ames 数据集则完全不同。它有 79 个特征,其中既有连续数值型特征如地块面积 LotArea、地面生活面积 GrLivArea,也有丰富的类别型特征如 MSZoning(区域划分)、Neighborhood(街区)、SaleCondition(销售条件),还有大量有序的评分特征如 OverallQual(整体质量)、OverallCond(整体状况)。数据集中一部分列缺失率高得离谱,比如 PoolQC(泳池质量)缺失率超过 99%,Alley(巷子类型)缺失率也超过 90%。你不可能跳过处理直接训练模型,必须动手做决策。这正是这个比赛作为入门进阶的价值所在:它逼着你离开舒适区。

1.3 求职角度:怎么让这段比赛经历真正加分

关于“参加 Kaggle 竞赛对求职的帮助”,我见过两种极端。一种人把这个比赛刷到 Top 1%,简历写得很漂亮,但面试时被问“你的缺失值填充为什么用中位数而不是均值”就卡壳了。另一种人分数一般,但能把从数据探索到模型集成的链路讲得清清楚楚,反而拿下了 offer。这背后的原因很简单:面试官想从项目经历里看到的不是你背诵了多少 API 调用,而是你有没有形成一套自己的建模思考框架。

所以建议你复现这个比赛时,每做完一步就在旁边记一笔“为什么”。为什么填中位数?因为 SalePrice 和很多特征存在偏态,均值更容易被极端值拉偏。为什么做特征标准化?因为 Lasso 和 Ridge 这类带 L1/L2 惩罚项的模型对特征的尺度敏感。这些记录不仅是写博客的素材,更是你面试时的弹药。从这个角度说,你花在理解上的时间,远比花在刷分上的时间回报率高。

2. 赛前拆解:赛题机制与那些容易忽略的规则细节

2.1 四份文件各有用途,别只盯着 train.csv

进入比赛的 Data 页面,你会看到四个文件:train.csv、test.csv、sample_submission.csv、data_description.txt。新手最容易犯的错误是一上来就激动地打开 train.csv 开始跑,其他三个文件一律无视。这其实是个效率极低的习惯。

  • train.csv:包含 1460 行,79 个特征列和 1 个目标列 SalePrice,用于训练模型。
  • test.csv:包含 1459 行,只有 79 个特征列,没有 SalePrice,你的任务是预测这些房子的价格并提交。
  • sample_submission.csv:提交样本,包含 Id 和 SalePrice 两列。SalePrice 这一列的值是随便填的,真正的意义是告诉你提交格式长什么样。
  • data_description.txt:这是整个比赛里最重要的文件,但也是被读得最少的一个。里面详细解释了每一个特征字段的含义。比如 MSSubClass 表面上是一串数字,但它代表的是建筑类型编码,其中 20 表示“1 层 1946 年后建”,60 表示“多层 1946 年后建”,数字之间没有大小关系,是名目类别而不是数值。

我强烈建议你花半小时老老实实读一遍 data_description.txt,把每个字段的语义搞清楚再动手写代码。你以为你在做回归,其实你是在用一堆建筑、区位、交通、装修的碎片信息推断一栋房子的价值。不理解字段含义,你做的特征工程就是闭着眼睛猜。

2.2 评估指标 RMSLE:一个影响全局的小细节

这个比赛用的评估指标是均方根对数误差,英文缩写 RMSLE(Root Mean Squared Logarithmic Error),公式长这样:

RMSLE = sqrt( (1/n) * sum( (log(1 + p_i) - log(1 + a_i))^2 ) )

其中 p_i 是预测值,a_i 是实际值。对比我们更熟悉的 RMSE,RMSLE 有三个明显特点。

第一,它在对数空间里计算误差,所以对价格高的房子的绝对误差惩罚相对更小。预测一栋 20 万美元的房子差 1 万美元,和一栋 50 万美元的房子差 1 万美元,在线性空间里同样严重,但在对数空间里,后者相对误差更小,RMSLE 的惩罚更轻。这实际上和房价预测的现实逻辑更贴近:人们通常关心的是“这房子估值偏了多少个百分点”,而不是“偏了多少美元”。

第二,RMSLE 对低估的惩罚大于高估。因为 log(1 + p) 增长是压缩的,当预测值小于实际值时,同样偏差产生的对数差往往更大。用大白话说,你把一栋值 30 万的房子预测成 15 万,比预测成 60 万“扣分”更狠。这个特性会直接影响你对模型的选择和调参取向。

第三,因为要在对数空间比较,最简单也最常用的预处理方式就是对 SalePrice 做 log1p 变换,即 y_log = log(1 + SalePrice)。做完这个变换之后,你在对数空间里用 RMSE 优化,等价于在原始空间里优化 RMSLE。这也是为什么几乎所有公开方案的第一步都是对目标列做 log1p——不是玄学,是顺着指标倒推出来的必然选择。

顺带一提,数据科学界还有个经典的波士顿房价预测数据集,用的也是回归,但指标通常是 RMSE 或 R2,没有 RMSLE 这种对数空间的约束。两者对比着做一次,你会对“指标选择如何影响建模行为”有很直观的体会。

2.3 开局先摸清 79 个特征的缺失率分布

拿到数据之后,不要立刻建模。先用一行代码统计每一列的缺失率,把特征分一下类。这个比赛的缺失值分布非常有层次感,大致可以分成四种情况。

缺失率范围典型字段处理策略
>90%PoolQC、MiscFeature、Alley、Fence这些字段绝大多数房子压根没有对应属性,缺失本身就是“无”的含义,填 None 或构造布尔标志位
60%-90%FireplaceQu房子没有壁炉,所以质量字段缺失。填 None 或构造 HasFireplace
10%-60%LotFrontage、GarageType、GarageFinish、BsmtQual缺失大多因为该房子没有车库或地下室,但也存在个别真缺失,需要结合业务判断填充
<10%Electrical、MasVnrArea、MasVnrType少量随机缺失,用众数或中位数填充即可

这张表是我自己整理时的习惯,你也可以按自己的理解重排。关键是脑子里要有一张清晰的“缺失地图”,而不是对着 null 百分比发呆。我在第一次复现时直接对所有缺失列统一填了 None,导致 LotFrontage 这种数值列变成字符串,后面做特征缩放时报错才回头处理。类似的坑,你不必再踩一遍。

3. 特征工程是上限,模型只是逼近这个上限

3.1 目标变量先做 Log 变换:不只是为了指标

在动手处理任何特征之前,第一件事就是看 SalePrice 的分布。你会发现它明显右偏,长尾拖得很长,有几栋房子价格明显高于整体水平。对于线性模型来说,偏态目标会导致残差非正态,影响模型效果;对于树模型来说,虽然影响没那么致命,但在集成学习和交叉验证的框架下,所有模型的预测误差都共享同一个评估指标,所以统一做变换依然是收益最大的做法。

实际操作很直接:

import numpy as np import pandas as pd train = pd.read_csv('train.csv') test = pd.read_csv('test.csv') train['SalePrice'] = np.log1p(train['SalePrice'])

注意这里存下来的 y 已经是对数空间的值了。后面所有模型训练都基于变换后的目标,等到生成提交文件时,记得用 np.expm1() 把预测值还原回原始房价,否则你提交的价格会是对数量级,分数直接崩盘。

3.2 缺失值填补的顺序和策略,比你想的更讲究

缺失值填充没有唯一正确答案,但有一些通用原则。

对类别特征,我推荐的策略是:缺失率高的列统一填 "None",并额外构造一个"该字段是否缺失"的辅助特征。比如 Alley(巷子类型),大多数房子根本没有巷子入口,缺失是常态而不是异常,你直接填 "None" 再做一个 HasAlley 列,模型就能同时学到“没有巷子”和“有巷子但类型未知”两种信息。缺失率低的类别列则用众数填充,比如 Electrical 只有一个缺失值,填最频繁的 "SBrkr" 完全没问题。

对数值特征,中位数通常比均值更稳。原因在于这个数据集里有不少偏态分布的特征,比如 LotFrontage(临街长度)和 LotArea(地块面积),均值会被极端值拉高,中位数更能代表典型值。另外我习惯在填充完数值特征后,增加一列"该列是否缺失"的 0/1 标志位,尤其是当缺失率在 5%-20% 这个区间时,这个标志位本身往往就是有用的预测信号。

顺序问题也很关键。我的做法是:先合并 train 和 test 到一个统一的 all_data 里,再统一做填充和编码。这样做的意义在于,测试集和训练集中的特征分布来自同一批数据生成过程,合并后计算中位数和众数时用的样本量更大,估计更稳定。具体到这个比赛,训练集 1460 条,测试集 1459 条,几乎一半一半,如果你只按训练集算中位数,等于扔掉了一半有效信息。

3.3 偏态特征与组合特征:从“字段”到“领域知识”

处理好缺失值之后,下一步是看连续型特征的偏度。SalePrice 是偏态的,但不代表只有它需要处理。用 df.skew() 扫一遍,你会发现很多特征比如 LotArea、GrLivArea、BsmtFinSF1 等都存在明显偏态。处理方式有两种:一是直接对正偏特征做 Log 变换,二是用 scipy 的 Box-Cox 变换自动估计最优 lambda。Box-Cox 要求输入严格为正,而 Yeo-Johnson 可以处理负值,实际使用中我一般直接用 sklearn 的 PowerTransformer(method='yeo-johnson'),一步到位。

特征工程的重头戏是组合特征。Ames 数据集的字段之间天然存在很多业务关系,组合出来的新特征往往比原始字段更有预测力。我复现过程中长期保留并验证有效的三个组合特征:

  • TotalSF = TotalBsmtSF + 1stFlrSF + 2ndFlrSF。它是房子总面积的近似值,比单独看某一个面积字段更能代表房子体量。
  • OverallScore = OverallQual * OverallCond。质量和状况是两回事,但一栋房子最终是否宜居,取决于质量与维护状况的乘积关系。质量 9 但状况 2 的房子,显然不如质量 6 状况 7 的房子住得舒适。
  • Age = YrSold - YearBuilt,以及 RemodAge = YrSold - YearRemodAdd。房龄和翻修年龄是折旧的核心变量,很多真实房价模型里它们比建造年份本身更重要。

这些组合特征不是拍脑袋想的,而是从 data_description.txt 的字段解释里读出来的业务逻辑。这也是我反复强调读文档的原因:任何高级特征都没有捷径,先理解字段,才能组合字段。

3.4 编码方式怎么选:one-hot、label encoding 还是 frequency encoding

类别特征的处理是新手最容易晕的地方。这个数据集里类别特征接近 40 个,策略不对很容易把特征矩阵撑爆。我压箱底的建议分三层。

对确实有序的类别特征,比如 ExterQual(外部材质质量,分为 Po、Fa、TA、Gd、Ex 五档),用 LabelEncoder 即可,因为档位之间天然有顺序关系,编码成 0-4 不会误导模型。

对无序且取值数量少的类别特征,比如 MSZoning、SaleCondition,用 one-hot 编码,列数膨胀可控。

对无序但取值极多的类别特征,比如 Neighborhood(街区,多达 20+ 种),one-hot 会让矩阵多出几十列,而且在 1460 样本的限制下很多类别出现的频率很低,容易造成稀疏问题。这种字段我用 frequency encoding,用“每个类别在该字段中出现的次数”替换原始字符串。这个思路的本质是告诉模型:居住在某个人口密集街区,和在某个稀有街区,本身隐含了不同的市场结构信息。

4. 高级回归技巧,真正高级在哪

4.1 正则化线性模型在表格数据里的统治力

很多人看到“高级回归技巧”这个标题,觉得一定是 XGBoost 或者深度学习。但在这个比赛里,带正则化的线性模型表现一点都不差,甚至经常是融合模型里不可或缺的底座。原因在于特征经过 one-hot 和 frequency encoding 后会变得非常多,变量之间相关性极强,普通线性回归在这种情况下会出现系数极度不稳定的问题。Lasso(L1 正则)可以稀疏化特征,Ridge(L2 正则)可以压缩系数幅度,ElasticNet 则是两者的结合。

在 sklearn 里用 LassoCV 可以很方便地完成带交叉验证的调参:

from sklearn.linear_model import LassoCV lasso = LassoCV(alphas=np.logspace(-4, 0, 100), cv=5, random_state=42) lasso.fit(X_train, y_train)

LassoCV 会在内部跑交叉验证并选出最优 alpha,省去了手动调参的麻烦。实际运行中你会发现它选出来的模型在验证集上的表现和树模型几乎打平,这在很多实际业务场景里也同样成立:表格数据、特征中等到高维、特征间存在复杂线性关系时,正则化线性模型是性价比最高的起点。

4.2 树模型为什么强,以及它最容易过拟合的地方在哪

如果说正则化线性模型是这个比赛的地基,树模型就是主力部队。XGBoost、LightGBM、CatBoost 这三个在表格数据竞赛里轮流称王,房价预测这个比赛自然也不能缺席。它们的核心优势是能捕捉非线性关系,而且对异常值相对鲁棒,对特征的尺度不敏感,省去很多标准化的麻烦。

但优势也是劣势的来源。树模型太容易在 1460 条样本上过拟合了。处理过拟合的几个有效手段我在复现中反复验证过:

  • 控制深度:max_depth 一般设在 3 到 6 之间,深度超过 8 在这么小的数据集上基本就开始“背答案”了。
  • 控制叶子节点信息量:XGBoost 的 min_child_weight 和 LightGBM 的 min_data_in_leaf 都可以适当调高,防止模型从极少样本里学出无意义的规则。
  • 随机抽样:subsample 和 colsample_bytree 分别控制在 0.7 到 0.9,既增加多样性,又能防止过拟合。
  • 早停:learning_rate 调低到 0.01-0.05,同时配合 early_stopping_rounds,观察验证集误差不再下降就及时停止。

4.3 交叉验证是整条流水线里最不能用默认配置的一环

如果你的模型只用一次 train/test 划分来验证效果,那在这个比赛里你会被分数波动折磨到怀疑人生。1460 条样本实在太少了,随机划分的运气成分非常大。我推荐的方式是 K 折交叉验证,K 取 10。为什么是 10 而不是 5?因为样本量小,10 折让每一折训练集都有 1314 条,验证集有 146 条,既保证了训练充分,又让验证集稍微大一点,评估指标更稳定。当然你完全可以用 RepeatedKFold 做多次重复的 10 折交叉验证,然后取均值,这样能把波动压得更低。

交叉验证还有一个隐藏用途:生成 OOF(Out-Of-Fold)预测。简单说,在每一轮交叉验证中,用训练好的模型对验证折做出预测,最后把每一折的验证预测拼接起来,就得到了一个“模型在训练集上的全局预测”。这个 OOF 预测是集成学习和 Stacking 的基础材料。

4.4 从简单平均到加权集成,再到 Stacking

单独调用一个模型,在这个比赛里大概能到一个还不错的分数,但想继续往上走,几乎必然要进入集成阶段。最简单的集成是取多个模型的预测值平均。先对每个模型做交叉验证,记下它的验证分数,然后两个模型一组,测试不同权重组合下的验证分数变化。比如 Lasso 权重 0.3、XGBoost 权重 0.7,可能会比他们各自的 0.5/0.5 更好。因为 Lasso 在捕捉线性关系上有优势,XGBoost 在非线性特征交互上有优势,两者的误差相关性低,互补性强。

更高阶的集成就叫 Stacking:用基模型的 OOF 预测作为特征,训练一个二级模型(通常用 Ridge),让模型自动学习不同基模型的权重。二级模型一般不能太复杂,否则同样会过拟合。在这个数据量下,Stacking 的提升幅度不一定比手工加权平均大多少,但它能让你节省大量的手动调权重时间,属于工程师更偏好的方案。

5. 从零复现一条跑分流水线(可运行级代码)

5.1 环境准备:本地跑还是 Kaggle Notebook,这是个效率问题

我复现这个比赛时,第一版代码是在本地跑的,原因是迭代速度快、调试方便。你不需要 GPU,这个数据集小到 CPU 跑全流程也就一两分钟。本地环境需要安装 pandas、numpy、scikit-learn、xgboost、lightgbm、scipy 这几个核心库。Kaggle 官方也提供了在线 Notebook 环境,预装好了几乎所有常用库,而且可以一键提交,省去了本地文件上传的麻烦。

这里集中回答一下经常被问到平台问题。Kaggle 注册时如果遇到验证码不显示或者收不到验证邮件的情况,可以先看垃圾邮件箱,或者换一个浏览器重试,清除浏览器缓存也能解决一部分问题。如果你在学校或公司网络下注册不了,可以试试切换手机热点之类的网络环境再操作,很多时候问题出在网络环境而不是平台本身。成功注册之后,在 Notebook 右上角的 “Settings” 面板里可以勾选 “Turn on Internet” 来访问外部数据,如果不勾选,Notebook 只能访问比赛数据,安装额外包或加载预训练模型都会失败。提交时只要点右上角的 “Submit to Competition” 按钮就可以了。

5.2 数据预处理与建模主流程代码

下面这份代码是我简化后的核心流程,不是最优方案,但足够你跑通并理解整个链路。它的思路是:读数据 -> 合并 -> 补缺失 -> 编码与变换 -> 建模 -> 交叉验证 -> 集成交付。

import numpy as np import pandas as pd from scipy.stats import skew from sklearn.model_selection import KFold from sklearn.preprocessing import LabelEncoder, PowerTransformer from sklearn.linear_model import LassoCV from xgboost import XGBRegressor from lightgbm import LGBMRegressor train = pd.read_csv('train.csv') test = pd.read_csv('test.csv') y = np.log1p(train['SalePrice']) test_id = test['Id'] ntrain = train.shape[0] ntest = test.shape[0] # 合并处理,保证填充统计量的一致性 all_data = pd.concat([train.drop('SalePrice', axis=1), test], axis=0, ignore_index=True) all_data = all_data.drop('Id', axis=1) # 缺失率高的类别特征统一填 None,并增加标志位 high_missing_cols = ['PoolQC', 'MiscFeature', 'Alley', 'Fence', 'FireplaceQu'] for col in high_missing_cols: all_data[col + '_missing'] = all_data[col].isnull().astype(int) all_data[col] = all_data[col].fillna('None') # 数值特征统一填中位数 num_cols = all_data.select_dtypes(include=np.number).columns for col in num_cols: if all_data[col].isnull().sum() > 0: all_data[col] = all_data[col].fillna(all_data[col].median()) # 有意义的类别特征填众数 cat_cols = all_data.select_dtypes(include='object').columns for col in cat_cols: if all_data[col].isnull().sum() > 0: all_data[col] = all_data[col].fillna(all_data[col].mode()[0]) # 组合特征 all_data['TotalSF'] = all_data['TotalBsmtSF'] + all_data['1stFlrSF'] + all_data['2ndFlrSF'] all_data['OverallScore'] = all_data['OverallQual'] * all_data['OverallCond'] all_data['Age'] = all_data['YrSold'] - all_data['YearBuilt'] all_data['RemodAge'] = all_data['YrSold'] - all_data['YearRemodAdd'] # 类别编码 for col in cat_cols: all_data[col] = LabelEncoder().fit_transform(all_data[col]) # 偏态数值特征做 Yeo-Johnson 变换 skewed_feats = all_data[num_cols].apply(lambda x: skew(x.dropna())).sort_values(ascending=False) skewed_feats = skewed_feats[abs(skewed_feats) > 0.5].index pt = PowerTransformer(method='yeo-johnson') all_data[skewed_feats] = pt.fit_transform(all_data[skewed_feats]) # 切回训练集和测试集 X_train = all_data.iloc[:ntrain].reset_index(drop=True) X_test = all_data.iloc[ntrain:].reset_index(drop=True) # 10 折交叉验证设置 folds = KFold(n_splits=10, shuffle=True, random_state=42) def cv_predict(model, X, y, folds, is_lgb=False, is_xgb=False): oof = np.zeros(X.shape[0]) preds = np.zeros(X_test.shape[0]) for trn_idx, val_idx in folds.split(X): trn_x, trn_y = X.iloc[trn_idx], y.iloc[trn_idx] val_x, val_y = X.iloc[val_idx], y.iloc[val_idx] if is_lgb: model.fit(trn_x, trn_y, eval_set=[(val_x, val_y)], eval_metric='rmse', callbacks=[lgb.early_stopping(100)]) elif is_xgb: model.fit(trn_x, trn_y, eval_set=[(val_x, val_y)], verbose=False) else: model.fit(trn_x, trn_y) oof[val_idx] = model.predict(val_x) preds += model.predict(X_test) / folds.n_splits return oof, preds lasso = LassoCV(alphas=np.logspace(-4, 0, 100), cv=5, random_state=42) lasso_oof, lasso_pred = cv_predict(lasso, X_train, y, folds) xgb = XGBRegressor(n_estimators=3000, learning_rate=0.01, max_depth=4, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=1.0, random_state=42) xgb_oof, xgb_pred = cv_predict(xgb, X_train, y, folds, is_xgb=True) lgb = LGBMRegressor(n_estimators=3000, learning_rate=0.01, max_depth=4, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=1.0, random_state=42) lgb_oof, lgb_pred = cv_predict(lgb, X_train, y, folds, is_lgb=True) # 加权集成 final_pred = 0.3 * lasso_pred + 0.35 * xgb_pred + 0.35 * lgb_pred sub = pd.DataFrame({'Id': test_id, 'SalePrice': np.expm1(final_pred)}) sub.to_csv('submission.csv', index=False)

上面这段代码可以直接运行,但不建议直接抄完就提交。我特意把交叉验证封装成了一个函数,因为不同模型的接口和训练方式不一样,用同一个函数处理能大幅减少代码重复,也更方便你之后加新模型。权重 0.3/0.35/0.35 只是我随手给的初始值,真正合适的最优权重需要你根据自己 OOF 分数的表现手动微调。

5.3 提交文件格式:最容易在最后关头翻车的细节

提交阶段最常见的两个错误,第一个是忘了把 SalePrice 从 log 空间还原,第二个是提交文件的行数或顺序和 sample_submission.csv 不一致。测试集 1459 行,submission 必须也正好是 1459 行。提交文件的 Id 顺序最好保持原始 test.csv 的顺序,虽然 Kaggle 系统会按 Id 匹配,但顺序混乱很容易让你自己在核对时昏头。最后提交前用一行代码检查:

assert sub.shape == (1459, 2) assert not sub['SalePrice'].isnull().any()

如果 Safely 通过,再提交。分数出来如果和你本地 CV 分数差距很大,第一件事看格式,第二件事看是不是自己忘了 expm1。

6. 复现过程中最容易翻车的几个点

6.1 合并训练集和测试集做填充,到底算不算数据泄漏

有些新手教程会告诉你:绝对不能合并训练集和测试集,否则会数据泄漏。这个说法在这个比赛里并不完全成立。要分清楚什么是泄漏,什么不是。

泄漏指的是你的模型在训练阶段接触到了测试集的目标信息,或者用未来的信息预测过去。但在 train.csv 和 test.csv 合并填充缺失值这个操作里,我们只用到了测试集的特征分布,没有用到任何目标信息。而且在真实的预测场景里,模型最终要对测试集逐条预测,你本来就有权查看测试集的特征。所以合并填充是安全的,而且因为样本量增大了,填充统计量反而更稳定。

真正危险的泄漏是另外两种情况。一种是你在做异常值处理时,用全体数据(包括测试集)的分布来决定哪些点是异常点并删除,这会让你对测试集的判断先入为主。另一种是你在做特征缩放时把训练集和测试集一起标准化,这本身没问题,但如果你在交叉验证的每一折里都用了全体数据的均值和标准差,每一折的验证集信息就被提前混入训练过程了。同样的道理也适用于 PowerTransformer。正确做法是:在交叉验证内,只用训练折的数据去 fit 变换器,再 transform 验证折和测试集。初学者为了省事常常直接对全量数据 fit_transform,这也是很多本地分数虚高的根源。

6.2 填缺失值的顺序会影响结果,这不是玄学

我在第三节提到先合并再统一填充,实际操作中你会发现一个有趣的现象:对于中位数这种依赖分布的统计量,你在合并后的 all_data 上计算出的中位数,和只在 train 上计算的中位数,可能很接近,也可能完全不同。一个字段如果有 20% 的缺失,而测试集的分布又和训练集有差异,那么两种方式填充后的值就会不一样,后面模型结果自然也不同。

更隐蔽的是类别特征。如果你先填了 None,再做 LabelEncoder,编码的结果会因为你填充顺序不同而不同。假设某个列里原来的值是 A、B、C,缺失部分填了 None,编码后 None 变成 0,A 变成 1;如果你先编码再填充,A 是 0,None 反而被映射到某个新值。两种方式 A 的编码数字完全不同,模型训练出来的特征重要性也会跟着变。如果模型是树模型,其实对单调变换不敏感,但对线性模型来说,编码顺序完全影响结果。

所以请你固定一套流程,不要一会儿先补缺失再编码,一会儿先编码再补缺失。你不需要判断哪种绝对正确,只需要保证交叉验证和最终预测用完全一致的处理顺序。

6.3 为什么我劝你别用 TPU 跑这个比赛

Kaggle 提供了免费的 TPU 配额,很多人一上来就想着用 TPU 加速。但在这个比赛里,TPU 不是加速器,而是麻烦制造者。原因有三点。第一,TPU 主要面向大规模张量运算,典型的应用场景是深度学习的卷积和 Transformer 训练,而房价预测这种小规模表格数据,用 TPU 跑树模型基本发挥不出任何硬件优势。第二,TPU 的 API 习惯和 CPU/GPU 完全不同,比如 TPU 上处理数据需要经过 tf.data 管道,你原本用 pandas 和 numpy 处理好的数据还得费劲转换格式。第三,Kaggle 的 TPU 配额有每周上限,你用在这种小比赛上纯属浪费。

我的建议是:本地或者 Kaggle Notebook 的 CPU/GPU 跑都足够。如果你已经选了 TPU 运行,也可以在 Notebook 的 Settings 里直接切换回 GPU,不需要重新创建笔记本。

6.4 关于随机性、Seed ensemble 与分数波动的观察

同一个模型,同一个参数,只改 random_state,你得到的分数会在一个区间内波动。1460 条训练样本太少,随机种子的影响会被放大。所以很多人问我“为什么我和教程用一样的代码,提交分数却不同”,答案很可能就是随机种子不同。处理方式有两种:一是固定一个随机种子并接受波动,二是使用 Seed ensemble,也就是用多个不同的随机种子分别训练模型,最后对预测值取平均。后者的核心逻辑是减少单次随机性带来的方差,在实践中通常能稳定提升一点点分数。

做 Seed ensemble 时需要注意,它不是简单地把多个模型输出平均就完事了。你最好为每个种子都记录下它的 OOF 分数,剔除明显偏离平均水平的种子。有时某个种子训练出来的模型在本地表现很好,但在测试集上翻车,反而是加入更多种子的平均结果更稳定。

6.5 提交报错与平台问题速查

最后把我在实践里遇到过的 Kaggle 平台问题做一个速查表,希望能帮你省掉不少时间。

问题原因解决方法
提交后提示 Incorrect number of rows提交文件行数和测试集不一致重新检查 test.csv 行数,确保 submission 行数一致
提交文件报错 Missing value in SalePrice预测值里有 NaN检查模型预测前是否有未处理的缺失值,或在生成文件前用 isnull() 检查
Notebook 运行时报错找不到 xgboost/lightgbm当前环境未安装在 Notebook 开头执行 !pip install xgboost lightgbm,并开启 Internet 访问
注册后收不到验证邮件邮件被拦截或延迟检查垃圾邮件箱,等待几分钟后重新获取验证邮件
注册页面验证码不出现浏览器插件或缓存问题更换浏览器、清除缓存、关闭广告拦截插件后重试
Notebook 无法连接外部数据Internet 设置未开启在 Settings 面板勾选 “Turn on Internet”

这些平台问题大多数不难解决,但卡住的时候确实耽误时间。顺便说一下,如果你看到 Notebook 页面有 “Schedule” 功能,它是用来设置定时运行的,通常用于定期刷取最新数据或者自动跑完流程后把结果存到数据集里。对于房价预测这种静态赛题,用处不大,我更建议你先在本地把逻辑跑通,再决定要不要用平台自动化功能。

7. 涨分进阶:从 Baseline 到 Top 方案还差什么

7.1 继续加模型:Stacking 的正确打开方式

当你手上已经有了 Lasso、XGBoost、LightGBM 三个模型的 OOF 预测后,接下来最自然的涨分手段就是把它们 Stacking 起来。具体做法是:

把三个模型的 OOF 预测并成一个新特征矩阵,用 Ridge 回归作为二级模型去拟合真实的 y。Ridge 的正则化项在这里很重要,它能防止二级模型过拟合。如果你没有用正则化,而直接用线性回归去拟合,Stacking 很可能不如手工加权平均。

如果继续往上层堆,你可以再训练一个神经网络模型,或者加入 CatBoost,让二级模型看到更多不同风格的预测器。每次加一个新的基模型,都要重新生成它的 OOF 预测和测试集预测,然后重新训练二级模型。这个过程可以迭代很多轮,但收益会逐渐递减。

7.2 特征工程的业务理解进阶:从 Neighborhood 与 SaleCondition 里挖信息

到了后期,你会发现模型分数提升的瓶颈不在模型结构,而在特征的信息量。Top 方案并不会使用特别神秘的算法,而是在特征里加入了更多领域知识。举两个例子。

Neighborhood(街区)是 Ames 市区内部不同地段的划分。不同街区的房价中位数差异巨大,这种差异靠 one-hot 或 frequency encoding 虽然能捕捉一部分,但无法捕捉“街区价格梯队”的连续信息。更高级的做法是:用训练集里每个街区的平均 SalePrice 作为特征编码,直接告诉模型这个街区的整体价位水平。这种目标编码(Target Encoding)有泄漏风险,必须配合交叉验证在每一折内单独计算,但处理得当的话,效果常常优于 frequency encoding。

SaleCondition(销售条件)也是类似道理。字段里包含 Normal(正常)、Abnorml(异常,比如法拍或遗产出售)、Partial(部分,例如房子未完工时已经签约)等情况。Partial 条件下成交的房子,通常是买家参与了建造过程,价格形成机制和 Normal 完全不同。你可以构造一个特征表示“是否为 Partial 销售”,或者把 Abnorml 和 Family 等其他非正常条件分组,让模型能区分出不同的议价环境。

这些特征单看似乎都是小改动,但组合起来的提升往往比调参明显。因为模型的上限由信息量决定,特征工程就是喂给模型更多信息的过程。

7.3 这个比赛的经验如何迁移到真实业务里

最后想聊一点更长远的。

Kaggle 比赛和企业里的建模工作之间最大的差异,不是模型复杂度,而是问题定义方式。比赛中所有评估指标都是给定的,测试集也是固定的;而在真实业务里,目标定义、指标选择、数据采集都是你自己要回答的问题。但房价预测这个比赛教给你的东西,尤其是“先理解字段语义再做特征工程”“用交叉验证衡量模型稳定性”“把多个模型集成降低波动”这几件事,在任何业务场景里都是通用的。

我后来在做信用评分、销量预测这些实际项目时,遇到一个新的数据集,第一反应仍然是先画缺失地图、看分布、读字段说明,而不是急着训练模型。这个习惯就是从这个比赛开始养成的。

关于这个比赛,网上资料很多,从几千字到几万字的都有。但如果你只带走一个观念,我希望是:模型是最后一个环节,前面所有的数据理解、缺失值策略、特征工程、交叉验证设计,才是决定你分数上限的东西。你先想清楚“为什么”,再动手写代码,整个过程会顺利得多。

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

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

立即咨询