简介:《Kaggle房价预测比赛代码.zip》是一份面向数据科学初学者与竞赛爱好者的完整参赛源码,对应Kaggle经典“房价预测”赛题,涵盖从数据读取到结果输出的全流程。压缩包共13个文件,主要包含7个csv数据文件(提供训练集、测试集和提交样例)、5个Python脚本(覆盖随机森林、增强集成、堆叠、神经网络等模型方案)以及1个Markdown说明文档,整体大小约274KB,结构清晰,便于按模块阅读。目前已有250人学习浏览,适合希望借助真实赛题快速上手机器学习流程的读者。代码从数据预处理出发,依次展示特征工程、多种模型训练、交叉验证、超参数调优和结果融合等环节,尤其侧重随机森林与集成增强的组合策略以及神经网络建模路径。通过研读源码,可以理解Kaggle竞赛的标准操作流程,包括数据清洗、缺失值处理、标准化、模型评估与提交文件生成等细节,是一份能够直接运行并迁移到同类预测任务的实战参考。
1. Kaggle房价预测比赛代码:先看清这份比赛源码能给你什么,再谈复现
先说一个反直觉的结论:拿到这份 Kaggle 房价预测比赛的源码包,最值钱的不是最后一名的排名,而是中间那几步“走弯路”的状态。这个 zip 里装的是完整的比赛项目结构:input 目录放 train.csv、test.csv、sample_submission.csv,src 里按随机森林、集成提升、堆叠融合、神经网络几个方向拆开,外加一个 README。你要预测的只是 SalePrice 一个数值,但真正要学的是:面对 79 个特征,哪些列要清洗、哪些特征要交叉、树模型和集成怎么配合、最后怎么组织一次不会被判错的提交。它适合两类人:一是想完整跑通一场 Kaggle 比赛的初学者,照着目录挨个读比看零散教程有效;二是已经做过几轮房价预测、想拿别人的工程结构对比自己边界在哪的熟手。这篇笔记按“数据→特征→模型→融合→提交”的顺序,把源码里最有肉的几条路径拆开讲。
2. 数据预处理:先把 train.csv 读透,再做目标变换和缺失值兜底
这一章的目的是先解决三个影响后面全部模型的问题:训练集和测试集行数不一致时,特征工程怎么共用一套代码;SalePrice 严重右偏时,目标变量要不要变换;缺失值里面哪些是“缺数据”哪些是“本来就没有”。
2.1 载入数据与目标变量分布检查
拿到压缩包后我一般先不急着解压直接跑,而是先看目录,确认数据文件在 input 里,然后动手写数据载入。train.csv 有 1460 行 80 列,其中一列是 SalePrice;test.csv 有 1459 行 79 列;sample_submission.csv 只有 Id 和 SalePrice 两列。80 列里数值型和类别型大约各占一半,这种规模的数据跑随机森林和 LightGBM,单机内存完全不是瓶颈,真正的瓶颈在特征构造的思路。
第一件事是看 SalePrice 的分布。很多新手图省事,直接 load 进去就开训练,最后分数上不去又不知道问题在哪。标准做法是先打印描述统计和偏度:
import pandas as pd import numpy as np train = pd.read_csv('input/train.csv') test = pd.read_csv('input/test.csv') print(train.shape, test.shape) print(train['SalePrice'].describe()) print('skew:', train['SalePrice'].skew())输出里 SalePrice 的均值大约在 18 万美金,中位数在 16 万左右,最大值超过 75 万,偏度在 1.8 上下。> 提示:当偏度超过 1.0 时,我一般直接考虑对数变换。这个比赛的评估指标是均方根误差,官方用的就是 log1p(SalePrice) 之后的目标,你在本地算的 RMSE 要和排行榜对齐,就得在同一个变换空间里比较。
2.2 目标变量 log 变换与训练测试集合并
我习惯把目标变量取出来单独存一份,剩下的特征列全部拼到一个 DataFrame 里做特征工程。这样后面做缺失值填充、编码、组合特征时,train 和 test 用的是同一套逻辑,不会出现“train 填了中位数、test 忘了填”的尴尬。
y = np.log1p(train['SalePrice']) train = train.drop(columns=['SalePrice']) full = pd.concat([train, test], axis=0, ignore_index=True) print('full shape:', full.shape) print(full.isnull().sum().sort_values(ascending=False).head(20))为什么合并而不是 train、test 各做一套?因为特征工程里所有 fit 操作,比如中位数、类别编码映射,都必须在全量数据上做,否则 train 和 test 的分布会不一致。比如某些类别状态只在 test 里出现,单独处理时就只能干瞪眼。合并后列名、数据类型、编码字典统一,后续交叉验证也顺手。注意 concat 时一定要 ignore_index=True,否则索引会带着 train 的尾巴,后面切折时容易出位置错位。
2.3 缺失值:分清“缺数据”和“没有这项”
合并后打印的缺失统计里,PoolQC、MiscFeature、Alley、FireplaceQu 这些列缺失率很高,LotFrontage、GarageYrBlt、MasVnrArea 也有不少缺失。这里最容易踩的坑是把所有缺失统一填成中位数或 0,结果模型学到错误信号。
我一般先确认缺失的含义。在房价比赛这个场景里,PoolQC 缺失表示这个房子根本没有游泳池,而不是“有游泳池但质量忘了填”;Alley 缺失表示没有巷道入口;FireplaceQu 缺失多数是没壁炉。处理代码如下:
num_cols = full.select_dtypes(include=['int64', 'float64']).columns cat_cols = full.select_dtypes(include=['object']).columns for c in num_cols: full[c] = full[c].fillna(full[c].median()) for c in cat_cols: full[c] = full[c].fillna('None') print('missing after fill:', full.isnull().sum().sum())数值列填中位数而不是均值,原因是房价特征里常有极端值,均值会被带偏,而中位数对树模型分裂影响更稳定。类别列填字符串'None',为的是让它在编码后变成一个独立的“没有这一类设施”的分组,而不是混进某个真实类别。填完后用 full.isnull().sum().sum() 确认没有遗漏,这一步我在每个项目里都会跑一遍,属于强迫症级别的检查。
2.4 把“看着是数字”的列还原成类别
这一步是很多人拿到源码包时才反应过来的。MSSubClass 是 20 到 190 的整数,但它是房屋类型编码,不是尺寸大小,当数值列用就会产生“190 比 60 贵”这种无意义关系。MoSold 销售月份、OverallQual 等级同理。处理方式很简单:
full['MSSubClass'] = full['MSSubClass'].astype(str) full['MoSold'] = full['MoSold'].astype(str) obj_cols = full.select_dtypes(include=['object']).columns for c in obj_cols: full[c] = full[c].fillna('None')先把 MSSubClass 和 MoSold 转成字符串,再对全部类别列做一次兜底填充,避免有些列里混入 NaN 导致后续编码报错。到这里,数据清洗的骨架就出来了。下一步进入特征工程,那才是拉开分数差距的地方。
3. 特征工程:不是堆特征,而是构造有物理含义的组合变量
特征工程做得好的源码包,往往不是特征数量多,而是每一列都能说出“为什么”。这个比赛里房价由面积、质量、区位、年份共同决定,单独看某个字段很难表达出综合价值,组合特征就是把这些维度拼起来。
3.1 面积与年龄组合特征
我复现这份源码时,最先看的就是 src 里特征构建那一段。常见的做法是把一楼、二楼和地下室的面积合并成总居住面积,把卫生间按全卫半卫折算成总数,再把售出年份和建造年份做差得到房龄。代码大概是这样的:
full['TotalSF'] = full['TotalBsmtSF'] + full['1stFlrSF'] + full['2ndFlrSF'] full['TotalBath'] = full['FullBath'] + 0.5 * full['HalfBath'] \ + full['BsmtFullBath'] + 0.5 * full['BsmtHalfBath'] full['HouseAge'] = full['YrSold'] - full['YearBuilt'] full['RemodAge'] = full['YrSold'] - full['YearRemodAdd'] full['HasPool'] = (full['PoolArea'] > 0).astype(int) full['HasGarage'] = (full['GarageArea'] > 0).astype(int)TotalSF 把三个关联面积合成一个主变量,树模型虽然能自己找交互,但显式给出来会让分裂更早命中关键维度。TotalBath 用 0.5 折算半卫,是这类比赛里默认的处理习惯。HouseAge 比直接丢 YearBuilt 和 YrSold 两列更直观,而且减少了特征维度。HasPool 和 HasGarage 则是把面积类字段变成“有没有”的开关,专门处理大量 0 值面积对模型造成的干扰。
3.2 质量等级的序数编码
源码里另一块关键内容是质量等级处理。ExterQual、BsmtQual、KitchenQual 这类字段的取值是 Ex、Gd、TA、Fa、Po,本身是有序的,但 Pandas 读进来是字符串,直接 one-hot 会丢掉次序信息。正确的做法是给它们建一张序数映射表:
qual_dict = {'None': 0, 'Po': 1, 'Fa': 2, 'TA': 3, 'Gd': 4, 'Ex': 5} qual_cols = ['ExterQual', 'ExterCond', 'BsmtQual', 'BsmtCond', 'HeatingQC', 'KitchenQual', 'FireplaceQu', 'PoolQC', 'GarageQual', 'GarageCond'] for c in qual_cols: full[c] = full[c].map(qual_dict).fillna(0).astype(int)映射后,缺失值天然落在 0,也就是“没有该项设施”,和前面缺失值填充的逻辑完全一致。这样处理比直接 LabelEncoder 靠谱,因为 LabelEncoder 是按字母序给类别编号,Fa、TA、Po 的顺序就乱了。对于 Neighborhood、MSZoning 这种没有大小关系的字段,我一般保留字符串,后续让模型库自己处理,或者用频次编码,避免 one-hot 把维度撑爆。
3.3 特征放到什么程度就该停
源码里还有个容易被忽略的细节:并不是特征越多越好。我见过有人把 GrLivArea 和 LotArea 做比值,又做差值,再做乘积,最后加了二十几个相关性极高的列,树模型的分裂被冗余信息干扰,分数反而下降。这个比赛里常见的做法是控制在二十个组合特征以内,核心围绕面积、质量、房龄、地段四个维度展开。
另外要留意的是,特征工程做在 full 上还是做在 train 上,结果完全不同。所有基于统计量的特征,比如某个类别的 SalePrice 均值编码,只能在 train 上计算后映射到 test,直接用 full 算就是变相把 test 信息泄漏进训练过程。我一般在编码前把 full 按行号切回 train 和 test 两个部分,分别处理段位统计特征,其他全局特征才在 full 上做。> 注意:凡是需要从 y 里学统计量的特征,都必须先切分再编码。
4. 模型与集成方案:random_forest、boosting 与 stacking 怎么配合
这个源码包最值得读的是模型目录的命名方式:random_forest、ensem+boosting、ensem_stacking、Neural Networks 四个方向,基本覆盖了树模型和融合的主流打法。单模型在这个比赛里很难进前排,集成才是提分主力。
4.1 随机森林基线:先建立可对比的 RMSE 基准
先跑随机森林的原因很简单:参数少、收敛快、能快速验证特征工程有没有做错。我把特征矩阵准备好后,用五折交叉验证建立基线:
from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import KFold from sklearn.metrics import mean_squared_error def run_cv(model, X, y, n_splits=5): kf = KFold(n_splits=n_splits, shuffle=True, random_state=2024) scores = [] for tr_idx, va_idx in kf.split(X): X_tr, X_va = X.iloc[tr_idx], X.iloc[va_idx] y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx] model.fit(X_tr, y_tr) pred = model.predict(X_va) scores.append(np.sqrt(mean_squared_error(y_va, pred))) return np.mean(scores), scores rf = RandomForestRegressor( n_estimators=400, max_depth=12, min_samples_leaf=5, random_state=42 ) print('RF OOF RMSE:', run_cv(rf, X_train, y_train))这里 X_train 和 y_train 已经是 log 变换后的目标。n_estimators 取 400 是因为这个数据量下 400 棵树的收敛已经稳定,再加大收益递减;max_depth=12 和 min_samples_leaf=5 用来防止树过拟合到噪声特征。OOF RMSE 在 0.11 到 0.12 之间算正常,如果跑出来低于 0.09,我反而会怀疑是不是特征或目标泄漏。
4.2 ensem+boosting:平均集成让单模型分数稳定
LightGBM 是这类表格比赛的主力。它和随机森林的差异在于逐个学习残差,对特征交互的拟合能力更强。源码里 ensem+boosting 目录对应的典型实现是:
from lightgbm import LGBMRegressor lgb = LGBMRegressor( n_estimators=1500, learning_rate=0.01, num_leaves=31, colsample_bytree=0.7, subsample=0.8, random_state=42 ) lgb_score = run_cv(lgb, X_train, y_train) print('LGB OOF RMSE:', lgb_score)n_estimators 放到 1500 配 learning_rate=0.01,是树模型调参里最常见的组合:学习率小一点,树多一点,精度高且不容易过拟合。num_leaves=31 对应深度约 5 的二叉树复杂度,对这个数据规模比较合适。colsample_bytree=0.7 和 subsample=0.8 让每棵树只看部分特征和部分样本,增加多样性,融合时效果更好。
平均集成很简单,把随机森林和 LightGBM 的预测值按权重相加。我一般先等权试,再看 OOF 结果微调权重:
pred_rf = rf.predict(X_val) pred_lgb = lgb.predict(X_val) blend = 0.3 * pred_rf + 0.7 * pred_lgb权重 0.3 和 0.7 不是拍脑袋,LightGBM 在 OOF 上通常比随机森林低 0.01 到 0.02 的 RMSE,所以给它更高权重。每次调整后都重新打印 OOF,对比确认提升。
4.3 ensem_stacking:OOF 预测作为 meta 模型的输入
平均集成只用了预测值的线性组合,stacking 则把基模型的 OOF 预测当成特征,交给 meta 模型再学一层。源码里 ensem_stacking 目录做的就是这件事。核心代码如下:
def get_oof(model, X, y, n_splits=5): kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) oof = np.zeros(len(X)) for tr_idx, va_idx in kf.split(X): model.fit(X.iloc[tr_idx], y.iloc[tr_idx]) oof[va_idx] = model.predict(X.iloc[va_idx]) return oof rf_oof = get_oof(rf, X_train, y_train) lgb_oof = get_oof(lgb, X_train, y_train) stack_X = np.column_stack([rf_oof, lgb_oof]) meta = LGBMRegressor(n_estimators=300, learning_rate=0.02) meta.fit(stack_X, y_train)关键在于 get_oof 里的 KFold 循环:每个折的模型只在自己的训练部分拟合,再预测验证部分,整个过程不接触验证数据。如果图省事用全量 train 预测再丢给 meta,就相当于 meta 模型看到了训练时见过的预测结果,交叉验证分数会虚高,提交后直接翻车。
stacking 的输入是多个基模型的 OOF 预测,所以基模型越多样越好。源码里放 Neural Networks 目录也基于这个原因:神经网络和树模型的结构差异大,融合时能提供树模型没有的视角。常见的做法是对特征做标准化后训练一个两层 MLP,效果不一定比树模型好,但放进 stack 里通常能再挤一点分数。
5. 实战避坑:五个让分数翻车的常见问题
这一章全部来自真实跑比赛的血泪经验。每一条我都踩过,或者看旁边的人踩过,列出来省得你再交一遍学费。
5.1 坑一:log 变换的逆运算忘了做
- 现象:本地交叉验证 RMSE 很低,提交后却提示格式错误,或者分数比预期差很多。
- 原因:训练时 y 用的是 np.log1p(SalePrice),预测出来的也是 log 空间的值,直接写到 sample_submission.csv,数值会整体偏小,和真实房价差一个指数级。
- 解决:提交前必须对预测值做逆变换,pred_original = np.expm1(pred_log)。> 提示:我每次写完提交脚本都会先打印预测值的 min、max、mean,如果均值在 10 万到 30 万之间,说明逆变换做对了;均值只有几或者几十,肯定是漏了 expm1。
5.2 坑二:预处理在交叉验证外面做完,造成数据泄漏
- 现象:五折交叉验证 RMSE 低到离谱,比如 0.05 以下,但提交分数和本地对不上。
- 原因:在切分前就对整个 X 做了中位数填充、类别编码、标准化,验证集的信息在训练时就已见到。树模型对填充不敏感,真正泄漏的是统计类特征和标准化时用的均值方差。
- 解决:把预处理拆成两个阶段。全局的缺失值填充可以在 full 上做,但所有基于 y 或基于全量 X 统计量的编码,必须放到 KFold 循环内部,每次只对训练部分 fit,再 transform 验证部分。
5.3 坑三:类别缺失值统一填 0 或填众数
- 现象:模型对 PoolQC、FireplaceQu 等列的缺失值响应异常,特征重要性里出现一堆不该出现的列。
- 原因:这些缺失在后端对应“没有这项设施”,填 0 在数值上等于 Po 级,填众数则把所有没游泳池的房子归进了最常见的质量等级,语义全乱。
- 解决:按列语义区分处理。表示“有没有”的设施类字段,缺失填'None'并编码成独立 class;真正属于数据采集遗漏的数值字段,比如 LotFrontage,才用中位数填充。动手前花五分钟翻一下 data_description.txt,比盲目 fillna 值钱得多。
5.4 坑四:年份和 MSSubClass 被当成数值特征
- 现象:特征重要性前十里出现 YearBuilt、MoSold,但模型预测在部分区间上系统性偏低。
- 原因:把年份当连续数值,模型会学出“年份越大房价越高”的线性关系,而实际上有些年份的建材或风格反而影响房价;MSSubClass 是 20 到 190 的房屋类型编码,数值大小完全无意义。
- 解决:MSSubClass 和 MoSold 一律转成字符串类别列,YearBuilt 和 YearRemodAdd 保留数值但配合 HouseAge 一起使用,让模型同时看到绝对年份和相对房龄两个视角。
5.5 坑五:反复提交只看公共排行榜
- 现象:公共榜分数一直小幅波动,最终私有榜出来时排名大幅下滑。
- 原因:公共榜只包含一部分测试数据,反复根据公共榜调参,等于在拿那部分数据做验证,投票多了就会过拟合到公共榜噪声上。
- 解决:把本地 OOF 当作主要判断依据,公共榜只用来确认提交格式和数据 pipeline 是否正常。同一份特征和模型,OOF 和公共榜分数相差不大,说明泛化正常;相差太大就要回头查泄漏。> 注意:不要为了公共榜好看去专门调整某些样本的预测,那是最典型的自欺欺人。
6. 提交环节:从 CV 到合格提交的习惯
最后的提交不只是一个 to_csv 的事情,它决定了前面所有工作能不能被正确评估。我见过太多人模型跑得很好,最后因为 Id 顺序错位或者列名不匹配被判零分。这里把我固定使用的验证流程写出来,你可以直接照着用。
第一步,确认 sample_submission.csv 的格式。比赛要求只有 Id 和 SalePrice 两列,Id 顺序必须和 test.csv 完全一致,SalePrice 是原始房价而不是 log 值。源码包里的 sample_submission.csv 可以直接作为模板,读进来后替换 SalePrice 列,保留 Id 列原顺序:
sub = pd.read_csv('input/sample_submission.csv') sub['SalePrice'] = np.expm1(pred_final) sub.to_csv('submission.csv', index=False)第二步,做三个硬性检查。第一,len(sub) 必须等于 len(test),多一行少一行都会被判错;第二,sub['Id'].values 与 test['Id'].values 完全一致,用 (sub['Id'].values == test['Id'].values).all() 验证;第三,预测值的范围合理,np.expm1 之后最大值在几十万美金量级。
第三步,记录这一版的参数和 OOF 分数。我一般会在提交目录里放一个 submission_notes.txt,写清模型、特征版本、OOF RMSE、公共榜分数、提交时间。比赛进行到后期会有几十个提交版本,没有记录根本分不清哪版是哪版。
第四步,关于融合的最终选择。stacking 和简单平均之间,我最后的习惯是看 OOF 稳定性:把同一个融合方案用三个不同 random_seed 各跑一遍,如果三次 OOF 的标准差小于 0.002,我才放心用它做最终提交。标准差大说明融合对随机性敏感,需要回到基模型层面找原因。源码包里 ensem_stacking 和 ensem+boosting 两个目录,对应就是这两条路线,前者上限高但风险大,后者稳定但提升有限。
从那以后我每次跑比赛都会强制自己走一遍这套流程:解压先看目录结构,数据先查偏度和缺失语义,特征统计类编码必须在折内做,提交前必查 Id 顺序和预测值量纲。这套习惯让我的翻车率降了很多,也希望帮到你。
本文还有配套的精品资源,点击获取