news 2026/10/2 4:42:50

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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. 数据读取与标签平滑:为什么第一步就要做log1p

2.1 用index_col=0读数据,并合并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_col=0) test_df = pd.read_csv('house price/input/test.csv', index_col=0) print(train_df.head())

index_col=0的意思是让CSV的第一列(也就是Id列)成为DataFrame的行索引,而不是被当成一个普通特征读进来。这有两个实际好处:一是后续做pd.concat((train_df, test_df), axis=0)合并时,行索引不会出现重复错位;二是在特征工程做完之后,可以靠.loc[train_df.index]干净地把训练集和测试集切回去,这个操作在后面会反复用到。

跑完head()之后,建议顺手看一眼train_df.info()和train_df.describe(),确认哪些列是object类型、哪些列的缺失值比例异常。这一步不用花太多时间,但能让你心里有数:哪些地方需要人为处理,哪些列可以直接交给模型。

数据合并这一步,原代码做得非常果断:

# 把train和test合并成一个DF,预处理完再分开 all_df = pd.concat((train_df, test_df), axis=0) 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(x+1),避免x=0时出现负无穷 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是为了避免x=0时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'], prefix='MSSubClass').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就是20,60就是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(ascending=False).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、方差1,Ridge的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_col=0留下的索引。loc[train_df.index]会把合并前的那些行原封不动地挑出来,不会出现行错位的灵异事件。如果你读数据的时候没用index_col=0,到这里就只能靠行号硬切,一旦中间做过排序或删行,就很容易翻车。

转成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, cv=10, scoring='neg_mean_squared_error')) test_scores.append(np.mean(test_score)) # plt.plot(alphas, test_scores) # plt.title("Alpha vs CV Error") # plt.show() # 可见大概alpha=10~20的时候,可以把score达到0.135左右

np.logspace(-3, 2, 50)是从10的-3次方到10的2次方之间均匀取50个数,覆盖了0.001到100这个跨度。因为alpha的合适量级往往跨了好几个数量级,用线性等间隔扫效率很低,logspace能让小值和大值都照顾到。

这里的scoring='neg_mean_squared_error'是个老手才知道的细节:sklearn的交叉验证是“分数越大越好”,而MSE是“越小越好”,所以sklearn把它包装成负的MSE。直接取MSE你会得到一堆负数,别慌,加个负号再开根号才是真正的RMSE。

Ridge的优势在于它是线性模型,在80+维特征上训练极快,而且能处理特征之间的共线性。这个数据集里很多特征本来就相关(比如面积和房间数),Ridge的L2正则化能把权重压住,不让模型被共线性带偏。

4.3 RandomForest:max_features比树的数量更值得调

第二个模型是随机森林回归。原代码的调参重点放在max_features上,这和大多数人的习惯不太一样:

max_features = [.1, .3, .5, .7, .9, .99] test_scores = [] for max_feat in max_features: clf = RandomForestRegressor(n_estimators=200, max_features=max_feat) test_score = np.sqrt(-cross_val_score(clf, X_train, y_train, cv=5, scoring='neg_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_estimators=200是树的棵数。在CV阶段用200棵已经能看出趋势,最终提交前再加大到500。树的数量其实有边际递减效应——从200加到500,精度提升很有限,但训练时间翻了不止一倍。原代码最终选了n_estimators=500, 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(alpha=15) rf = RandomForestRegressor(n_estimators=500, 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_col=0在这时候又救了一次场——你不需要额外读原始CSV去对齐Id,test_df的index就是标准答案。

提交前的自检脚本我建议固定成三步:打印submission_df.describe()看SalePrice的量级和范围、确认Id数量和test_df一致、把预测结果直方图画出来和训练集聚类对比。这三步都过了,这份提交基本就稳了。

这套流程跑完之后,我最大的一个教训是:在拿到数据的前30分钟里,决定整个项目上限的不是模型选得多花哨,而是你对数据和缺失值的理解够不够深。从那以后我每次做这类带log变换的回归,都会强制走一遍“分布检查 → log1p → 合并预处理 → 建模 → expm1还原”的闭环,在提交前再花5分钟确认标签量级。这套习惯帮你省下的返工时间,远比你调出一个新参数省得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 4:41:54

通信原理大作业实战:MATLAB QPSK仿真从链路搭建到报告输出

简介:西安电子科技大学2023年通信原理课程大作业,围绕第四代移动通信技术(4G)展开系统综述,内容涵盖4G网络概念界定、关键技术要求、九大主要特点、对通信产业的深远影响,以及对5G未来演进的展望。全文采用…

作者头像 李华
网站建设 2026/10/2 4:39:26

Lombok @Accessors链式调用与fluent模式实战指南

1. 为什么你写的DTO还在手写getter/setter?Accessors不是“语法糖”,而是Java对象建模的效率分水岭我第一次在团队代码里看到Accessors(chain true)是三年前,当时正为一个电商订单系统重构DTO层——每天要改十几个VO、DTO、BO类,…

作者头像 李华
网站建设 2026/10/2 4:39:25

RTX 4090实战:27B三元量化模型本地部署与性能调优全记录

最近把一台 RTX 4090 从游戏机改造成了正经的推理工作站,折腾的主角是Ternary-Bonsai-2-27B(PTQ1_0)这个 27B 参数的模型。说实话,第一次看到"PTQ1_0"这种量化标识我也愣了一下,等到真正跑起来才发现,这套方案就是为了解…

作者头像 李华
网站建设 2026/10/2 4:39:10

8GB显存跑35B大模型:量化、CPU Offload与内存分配的实战记录

拿一张 8GB 显存的消费级显卡去跑 35B 参数规模的大模型,放两年前说出去会被当成玩笑。全精度 35B 模型光权重就要 70GB 显存,顶配 A100 都吃紧。但到了本地大模型工具链逐步成熟的今天,我实测下来,RTX 4060 8GB 配上 32GB 内存&a…

作者头像 李华
网站建设 2026/10/2 4:39:07

AI研报生成全流程复盘:从提示词框架到多智能体编排

前阵子我在系统梳理AI智能体落地软件研发的线索,连着看了十几份券商、咨询公司和科技媒体的研报。真正让我反复停下来记笔记的,不是某家大行明星分析师的年度策略,而是一份明显由AI辅助生成的研究报告。这句话说出来有点反常识,因…

作者头像 李华
网站建设 2026/10/2 4:38:05

Harness桌面端实战指南:AI工作流编排与模型接入避坑经验

1. 先搞清楚一件事:Harness桌面端到底解决什么问题那几天我正被一堆Agent任务搞得头大,频繁在终端和网页之间来回切换调试工作流。结果翻DeepSeek官方更新时,突然看到多了一个以前没有的入口,点进去发现是Harness桌面端安装包&…

作者头像 李华