ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Kaggle房价预测实战:高维数据特征工程与Ridge/随机森林融合

Kaggle房价预测实战:高维数据特征工程与Ridge/随机森林融合 简介房价预测是Kaggle的高频赛题这份PDF资源围绕高维数据下的分类/回归问题展开以Stacking思想为主线面向希望系统掌握数据预处理与模型融合实战流程的机器学习初学者和竞赛玩家。资源共1个PDF文件压缩包仅131KB虽小巧但浓缩了完整实战笔记从读取train.csv/test.csv、对数化标签平滑分布、合并数据集到独热编码、缺失值填充、特征标准化前后步骤均有代码片段和输出说明同时结合Ridge与RandomForestRegressor进行交叉验证阐述如何将多个模型预测结果作为新特征参与Stacking训练以汲取多种模型优点。作者在PDF中提供了可运行的代码思路并注明完整代码和数据集存放于GitHub便于读者对照复现。已有1067人学习/下载适合需要快速上手Kaggle房价预测、理解高维特征处理与模型堆叠技巧的学习者。1. 高维数据房价预测实战这份Kaggle笔记能帮你少走多少弯路拿到Kaggle的House Prices房价预测数据集时第一反应通常是懵的训练集80个特征、1460行测试集还有1459行里面混杂着数值变量、类别变量、大量缺失值SalePrice的分布还严重右偏——这不是教材里波士顿房价那种13个特征的小玩具而是真正的高维数据回归场景。这篇实战笔记没有堆砌花哨模型而是老老实实走完了读数据、合并预处理、特征工程、Ridge和RandomForest分别交叉验证调参、最后用平均化的方式把两个模型的预测结果叠起来——也就是用Stacking的思维取两种模型的优点。它适合刚跑通sklearn、第一次面对高维回归数据集的人照着复现一遍也适合想看看老手怎么处理缺失值和One-Hot的人。2. 数据读取与标签平滑为什么第一步就要做log1p2.1 用index_col0读数据并合并train/test两份表原代码的第一步很朴素但有一个细节值得先说清楚import numpy as np import pandas as pd from sklearn.linear_model import Ridge from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestRegressor # 一般来说源数据的index那一栏没什么用 # 可以直接拿它当pandas dataframe的index之后检索也省事儿 train_df pd.read_csv(house price/input/train.csv, index_col0) test_df pd.read_csv(house price/input/test.csv, index_col0) print(train_df.head())index_col0的意思是让CSV的第一列也就是Id列成为DataFrame的行索引而不是被当成一个普通特征读进来。这有两个实际好处一是后续做pd.concat((train_df, test_df), axis0)合并时行索引不会出现重复错位二是在特征工程做完之后可以靠.loc[train_df.index]干净地把训练集和测试集切回去这个操作在后面会反复用到。跑完head()之后建议顺手看一眼train_df.info()和train_df.describe()确认哪些列是object类型、哪些列的缺失值比例异常。这一步不用花太多时间但能让你心里有数哪些地方需要人为处理哪些列可以直接交给模型。数据合并这一步原代码做得非常果断# 把train和test合并成一个DF预处理完再分开 all_df pd.concat((train_df, test_df), axis0) print(all_df.shape)合并的理由很直接训练集和测试集的特征分布大概率是同一套逻辑生成的如果分别在两份数据上做fillna、get_dummies很可能出现训练集有12个category、测试集只有11个的尴尬局面。合并后统一处理能保证One-Hot生成的列完全一致这是后面模型能正常fit和predict的前提。注意合并的只是特征部分目标变量SalePrice不能混进去。测试集没有SalePrice训练集的SalePrice要提前拿出来单独存。2.2 标签右偏是回归的隐形杀手log1p把它拉回正态这一步是很多人会漏掉的原代码作者也特地强调过直接用原始SalePrice去训练CV分数很难达到理想水平。# 先看看SalePrice长什么样 prices pd.DataFrame({price: train_df[SalePrice], log(price 1): np.log1p(train_df[SalePrice])}) # prices.hist() # 打开看一下分布对比 # 用log1p平滑标签也就是log(x1)避免x0时出现负无穷 y_train np.log1p(train_df.pop(SalePrice)) print(y_train.head())为什么要把标签做log变换因为SalePrice的分布严重右偏——大部分房子在10万到20万美元区间但少数豪宅能冲到50万以上。线性回归和基于MSE的模型对极端值极其敏感一个50万的样本在误差计算里的权重可能抵得上几十个普通样本。取log之后分布会被压得接近正态模型学起来更稳。log1p就是log(x 1)加这个1是为了避免x0时log无定义在房价这种恒大于0的场景里它是个保险写法。更关键的是还原log变换过的预测值不能用np.exp()直接还原得用np.expm1()也就是exp(y) - 1。原代码里有一句话总结得很到位“按照‘怎么来的怎么去’原则log1p就需要expm1”。这个对应关系在最后提交的时候就是你的后悔药。我一般会顺手把变换前后的分布图打出来对比一下确认log之后确实接近正态了再继续省得后面白跑一轮。这一步不折腾数据折腾的是你的判断力。3. 特征工程三步走类型修正、One-Hot编码与缺失值填充3.1 MSSubClass这类数字型类别必须先转成字符串特征工程的第一步是把数据类型搞对。原代码专门拿MSSubClass举例这是个非常典型的坑# all_df[MSSubClass].dtypes # 默认会被pandas记成数字类型int64不信可以自己打印看看 all_df[MSSubClass] all_df[MSSubClass].astype(str) # 转成string # all_df[MSSubClass].value_counts()MSSubClass是建筑的住房类型分类取值是20、30、40、60、70这些数字但它本质上是类别不是数值。如果放任它保持int类型pandas和sklearn会把它当成有序数值——模型会认为60比40“大”、70比50“大两倍”这种虚假的大小关系会污染回归模型的权重分配。转成string之后后面的get_dummies才能把它按类别处理。这里有个判断标准我一直在用如果一列数字的加减乘除没有实际业务含义那它就应该被当成object。不止MSSubClass像OverallQual这种评分列虽然也是整数但它有明确的大小含义1到10的评分就可以保留为数值。这个区分做对了特征工程就成功了一半。3.2 get_dummies做One-Hot为什么category不能直接用数字把类别列转成string之后下一步就是用One-Hot编码把它们变成模型能吃的形式。原代码直接用了pandas自带的get_dummies# pd.get_dummies(all_df[MSSubClass], prefixMSSubClass).head() # 这一步会把MSSubClass拆成12列每个category一列是就是1不是就是0 # 对所有的category数据统一做One-Hot all_dummy_df pd.get_dummies(all_df) print(all_dummy_df.head())One-Hot的原理不复杂假设MSSubClass有12个取值那就拆成12个新列每列对应一个取值样本属于哪个取值就在哪列写1其余写0。这样做的好处是彻底消除了“数字大小”带来的顺序误导——20就是2060就是60它们之间没有任何大小关系。pd.get_dummies(all_df)默认会把所有object类型的列都做One-Hot数值列原样保留。这也是为什么上一步必须把MSSubClass先转成str——如果你忘了转get_dummies就不会动它模型依然会把它当成连续数值。注意get_dummies不会自动处理缺失值NaN会在所有类别列里都写0。所以One-Hot之后的缺失值处理是另一个独立问题见下一节。3.3 缺失值均值填充与数值列标准化高维数据里缺失值几乎是必然出现的。原代码的处理方式很直接# 看看哪些列缺失最多 # print(all_dummy_df.isnull().sum().sort_values(ascendingFalse).head(10)) # 缺失最多的列是LotFrontage # 用每列的均值填掉所有空缺 mean_cols all_dummy_df.mean() all_dummy_df all_dummy_df.fillna(mean_cols) # 再检查一遍确保没有遗漏 print(all_dummy_df.isnull().sum().sum()) # 输出0用均值填充是最省事也最常用的做法。对数值型缺失均值填充不会改变该列的整体均值而且在不了解业务背景的情况下它比删行、填0都更稳妥。LotFrontage是临街长度如果某套房子的临街数据缺失用整个数据集的平均值去顶替虽然不精确但至少不会引入极端值。这里我想提醒一句填缺失值之前最好看一眼数据集的说明文档data_description.txt很多缺失是有业务含义的。比如PoolQC泳池质量缺失往往代表这房子没有泳池填0或填“None”才是对的填均值反而失真。原代码用的是通用的均值填充属于“不知道背景时最安全的默认方案”你要是有时间看文档可以做更精细的处理。接下来是标准化这一步是否必要取决于你用的模型但原代码的做法值得学# 找出哪些列是真正的numerical没被get_dummies处理的 numeric_cols all_df.columns[all_df.dtypes ! object] # print(numeric_cols) # 只对numerical列做标准化(X - mean) / std numeric_col_means all_dummy_df.loc[:, numeric_cols].mean() numeric_col_std all_dummy_df.loc[:, numeric_cols].std() all_dummy_df.loc[:, numeric_cols] (all_dummy_df.loc[:, numeric_cols] - numeric_col_means) / numeric_col_std这里最值得注意的操作是最后一行all_dummy_df.loc[:, numeric_cols] ...。它只更新numeric_cols这些列完全不会碰One-Hot出来的那些0/1列。你要是把整个DataFrame一股脑标准化One-Hot的0/1也会被挪动变成负数和大于1的浮点数那就彻底毁了——0/1编码必须保持原样这一点不能妥协。标准化对Ridge这类线性模型尤其重要因为这些模型对特征的量纲很敏感。面积是几百平米、年份是2000左右、One-Hot是0/1这些量级混在一起正则化项会失衡。标准化之后每个数值特征都变成均值0、方差1Ridge的alpha才能公平地作用于所有特征。RandomForest这种树模型不怕量纲但标准化做了也不会有坏处。4. 模型训练与调参Ridge和RandomForest的交叉验证对比4.1 切回训练/测试集把DataFrame转成Numpy数组特征工程做完终于可以建模了。第一步是把合并的DataFrame按index切回两份# 把数据集分回训练/测试集 dummy_train_df all_dummy_df.loc[train_df.index] dummy_test_df all_dummy_df.loc[test_df.index] # print(dummy_train_df.shape, dummy_test_df.shape) # 转成numpy array和sklearn的接口更配 X_train dummy_train_df.values X_test dummy_test_df.values这一步之所以能顺利切回去靠的就是第2章里index_col0留下的索引。loc[train_df.index]会把合并前的那些行原封不动地挑出来不会出现行错位的灵异事件。如果你读数据的时候没用index_col0到这里就只能靠行号硬切一旦中间做过排序或删行就很容易翻车。转成numpy array不是必须的sklearn的接口直接吃DataFrame也行但转成array之后可以避免一些pandas索引对齐的隐式行为速度也略快一些。这一步没有参数要调属于习惯性操作。4.2 Ridge用logspace扫alpha看CV误差曲线第一个模型是Ridge回归。原代码的思路很清晰先扫alpha找到交叉验证误差最小的点再拿这个参数做最终模型。# 用Sklearn自带的cross validation方法来测试模型 alphas np.logspace(-3, 2, 50) test_scores [] for alpha in alphas: clf Ridge(alpha) test_score np.sqrt(-cross_val_score(clf, X_train, y_train, cv10, scoringneg_mean_squared_error)) test_scores.append(np.mean(test_score)) # plt.plot(alphas, test_scores) # plt.title(Alpha vs CV Error) # plt.show() # 可见大概alpha10~20的时候可以把score达到0.135左右np.logspace(-3, 2, 50)是从10的-3次方到10的2次方之间均匀取50个数覆盖了0.001到100这个跨度。因为alpha的合适量级往往跨了好几个数量级用线性等间隔扫效率很低logspace能让小值和大值都照顾到。这里的scoringneg_mean_squared_error是个老手才知道的细节sklearn的交叉验证是“分数越大越好”而MSE是“越小越好”所以sklearn把它包装成负的MSE。直接取MSE你会得到一堆负数别慌加个负号再开根号才是真正的RMSE。Ridge的优势在于它是线性模型在80维特征上训练极快而且能处理特征之间的共线性。这个数据集里很多特征本来就相关比如面积和房间数Ridge的L2正则化能把权重压住不让模型被共线性带偏。4.3 RandomForestmax_features比树的数量更值得调第二个模型是随机森林回归。原代码的调参重点放在max_features上这和大多数人的习惯不太一样max_features [.1, .3, .5, .7, .9, .99] test_scores [] for max_feat in max_features: clf RandomForestRegressor(n_estimators200, max_featuresmax_feat) test_score np.sqrt(-cross_val_score(clf, X_train, y_train, cv5, scoringneg_mean_squared_error)) test_scores.append(np.mean(test_score)) # plt.plot(max_features, test_scores) # plt.title(Max Features vs CV Error) # plt.show() # 用RF的最优值达到了0.137左右max_features控制每棵决策树随机挑选多少个特征参与分裂。取0.3的意思是每次分裂只看30%的特征这个随机性让森林里的树长得各有差异降低树与树之间的相关性从而提升整体泛化能力。如果这里设成1.0那和一堆“克隆树”没什么区别bagging的意义就失去了。n_estimators200是树的棵数。在CV阶段用200棵已经能看出趋势最终提交前再加大到500。树的数量其实有边际递减效应——从200加到500精度提升很有限但训练时间翻了不止一倍。原代码最终选了n_estimators500, max_features.3这个组合的RMSE在0.137左右和Ridge的0.135非常接近。两个模型分数接近是个好信号说明它们各自抓住了数据中的不同模式Ridge擅长捕捉线性趋势RandomForest擅长捕捉非线性交互。这为下一步的融合提供了基础。5. 避坑指南高维回归最容易翻车的五个实操细节5.1 直接用原始SalePrice训练CV分数永远上不去现象跑了半天交叉验证RMSE在0.4甚至0.5以上怎么调参都降不下来。原因SalePrice的分布严重右偏少数豪宅样本在MSE计算中权重过大模型把大量精力花在拟合极端值上普通房子的预测反而不准。解决对标签做log1p变换后再训练用expm1还原预测值。这是整个流程里性价比最高的一步很多人一开始嫌麻烦直接跳过最后发现CV分数怎么都追不上kaggle前排回头看全是这个原因。我一般先把分布画出来右偏明显就直接log不用犹豫。5.2 独热编码后把0/1列也标准化模型输出变怪异现象做完标准化后Ridge的RMSE明显恶化甚至出现负的预测值。原因标准化操作作用到了整张DataFrame上包括One-Hot生成的0/1列。0/1列被减去均值再除以标准差之后变成了负数和大于1的小数编码语义被彻底破坏模型拿到的特征含义已经不是“是/否”了。解决标准化之前先筛出真正的数值列只对all_df.columns[all_df.dtypes ! object]筛出来的列动手。原代码里用的numeric_cols方案就是这个思路别图省事对整个DataFrame做标准化。5.3 预处理分别在train和test上做One-Hot列数对不上现象训练集有290列测试集只有287列模型predict时报维度不匹配。原因train和test各自调用了get_dummies而某个类别只在测试集里出现或者训练集里的某个类别在测试集里全部缺失导致生成的列集合不一致。解决先把train和testconcat成all_df在合并后的DataFrame上统一做One-Hot和缺失值填充处理完再用loc[train_df.index]和loc[test_df.index]切回去。这是原代码第2章的核心思路能让train/test的特征列完全对齐省掉后面一堆对齐工作。5.4 预测完忘记expm1还原提交结果全偏现象提交到kaggle之后分数奇差本地CV的RMSE是0.135线上却完全对不上。原因训练时用的是log1p(SalePrice)模型输出的预测值是log域的量大概在10到14之间不是真实房价。没做还原就提交相当于交了一份“对数价格”上去。解决预测完统一走np.expm1()还原。我自己的习惯是提交前打印submission.head()看看SalePrice列的量级是不是在几万到几十万之间如果是十几这种量级那肯定忘了还原。5.5 填缺失值不看业务背景该填0的填成了均值现象某特征缺失率很高但填均值之后特征重要性排名出现异常模型表现反而变差。原因有些列如PoolQC、Alley的缺失代表“没有这个东西”不是“值丢了”。用均值填充等于给所有没泳池的房子编造了一个“平均泳池质量”制造了大量虚假信息。解决花10分钟读一下data_description.txt区分“缺失 没有”和“缺失 数据丢失”两种场景。前者填0或填“None”后者才能用均值/中位数填充。原代码是通用均值方案作为基线没问题但想往上提分这个细节值得单独抠。6. 用Stacking的思维做集成平均融合两个模型的结果6.1 为什么“平均化”是Stacking的入门版本很多讲Stacking的文章一上来就摆五层结构、加meta-model容易劝退新手。原代码用了更朴素的手法——把两个模型的预测结果直接取平均# 把调参阶段找到的最优参数拿出来做成最终的model ridge Ridge(alpha15) rf RandomForestRegressor(n_estimators500, max_features.3) ridge.fit(X_train, y_train) rf.fit(X_train, y_train) # 把预测值从log域还原回真实房价 y_ridge np.expm1(ridge.predict(X_test)) y_rf np.expm1(rf.predict(X_test)) # 一个正经的Ensemble是把这群model的预测结果作为新的input再做一次预测。 # 这里用简单的方法直接平均化 y_final (y_ridge y_rf) / 2平均化为什么有效Ridge和RandomForest的误差来源不同Ridge把特征关系当线性拟合在平滑区域表现好RandomForest能捕捉非线性交互但在外推场景容易“平头”。两者的预测误差有相当一部分是不重叠的取平均之后一方的过拟合被另一方抵消整体方差下降RMSE通常能比两个单模型都低一点。严格来说完整Stacking应该把每个模型的预测结果作为新特征再交给一个meta-model比如Ridge训练一层。原代码直接用平均化是这个思路的精简版——训练成本几乎为零不需要额外的交叉验证来生成meta特征相当适合作为第一版提交。如果你后面想再进一步可以试试把y_ridge和y_rf当成两列特征用Ridge再拟合一次通常还能再挤出一两个千分点的提升。6.2 最终提交Id对齐与expm1还原# 提交结果 submission_df pd.DataFrame(data{Id: test_df.index, SalePrice: y_final}) print(submission_df.head(10))提交文件只有两列Id是测试集的行索引SalePrice是融合后的预测值。这里有两个检查点第一Id必须和test_df.index顺序一致如果前面有过排序操作这里排序会乱掉第二SalePrice是已经expm1还原过的真实价格不是log域的量。index_col0在这时候又救了一次场——你不需要额外读原始CSV去对齐Idtest_df的index就是标准答案。提交前的自检脚本我建议固定成三步打印submission_df.describe()看SalePrice的量级和范围、确认Id数量和test_df一致、把预测结果直方图画出来和训练集聚类对比。这三步都过了这份提交基本就稳了。这套流程跑完之后我最大的一个教训是在拿到数据的前30分钟里决定整个项目上限的不是模型选得多花哨而是你对数据和缺失值的理解够不够深。从那以后我每次做这类带log变换的回归都会强制走一遍“分布检查 → log1p → 合并预处理 → 建模 → expm1还原”的闭环在提交前再花5分钟确认标签量级。这套习惯帮你省下的返工时间远比你调出一个新参数省得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表