
简介这是一份在 Kaggle 房价预测比赛中取得 Top10 的 Python 实现源码面向计算机相关专业学生、竞赛爱好者及需要课设/毕设项目的开发者可用于学习特征工程、CatBoost 建模与结果提交的完整流程。压缩包共 8 个文件包含 6 个 Python 脚本、1 个说明文档和 1 个 Markdown 笔记源码覆盖数据清洗、并行处理、模型训练与预测提交等关键模块整体仅 26KB结构精简适合快速读懂与二次开发。目前已有 105 人学习作者在介绍中特别提醒将项目路径改为英文降低环境问题发生率。通过这份资源读者可以拿到完整的 Top10 解决方案框架理解赛题数据预处理思路并参考现有脚本改进自己的模型尤其适合想深入钻研机器学习实战的学习者。 如果你在 Kaggle 房价预测比赛House Prices: Advanced Regression Techniques里把 RMSLE 做到 0.15 就再也压不下去先别急着换更猛的模型。这是 Kaggle 上最经典的回归入门赛之一训练集 1460 行、79 个解释变量目标是爱荷华州 Ames 市的住宅成交价评估指标 RMSLE。数据量不大、任务不复杂但公开榜前十名的分数长期被压实到 0.11 附近一份标注“排名 Top10”的 Python 源码值钱的部分不是最后那行 XGBoost 调用而是从原始 csv 走到提交文件的每一步处理。这篇笔记写给两类人刚注册 Kaggle 想跑通第一个比赛的新手以及本地分数卡在 0.14 附近想再压一截的熟手。顺着评分逻辑、特征工程、模型集成和常见翻车点往下讲。2. 考的是 RMSLE 不是 RMSE为什么 Top10 源码第一行代码总是 log 变换这个比赛常被拿来和波士顿房价预测混为一谈但两者差别明显波士顿那个 UCI 数据集只有 13 个特征这里却有 79 个解释变量而且评分指标是 RMSLE不是 RMSE。别小看这一个字母的差别它直接决定了整个建模链路的第一步该怎么走。2.1 读懂 RMSLE低估一半和高估一倍的惩罚一样重RMSLE 全称是 Root Mean Squared Logarithmic Error公式是 sqrt(mean((ln(1p) - ln(1a))²))。p 是预测价a 是实际价ln 里加 1 是为了兜住价格为 0 的极端情况让对数有意义。和 RMSE 相比它多包了一层对数效果是把大房价的绝对误差压缩、把小房价的误差放大。具体打个比方一套房真实成交价 20 万美元你预测成 10 万log 空间的损失约 0.693预测成 40 万损失同样是 0.693。折算回美元低估一半和高估一倍的惩罚一样重这就是 RMSLE 对相对误差的执念。它不关心你差了多少美元只关心你差了百分之多少。对参赛者来说RMSLE 有一个直接推论目标列 SalePrice 必须先在 log 空间里拟合而不是直接拿原始价格回归。Top10 源码里几乎每一份都写着 np.log1p(train[SalePrice])这确实不是技术玄学而是指标和数据分布共同逼出来的必然选择。你在复现时如果看到某份源码直接对原始 SalePrice 训模型那它的分数基本不可能进入 0.12 以内。2.2 目标变量右偏到什么程度skew 接近 2log 后近似正态动手建模前花 10 秒看目标分布。SalePrice 的偏度大概在 1.8 到 2.0 之间属于明显右偏大多数房子在 10 万到 20 万美元区间少量豪宅把尾巴拖得很长。如果不做变换MSE 会被豪宅主导模型为了讨好尾部样本会牺牲掉普通房子的精度。做 np.log1p 之后分布接近正态回归误差在 log 空间近似均匀这正好匹配 RMSLE 想要的误差特性。有些源码还会对 GrLivArea、LotArea 这些面积特征做 log1p原因同源这些特征同样右偏。完整的阈值判断放在第 3 章写这里先记住结论目标 log 是必须做特征 log 是看偏度选择性做。2.3 一份 Top10 源码的典型文件结构在 Kaggle 竞赛官网的 Data 页签下载房价预测数据集后你会看到四个文件train.csv 是 1460 行带真实成交价的训练集test.csv 是 1459 行待预测集sample_submission.csv 是提交模板data_description.txt 解释了 79 个字段。源码部分正常会拆成feature_engineering.py 负责合并数据、填缺失值、造特征models.py 定义 Lasso、Ridge、LightGBM 等候选模型train.py 跑 K 折交叉验证生成每折的 OOF 预测值ensemble.py 把多个模型的 OOF 做加权或 Stacking产出最终提交文件。这个结构本身就是 Top10 方案的骨架——单模型负责下限融合负责上限。下载这类源码后我习惯先看 train.py 和 feature_engineering.py而不是先跑 ensemble.py。因为只有理解了 OOF 怎么产生、特征在哪一步被改才知道集成权重是在哪张表上调出来的。直接跑完整流程很容易得到一个漂亮分数但换个数据集就不知道怎么改。2.4 最小可运行 Baseline一个能压到 0.15 的 Ridge跑代码之前先确认 Python 环境里有 pandas、numpy、scikit-learn 和 lightgbm。我习惯用 VSCode 跑这类竞赛源码单独开一个虚拟环境比在全局装包干净得多避免不同项目依赖互相打架。下面是最小可运行的基线方案import pandas as pd import numpy as np from sklearn.linear_model import Ridge from sklearn.model_selection import KFold from sklearn.metrics import mean_squared_error train pd.read_csv(data/train.csv) test pd.read_csv(data/test.csv) y np.log1p(train[SalePrice]) train train.drop(columns[Id, SalePrice]) test test.drop(columns[Id]) num_cols train.select_dtypes(include[np.number]).columns X train[num_cols].fillna(train[num_cols].median()) X_test test[num_cols].fillna(train[num_cols].median()) kf KFold(n_splits5, shuffleTrue, random_state42) scores [] for tr_idx, va_idx in kf.split(X): model Ridge(alpha10.0) model.fit(X.iloc[tr_idx], y.iloc[tr_idx]) pred model.predict(X.iloc[va_idx]) score np.sqrt(mean_squared_error(y.iloc[va_idx], pred)) scores.append(score) print(CV RMSLE:, round(np.mean(scores), 4))这段代码的逻辑目标 y 先 log1p 再进模型预测结果留在 log 空间所以最后算出来的分数直接就是 RMSLE。数值列统一用中位数填充注意这里刻意用训练集的中位数去填测试集而不是用测试集自己的中位数否则就在数据泄漏的边缘试探。参数说明Ridge 的 alpha 取 10 是跑这个比赛比较稳的起点后面可以做小范围网格搜索KFold 的 random_state 固定为 42是为了让每次跑出来的 CV 可复现。这个 Baseline 只用数值特征CV 大概在 0.15 上下。它存在的意义是给你一个锚点之后每加一个特征、换一个模型都拿 CV 分数来对比而不是凭感觉觉得“这个特征可能有用”。3. 特征工程才是 Top10 的护城河同样的 Ridge 为什么别人能再压 0.02很多人下载这类源码后最喜欢翻的是模型参数最想跳过的是特征处理。但在这个比赛里单靠模型很难把 CV 从 0.15 压到 0.115。我复现过不少 Top10 方案特征处理的代码量通常是模型的 3 到 5 倍。模型决定分数下限特征决定上限。3.1 合并训练集和测试集为什么不是分别清洗而是先 concat 再一起处理Top10 源码几乎都有一段 pd.concat。原因有两个硬约束。第一类别特征做 OneHot 编码时训练集和测试集如果分开做可能出现测试集的某个类别在训练集里没出现过最后列数对不上预测直接报错合并后 get_dummies 能保证两边列完全一致。第二缺失值填充、偏度计算这类统计量分开处理很容易用测试集信息去填测试集平白引入数据泄漏。train pd.read_csv(data/train.csv) test pd.read_csv(data/test.csv) ntrain train.shape[0] y np.log1p(train[SalePrice]) full pd.concat([train.drop(columns[Id, SalePrice]), test.drop(columns[Id])], axis0, ignore_indexTrue)这里刻意先 drop 掉 Id 和 SalePrice 再合并。Id 是行身份不是特征SalePrice 是目标不能让它混进特征矩阵里当泄漏源。ntrain 记住 1460 这个切分长度后面所有特征做完用 full.iloc[:ntrain] 和 full.iloc[ntrain:] 把训练和测试重新拆开。这套“先合并、后处理、再拆分”的管道是 Top10 源码里的标准操作。3.2 缺失值按语义填同一个 NaN四种填法完全不同缺失值处理是这个比赛最容易被低估的一步。同样是 NaN有的代表“缺失数据”有的代表“这栋房子没有这个东西”填法完全不同。我常用的填法整理成一张表缺失列缺失的真实含义填法理由PoolQC / Fence / Alley / FireplaceQu没有泳池 / 栅栏 / 胡同 / 壁炉填字符串 None缺失不是未知是“没有”GarageCars / GarageArea / BsmtFinSF1没有车库 / 地下室面积就是 0填 0数值为 0 符合物理含义LotFrontage临街宽度未知按 Neighborhood 分组取中位数同一个小区的房子宽度更接近MSZoning区域分类缺失填众数类别型数据用众数最稳对应代码cat_fill_none [PoolQC, MiscFeature, Alley, Fence, FireplaceQu, GarageType, GarageFinish, GarageQual, GarageCond, MasVnrType] for col in cat_fill_none: full[col] full[col].fillna(None) num_fill_zero [MasVnrArea, GarageYrBlt, GarageArea, GarageCars, BsmtFinSF1, BsmtFinSF2, BsmtUnfSF, TotalBsmtSF, BsmtFullBath, BsmtHalfBath] for col in num_fill_zero: full[col] full[col].fillna(0) full[LotFrontage] full.groupby(Neighborhood)[LotFrontage].transform( lambda x: x.fillna(x.median()))参数说明fillna(None) 填的是字符串后面 get_dummies 会把它变成一个独立类别语义上等于“没有泳池”。填 0 而不是填均值是因为“没有车库”不能被“平均拥有 1.2 个车位”替代均值填充会让回归模型在车库这件事上产生幻觉。LotFrontage 用 groupby transform 而不是直接全局中位数因为同一个街区的临街宽度更相关。3.3 组合特征要放在 log 变换之前做TotalSF、OverallScore 和 RemodelAge组合特征必须在 log 变换之前构造。原因是 log1p 是单调变换但它会改变特征之间的线性关系如果先对 TotalBsmtSF 做 log 再相加得到的 TotalSF 不再是物理总面积的 log而是两个 log 的加和语义彻底歪掉。full[TotalSF] full[TotalBsmtSF] full[1stFlrSF] full[2ndFlrSF] full[OverallScore] full[OverallQual] * full[OverallCond] full[RemodelAge] full[YearRemodAdd] - full[YearBuilt] full[IsRemodeled] (full[RemodelAge] 0).astype(int)TotalSF 把地上地下生活面积压成一个维度比模型自己去拼三个高度相关的面积更省自由度。OverallScore 把整体等级和条件两个 1 到 10 的离散分相乘表达“又大又新还维护好”这层交互含义。RemodelAge 是改造年份减建造年份等于 0 表示从没翻新大于 0 表示有翻新记录比直接把两个绝对年份丢给模型更能表达“房子被动过”这件事。事实上年份类特征是这里最容易翻车的地方。YearBuilt 是 0 到 2000 多的大数值模型容易把它当连续值线性处理但它本质上是离散的“建造批次”。组合成 RemodelAge 之后这个信息才真正能被树模型利用。3.4 右偏数值特征做 log1p、类别特征 OneHot编码顺序也是参数看完偏度再决定哪些特征做 lognum_cols full.select_dtypes(include[np.number]).columns skewed full[num_cols].skew().sort_values(ascendingFalse) skewed_cols skewed[skewed 0.75].index.tolist() for col in skewed_cols: if (full[col] 0).all(): full[col] np.log1p(full[col])skew 大于 0.75 是我常用的经验阈值小于这个值的特征 log 前后分布差别不大做了反而增加理解成本。必须满足列内全部大于 0 才做否则填过 0 的列会出现 log(1)0 的塌缩把“没有地下室”和“地下室极小”混为一谈。类别列要单独处理。MSSubClass 看起来是 20、30、60 这类数字实际是房屋结构类型编号数字之间没有大小关系必须转字符串再 OneHot。OverallQual 虽然也是数字但它是 1 到 10 的有序等级可以直接当连续特征转成字符串反而丢掉顺序信息。这一项属于可调参数两种方案可以都跑一版留 CV 小的。for col in [MSSubClass, OverallQual, OverallCond, MoSold]: full[col] full[col].astype(str) full pd.get_dummies(full, dummy_naFalse)dummy_naFalse 是因为前面的缺失值已经填成了 None这里不能再把 NaN 额外抽成一类否则就产生了两个表示“没有”的类别模型会困惑。做完这一步full 的列数会从 79 个原始列膨胀到两百多个 OneHot 列这是正常现象。最后按之前记下的ntrain 拆回训练集和测试集X_train full.iloc[:ntrain].reset_index(dropTrue) X_test full.iloc[ntrain:].reset_index(dropTrue) y_train y.reset_index(dropTrue)4. 模型与集成把 CV 从 0.13 压到 0.11 的最后一公里特征工程把 Ridge Baseline 的 CV 从 0.16 拉到 0.13 左右再往下走就不是单纯加特征能解决的了。公开榜上 Top10 方案几乎都是集成两个以上 GBDT 家族模型与正则化线性模型做加权或 Stacking。单 Lasso 能到 0.12x单 LightGBM 也差不多但把它们按比例混在一起往往能再压 0.005 到 0.01。0.005 在 RMSLE 上看起来不起眼在这个榜单上就是几十名的差距。4.1 模型池怎么搭Lasso、Ridge、ElasticNet 加 XGB、LGBM、CatBoost这个比赛的数据量只有 1460 行深度神经网络基本没有生存空间主流武器是正则化线性模型和 GBDT。选它们的理由很直接线性模型对 OneHot 后的稀疏特征友好训练稳定GBDT 能自动捕捉非线性交互对领域知识要求低。模型类型在这个比赛里的定位参数起点Lasso / Ridge / ElasticNet正则化线性吃 OneHot 后的几百列特征做基线alpha 0.0005 到 0.01XGBoostGBDT非线性、特征交互、自带缺失处理eta 0.02n_estimators 2000LightGBMGBDT快但容易在小数据上过拟合要限制深度num_leaves 32lr 0.01CatBoostGBDT支持直接吃原始类别字符串depth 6lr 0.03以 Lasso 为例先把单模型跑通from sklearn.linear_model import Lasso model_lasso Lasso(alpha0.001, random_state42) model_lasso.fit(X_train, y_train)alpha0.001 不是随便给的。对 OneHot 后的特征矩阵alpha 在 0.0005 到 0.01 之间分数差别不大超过 0.1 就会明显欠拟合。先按 0.001 跑通再微调。4.2 回归任务也能分层pd.qcut 把 y 切成 10 桶再用 StratifiedKFold这个比赛大部分公开方案用 KFold(n_splits10)但也有不少 Top10 源码用的是分层抽样。注意这是回归任务不能直接用 StratifiedKFold 套在连续 y 上所以要先对 y 做分位切分from sklearn.model_selection import StratifiedKFold import pandas as pd y_bin pd.qcut(y_train, q10, labelsFalse) skf StratifiedKFold(n_splits10, shuffleTrue, random_state42) folds list(skf.split(X_train, y_bin))pd.qcut 把 log 后的房价从低到高切成 10 等份每折里有相同比例的豪宅和刚需房。普通随机 5 折很可能把高房价房子全部放进同一折那一折的 CV 就会被豪宅主导分数来回跳。分层能稳住每折的房价分布这是本地分数能稳定复现的前提。我这里用的是 log 之后的目标做分位比原始价格更贴合 RMSLE 的误差定义。4.3 加权融合为什么 Lasso 和 LGBM 混在一起能再压 0.005先写一个通用的 K 折训练函数返回 OOF 预测和测试集预测import numpy as np from sklearn.model_selection import KFold from sklearn.metrics import mean_squared_error def run_kfold(model, X, y, n_splits10): kf KFold(n_splitsn_splits, shuffleTrue, random_state42) oof np.zeros(len(X)) pred_test np.zeros(len(X_test)) 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]) pred_test model.predict(X_test) / n_splits cv np.sqrt(mean_squared_error(y, oof)) return oof, pred_test, cv逻辑说明每个 fold 在训练子集上拟合同时预测验证集和完整测试集测试集预测取 10 折平均相当于自带一层 bagging。OOF 预测之后有两个用途一是直接算集成后的 CV二是作为 Stacking 第二层的输入特征。所有预测都留在 log 空间。调用方式oof_lasso, pred_lasso, cv_lasso run_kfold( Lasso(alpha0.001, random_state42), X_train, y_train )注意 KFold 里的 random_state 和 Lasso 里的 random_state 都固定成一个值否则每次跑出来 CV 波动会超过 0.003没法判断特征或参数改动到底有没有效。LGBM 同样跑一份参数用小学习率配大树数import lightgbm as lgb model_lgb lgb.LGBMRegressor( n_estimators2000, learning_rate0.01, num_leaves32, max_depth5, subsample0.8, colsample_bytree0.8, random_state42, verbose-1 ) oof_lgb, pred_lgb, cv_lgb run_kfold(model_lgb, X_train, y_train)LGBM 参数说明1460 行的小数据上n_estimators 要开大配合低学习率避免单棵树步子迈太大num_leaves32 算是小项目配置叶子再多就会往噪声里钻。subsample 和 colsample_bytree 各 0.8是在样本和特征两个维度注入随机性给后面的集成分出差异化。然后手工搜一下两模型的最优加权比例best_score 9.9 best_w 0 for w in np.arange(0, 1.01, 0.05): blend w * oof_lgb (1 - w) * oof_lasso score np.sqrt(mean_squared_error(y_train, blend)) if score best_score: best_score score best_w w print(flgb{best_w:.2f}, lasso{1 - best_w:.2f}, CV{best_score:.4f})这个循环每次算 1460 维向量的 RMSE20 个组合只是一瞬间。注意调权重用的分数来自 OOF不是来自测试集否则等于拿榜单分数调参属于典型的数据泄漏。在这个比赛里LGBM 和 Lasso 的相关性不算高融合后通常能压出 0.003 到 0.008 的提升。如果两个模型 OOF 相关性极高集成收益就会很小这是正常现象不用迷信“模型越多越好”。Stacking 我会在加权之后才考虑因为第二层回归模型在小样本上更容易过拟合加权可控、可解释、不容易翻车。4.4 提交前把预测还原回原始价格融合后的预测仍在 log 空间提交前必须 np.expm1 还原sub pd.DataFrame({ Id: test[Id], SalePrice: np.expm1(0.35 * pred_lgb 0.65 * pred_lasso) }) sub.to_csv(submission.csv, indexFalse)如果预测值里有负的 log 价格expm1 会还原出负的房价提交直接判 0 分。所以提交前先看一眼 sub[SalePrice].min()低于 0 就得回头查模型预测里有没有出现极端负值。5. 源码落地避坑手册五个本地一定会上演的翻车现场5.1 现象pd.concat 报错 “Columns overlap but no suffix specified”原因train.csv 和 test.csv 在读入之后有一边的列被提前改过。最常见的是有人先 drop 了 train 的 SalePrice顺手把 Id 也 drop 了但 test 那边还留着 Id更隐蔽的是在某个特征工程函数里只对训练集加了列测试集没加。合并时列名对不上pandas 就会抛出这个错误。解决在 concat 之前打印两边 shape 和列名校验一次assert list(train.columns) list(test.columns)这个比赛的特征列两边完全一致不一致就是前面某一步写错了。把 assert 放在 concat 前能提前拦掉绝大多数列错位问题。5.2 现象本地 CV 0.12提交上 LB 分 0.6原因log 空间和原始空间混着用了。常见翻车位置有两处一是把验证集上的 OOF 预测直接当成提交价格忘了它是 log 值二是 Lasso 这类线性模型对 log 目标预测出负值expm1 之后出现负数房价RMSLE 直接烂掉。解决提交前检查 submission.csv 的 SalePrice 是否全部大于 0再做一个最小自检把本地验证集的预测 expm1 后和真实价格画散点图斜率明显偏离 45 度线就说明空间没对齐。5.3 现象Leaderboard 显示 score is NaN原因提交文件行数不等于 1459或者 Id 顺序对不上。很多人最后提交时直接取了预测数组的前几行没跟着 test 的 Id 走。sample_submission.csv 里的 Id 并不是顺序排列的不能想当然按行号拼。解决最终提交统一用 test[Id] 作为主键去 merge 预测值不要用行号。代码里的 sub 直接用 Id 列不要 reset_index 后当成 Id 用。5.4 现象同一份源码今天跑 0.118明天跑 0.121原因随机种子没固定。LightGBM 的多线程 shuffle、KFold 的随机切分任何一个环节的随机性都会在 1460 行的小数据上被放大成 0.003 的波动。解决源码里所有 random_state 统一成同一个值KFold 的 shuffle 也固定如果用了 GPU 推理把线程数和随机种子都钉死。另外要说清楚本地 CV 和 LB 差 0.003 到 0.005 在这个比赛里是正常现象不是代码错了是抽样波动。如果 LB 每次都比 CV 稳定低 0.005 以上那才需要回头查数据泄漏。5.5 现象Kaggle 新账号注册卡在验证码数据下不下来原因这类问题九成是浏览器插件拦截了验证码脚本或者当前窗口被识别成自动化环境和代码本身没关系。解决换 Chrome 普通窗口关掉广告拦截类扩展重新加载注册页。验证码如果一直不出现等几分钟刷新或者换一个浏览器内核再试。比赛数据本身就在 Kaggle 竞赛官网的 Data 页签不需要额外工具就能下载。下载完记得把 train.csv、test.csv、sample_submission.csv 按源码里的相对路径放好。6. 从复现到超越三个能立刻提高方案分数的验证技巧6.1 提交前自检五条 assert 守住最后防线任何一份 Top10 源码跑通之后都要自己检查提交文件。我的习惯是固定一段自检脚本sub pd.read_csv(submission.csv) assert sub[Id].nunique() 1459 assert sub[SalePrice].notna().all() assert (sub[SalePrice] 0).all() assert sub[SalePrice].max() 1e7 print(submission ok)前面三行分别检查行数唯一、无空值、全是正数第四个是对房价数量级的粗筛。这几个断言在分数极差的时候能帮你快速区分是模型问题还是文件问题而不是对着一个 0.6 的分数干瞪眼。6.2 新特征有没有用只看 CV 变化别看 LB判断一个特征该不该留标准只有一个加了它CV 降不降。base_cv run_kfold(model_lgb, X_train, y_train)[2] new_cv run_kfold(model_lgb, X_new, y_train)[2] if base_cv - new_cv 0.001: print(值得保留) else: print(删掉省得给模型添乱)在 1460 行数据上CV 差异小于 0.0005 基本就是噪声不用当真。一次只加一个特征加完立刻对比不要一口气丢五个进去然后一锅炖。我见过的血泪经验是特征做得多的人最后往往说不清哪个特征真正贡献了分数就是因为没有一次一验。6.3 用好 OOF 预测它是你最快的验错工具OOF 值比测试集预测值诚实得多。拿到 OOF 后别只算一个 CV 分数就扔了把真实 y 和 OOF 的残差按房价区间切开来看看。如果豪宅区间残差系统性偏大说明模型对尾部样本拟合不足优先去补总面积、地段相关特征而不是盲目调参。这套诊断方法比盯着 LB 猜问题高效得多。我现在拿到任何一份竞赛源码第一件事是打开它的 OOF 循环第二件事是找目标列有没有做 log1p第三件才是看模型参数。这个顺序帮我避开了绝大多数跑不通和分数对不上的坑。希望帮到你。本文还有配套的精品资源点击获取