ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

机器学习实战指南:房价预测中的特征工程与模型融合

机器学习实战指南:房价预测中的特征工程与模型融合 房价预测这东西我身边但凡想入门机器学习的朋友十有八九都会拿它当第一个练手项目。你翻各大平台的热门教程也好看经典书单也好几乎都绕不开这个场景。原因很简单数据足够干净、目标足够明确、算法覆盖足够广从线性回归到树模型甚至小型神经网络都能跑而且做完之后你对“数据清洗、特征工程、模型评估”这套完整流程会有特别具体的体感。这篇文章我就拿一个很典型的房屋销售数据集来走一遍完整实战。这份数据有79个解释变量和1460条训练样本目标是你非常熟悉的回归任务预测房屋最终成交价。适合谁看呢刚学完Python和pandas、想知道机器学习完整流程怎么串起来的人或者已经跑过几个demo、但对特征工程和模型融合还不太有感觉的人。看完之后你应该能独立完成一次从原始表格到预测提交的全过程。1. 项目到底在解决什么问题目标拆解与验收标准动手写代码之前我习惯先花时间把问题和评价标准定义清楚这一步往往决定了后面所有工作的走向。1.1 本质是回归问题但和分类完全不同这里的任务非常明确输入一堆房屋属性面积、年份、地段、装修质量等输出一个连续的数值——售价。所以它是一个典型的有监督回归问题和“判断房价高不高”这种分类问题有本质区别。回归问题的核心是让预测值尽可能逼近真实值误差越小越好而不在乎分类准不准。你可能会问那这个项目的最佳实践是什么直接套一个回归模型不就行了吗当然可以跑通但真正想做好你会遇到几个典型的坎特征缺失怎么补、异常值怎么处理、偏态分布怎么校正、离散特征怎么编码、模型过拟合怎么控制。这些坎每一个都对应着一个机器学习知识点。这也是我为什么坚持拿这个项目来练手。1.2 用RMSE还是RMSLE别小看这个选择评估指标不能随便定。房价预测里最常见的两个指标是RMSE和RMSLERoot Mean Squared Logarithmic Error。很多人直接用RMSE发现问题不大但如果你做过对比就会发现RMSE对异常大的差距非常敏感比如某栋豪宅实际卖200万你预测成了100万这一条就能把整体误差拉得很难看。RMSLE先取对数再算误差相当于把大误差的权重“压扁”了更关注预测值的相对误差。我的建议是核心阶段用RMSLE来评估因为房价数据的标签 SalePrice 本身是一个右偏分布取对数之后数据更接近正态分布模型拟合起来也更稳定。如果你用Python代码来写可以这样定义import numpy as np from sklearn.metrics import mean_squared_error def rmsle(y_true, y_pred): return float(np.sqrt(mean_squared_error( np.log1p(y_true), np.log1p(y_pred) ))) # log1p 等于 log(1 x)可以避免 x0 时出现 -inf提示目标值做 log1p 变换后模型预测出来的也是对数空间的值要还原成真实价格时记得用 expm1。我见过新手在这里踩坑预测出来数值怪怪的后来发现是忘了还原。1.3 判定项目“做成了”的标准我个人会把项目拆成几个阶段每个阶段有明确的验收标准第一阶段基线能用线性回归跑通流程在验证集上RMSLE做到0.15以下。第二阶段优化特征工程做完树模型介入RMSLE降到0.12左右。第三阶段精调集成模型做简单的融合或调参RMSLE稳定在0.11以内。注意这个数值是相对经验值不同数据集划分会有浮动但大方向是对的。有了这个标准你就不容易在调参的无底洞里越走越远——很多人做着做着就变成“调参侠”调了三天参数验证集却毫无变化。其实大概率问题出在前面特征工程上而不是模型参数上。2. 数据清洗与特征工程决定上限的关键环节有一句话我特别认同特征决定了上限模型只是逼近这个上限。房价预测这种表格型数据项目特征工程的收益非常直观甚至比你换任何高级模型都明显。2.1 先从缺失值下手分组、按语义而不是无脑填0拿到数据后第一步永远是检查缺失值。这个数据集的缺失情况比较典型有的列缺失率高达90%以上比如 PoolQC游泳池质量、MiscFeature其他特征、Alley巷子类型这些列绝大多数样本是NaN代表“没有这个东西”。对这类列我建议直接当“无此特征”处理填一个特殊值或直接删掉而不是强行用均值填充。还有一些列缺失率不高但有真实含义比如 LotFrontage临街距离。它反映的是房屋到街道的长度和整体街区规划有关。我试过两种方式一种是全样本填中位数效果一般另一种是按 Neighborhood街区分组取每组中位数填充RMSLE立刻往下降了。原因也合理不同街区的临街地块形态差异大统一填一个值等于把信息抹掉了。# 按街区分组填充临街距离 data[LotFrontage] data.groupby(Neighborhood)[LotFrontage].transform( lambda x: x.fillna(x.median()) )2.2 异常值先看图再决定动不动探索性数据分析EDA阶段我必画散点图尤其是 GrLivArea地面以上居住面积和 SalePrice 的散点图。这个数据集里有一个经典问题有少数样本面积特别大4000平方英尺以上但售价却低得离谱。这种其实不太合常理很可能是特殊交易或数据异常。很多教程会让你直接删除这些样本但我的做法是先看测试集里有没有类似面积的大户型。如果测试集里有就不能简单删掉否则训练时模型没见过这个区域预测时就会瞎猜。如果测试集里没有删除倒是安全。我当时检查了测试集也有几套大户型的样本所以我没有删而是保留了它们靠后面对数变换把极端值带来的影响降下来。这是一个很容易被忽略的坑新手往往拿到数据顺手就把outlier删了结果线上分数反而变差。2.3 偏态分布和处理为什么要把价格取对数SalePrice 的分布明显右偏就是少数豪宅把尾巴拉得很长。这种分布对线性模型非常不友好因为模型要最小化平方误差会被那几个豪宅拖住。处理方法有两种思路。一是对目标值做 log1p 变换训练时预测 log(SalePrice)评估时再还原。二是对数值型特征做 Box-Cox 变换尤其那些本身也偏态的强壮特征比如面积、车库容量等。我用 sklearn 的 PowerTransformer 统一处理数值特征效果比单独取对数更好。逻辑很简单很多机器学习模型假设输入特征和目标的分布相对均匀偏态特征会放大噪声降低拟合速度。让数据“变正”之后模型更容易找到规律误差也明显减小。2.4 类别特征编码有序和无序要分开处理这个数据集里有很多分类变量但并不是所有分类变量都适合 One-Hot 编码。比如 ExterQual外饰质量本身有明确等级顺序Ex优秀 Gd良好 TA一般 Fa差。对这种有序变量用 One-Hot 会丢失排序信息我会先映射成有序数值quality_map {Ex: 5, Gd: 4, TA: 3, Fa: 2, Po: 1} data[ExterQual] data[ExterQual].map(quality_map)对于 Neighborhood、MSZoning 这种没有天然顺序的类别变量再做 One-Hot 编码或者用 Target Encoding用该类别下的平均房价做编码。Target Encoding 效果通常更好但一定要配合交叉验证使用否则容易过拟合。我在这两者之间组合使用有顺序的做有序映射无顺序的做独热编码后期再用一个简单的均值编码补充。特征数量从79个扩展到两三百个是很正常的。一个容易犯的错把 MSSubClass 这个特征当成数值型。它看着是数字例如 20、60、120但实际上代表房屋类型独栋、联排等是分类变量必须转成字符串再处理。这种“披着数值外衣的类别特征”在结构化数据里很常见你不仔细看字段描述直接丢进模型结果可能还能跑但解释性和精度都会受影响。3. 模型选型与训练从线性回归到集成模型的演进这个项目的好处之一是模型选择路径非常清晰你可以从最简单的线性回归开始一路走到随机森林、梯度提升树甚至做个简单堆叠。每一步都能看到实际效果的提升。3.1 先跑基线模型别急着上大招我通常先用线性回归跑通整个流程目的不是拿最佳成绩而是验证数据管道、编码逻辑、评估代码有没有bug。线性回归对异常值、偏态分布特别敏感如果前面没处理好R²可能为负RMSLE也很难看。但一旦数据清洗到位线性回归的表现其实不会太差因为房屋价格和面积、质量等变量之间本身就有较强的线性关系。在这个阶段我会顺手做一个10折交叉验证每次记录验证集的RMSLE最后取平均。这个平均RMSLE就是你后续所有模型提升的参照线。如果复杂模型连简单线性回归都干不过那一定是哪一步处理出问题了。3.2 正则化线性模型的必要性为什么选Ridge而不是普通线性回归普通线性回归在这个数据集上有两个问题一是特征多两三百个独热编码列容易过拟合二是特征之间存在多重共线性比如 TotRmsAbvGrd 和 GrLivArea 明显相关。这时候L2正则化比纯线性回归稳定得多。Ridge 对权重做 L2 惩罚Lasso 做 L1 惩罚还有一个弹性网络 ElasticNet 两者兼顾。实际使用中我对数值特征做了标准化然后分别跑 Ridge、Lasso、ElasticNet发现 Ridge 在这个数据集上很稳Lasso 会把部分特征权重压到零变相做特征选择但精度提升不明显。ElasticNet 的综合表现介于两者之间。注意线性模型用之前一定要做数据标准化否则量纲大的特征天然会分到更大的权重模型理解偏了。树模型就不用标准化因为它的分裂点不依赖特征的量纲。3.3 树模型入场随机森林、梯度提升、XGBoost、LightGBM当你把数据管道跑通后树模型几乎必然会把分数往上推一大截。我个人的经验是随机森林是很好的中坚力量它对异常值和缺失值更鲁棒并行训练也快但预测精度上限一般容易在验证集上给出相对保守的结果。XGBoost在表格数据上表现极其稳定且自带正则化参数不太容易过拟合。缺点是调参项偏多对新手来说有点“玄学”。LightGBM训练速度比XGBoost更快对缺失值有内置处理在数据量大时优势更明显。在小样本1460条上差距不明显但可以一起放进集成里。我当时跑下来的结果大致是Ridge 基线RMSLE 0.14左右随机森林能降到0.13XGBoost能到0.12LightGBM 和 XGBoost 差不多。如果只追求快速落地直接XGBoost确实省心。但如果你只是为了做项目炫技我建议至少对比三个模型这个过程能提升你选模型的判断力。3.4 集成与堆叠把多个模型的优点拼起来到后期我的核心策略是做模型堆叠。方法很简单把每个模型的交叉验证预测结果作为新的特征喂给一个简单的回归器我常用Ridge。# 伪代码思路假设我们已经有三个模型的OOF预测 import numpy as np from sklearn.linear_model import Ridge # oof1, oof2, oof3 分别是三个模型的out-of-fold预测 stacking_features np.column_stack([oof1, oof2, oof3]) meta_model Ridge() meta_model.fit(stacking_features, y_train_actual)之所以用Ridge做第二层是因为它简单、稳健、不容易过拟合。第二层如果继续用复杂模型反而容易把噪声也学进去。堆叠之后我的验证RMSLE大概又下降了0.005到0.01。这个幅度看着不大在竞赛里就是几百名的差距。4. 模型评估与参数优化别被验证集误差骗了训练完模型并不是终点评估和调参阶段才是真正区分“会用模型”和“理解模型”的地方。4.1 交叉验证的细节随机种子和KFold都很关键我用的是KFoldK折交叉验证K通常取10。但这里有一个不太被新手重视的细节KFold分出来的每一折结果对你的验证集误差影响很大。如果随机种子变了分出来的折不同最终分数可能上下浮动0.005。所以我会固定随机种子在所有模型之间保持一致的划分方式。这样你在对比模型A和模型B时能确定分数的差异来自模型本身而不是运气。另外如果数据里存在明显的分组结构比如同一个街区的房子用普通的KFold会导致训练集和验证集有信息重叠造成验证误差虚低。我在这个项目里试过 GroupKFold按街区分组验证误差一瞬间变高但这其实更接近真实场景的泛化能力。如果你期望的是一个能在未知新街区的房子上也表现不错的模型按街区分组做验证会更有意义。4.2 用学习曲线判断过拟合还是欠拟合跑完每个模型我会画学习曲线横轴是训练集数量纵轴是RMSE/RMSLE同时画训练集和验证集两条曲线。这个图形能直接告诉你模型处于什么状态如果训练集误差低、验证集误差高两者差距大说明过拟合应该增加正则化、减少特征或增加数据。如果两条曲线都偏高且接近说明欠拟合此时增加数据意义不大应该换更复杂的模型或补充特征。随机森林和XGBoost默认参数下在小样本数据集上很容易出现过拟合。通过观察学习曲线我决定限制树的最大深度、最小叶子样本数并适度提高正则化系数泛化能力立刻改善。4.3 网格搜索还是随机搜索我的选择网格搜索GridSearchCV在参数组合多时非常昂贵。我在这里用的是随机搜索RandomizedSearchCV先设定一个较大的搜索范围随机组合参数跑200~300次找到合适的区域后再做小范围的网格精调。以XGBoost为例我通常会重点搜索这几个参数n_estimators、max_depth、learning_rate、subsample、colsample_bytree、reg_alpha和reg_lambda。经验值给出一个大致的搜索范围参数范围说明learning_rate0.01 ~ 0.1学习率越小树的数量要越多max_depth3 ~ 7太深容易过拟合subsample0.7 ~ 1.0样本采样比例colsample_bytree0.7 ~ 1.0特征采样比例reg_lambda1 ~ 10L2正则化强度一个容易忽略的点当你降低 learning_rate 时n_estimators 应该成比例增加否则模型可能欠拟合。我习惯先用一个较小的学习率比如0.01配合早停机制训练足够多的树这样既能保证效果又能避免手动纠结树的数量。提示早停early stopping在XGBoost、LightGBM里直接支持设定一个 validation set当连续50轮验证误差不再下降就停止训练。这个技巧非常省心也不容易过拟合。4.4 特征重要性不要盲目信任默认输出树模型的 feature importance 很方便但默认用的是“基于分裂次数的重要性”有一定偏向性类别多的特征容易被高估高度相关的特征之间会分摊重要性。我建议做两件事一是用 permutation importance打乱某个特征后观察误差变化二是用 SHAP 值分析看每个特征对预测结果的正负影响。SHAP 做出来之后你会很直观地看到 OverallQual整体材料与装修质量对房价影响最大GrLivArea地面生活面积其次而有些你花费大量时间构造的特征其实贡献很小。这时候就该果断做减法特征不是越多越好冗余特征会干扰模型训练。5. 实战中的坑与排查记录数据泄露、异常值和特征陷阱这一部分是全文最有价值的部分也是我在这个项目里被教育得最惨的地方。5.1 数据泄露一个不容易发现的低级错误我最开始跑模型时RMSLE低得离谱心里暗爽。后来仔细一查发现我在做缺失值均值填充时用了整个数据集的均值——包括测试集。这在代码层面只是一个粗心但本质上属于数据泄露模型在训练时接触到了测试集的信息看起来验证分数很好一旦换到全新的数据上立刻打回原形。正确的做法是只用训练集计算填充值然后应用到测试集。有一个工具叫 ColumnTransformer可以把预处理的fit操作限制在训练集上避免这种错误。务必检查你的代码所有fit操作必须在训练集上完成测试集只有transform。5.2 构造特征反而掉分以总面积为例子我一度构造了很多组合特征比较典型的是把好几个面积加起来得到 TotalSF。这个特征本身很有道理加入后验证分数确实提升了一点。但后来我又构造了一个更复杂的“质量指数”把装修质量、面积、车库容量、地下室面积用不同的权重相乘结果验证分数大幅下降。原因是这个特征太“硬编码”了权重拍脑袋定的反而把模型的灵活性限制死了。特征工程要做的是给模型提供更多信息而不是替模型做决策。5.3 缺失值填0还是填中位数要分语义像 PoolQC、Alley、Fence 这些特征缺失就代表“没有”所以填0或填“无”是合理的。但像 LotFrontage、GarageYrBlt车库建造年份这类特征缺失可能代表“没有车库”也可能只是数据没记。如果你统一填中位数等于告诉模型这些房子都有个年份相近的车库这显然不对。我当时的做法是对 GarageYrBlt 先判断是否有车库有 GarageArea 且大于0有车库但年份缺失的才填中位数没有车库的直接填0。这样处理后模型对车库相关特征的理解明显更准了。5.4 提交阶段才发现价格还原错误这是我给所有新手的一个真诚提醒如果你对目标值做了log1p变换预测完一定要用expm1还原。我当时做堆叠模型时第二层模型用的是原始价格而不是对数价格结果还原错误验证集分数看着不错因为预测的是log价格验证集也是log价格提交却一塌糊涂。这种“训练时用对数、评估时忘了还原”的错误我身边至少三个人犯过。一个稳妥的流程是在最终提交之前单独写一个测试函数对单个样本走完整条管线打印中间结果手动检查预测价格是否符合常识。跑通一遍再批量预测。6. 收个尾这套流程到底能迁移到哪些场景最后说点个人的体会。这个实战做完价值不在于你拿到了一个多漂亮的RMSLE而在于你完整经历了一遍从原始表格数据到最终预测结果的全链路目标指标怎么定、数据怎么清洗、特征怎么理解、模型怎么对比、过拟合怎么防范。这些方法论几乎可以原封不动地迁移到其他结构化数据场景。我后来做过二手物品估价、用户消费金额预测、设备故障剩余寿命回归思路完全一样。区别只是特征语义不同有的数据需要更多时间做领域理解但评估体系、交叉验证策略、特征工程的判断逻辑都是相通的。这条路径还有一个很自然的扩展方向用同样的数据尝试一些深度模型比如TabNet或者简单的多层感知机对比一下和树模型在小样本上的差距。你会发现大多时候树模型依然占优但亲手试过一次你对“深度学习在表格数据上不一定有优势”这句话的理解会扎实很多。如果你正在学机器学习我建议先别急着追新的奇技淫巧把这个项目从数据处理到模型融合完整做两遍——第一遍求跑通第二遍求理解。每多做一个循环你对特征和模型关系的感知就会精进一分。
返回列表