资讯动态

房价预测从零到一:评价指标、特征工程与正则化回归完整实战

发布时间:2026/10/15 17:09:07 来源:尧图企业网站定制
房价预测这个经典赛题我从零到一跑了很多遍也算数据竞赛里最经典的回归题目之一。第一次接触的朋友很容易产生一种错觉这不就是回归吗随机森林一拍不就完了结果提交后分数惨不忍睹甚至不如一个老老实实做特征工程的线性模型。问题往往不在模型不够复杂而在于评价指标没看透、数据里隐藏的语义没理顺、验证方式自欺欺人。这篇是系列第一篇我会把从评价指标、数据预处理、交叉验证、第一批特征工程到Ridge/Lasso/ElasticNet这套高级回归模型的完整链路讲一遍并且解释每一步为什么这么做。适合刚入门表格数据竞赛、或者刷过很多算法但分数一直上不去的朋友。1. 先看清赛题在问什么评价指标决定一切很多人一上来就忙着调模型我习惯先搞清楚一件事这个赛题到底用什么指标评价。评价指标决定了你的目标函数怎么设计、数据怎么变换、验证怎么划分。这一节看着基础却是后面所有操作的前提。1.1 RMSE的相对误差逻辑为什么目标先做对数变换绝大多数回归比赛的评价指标写的是RMSE均方根误差不是准确率也不是R²。对模型来说最小化RMSE和最小化MSE完全等价因为开根号是单调操作不会改变最优解。但这两个指标都默认了同一个逻辑所有样本的误差按绝对金额一视同仁。房价数据有个特点便宜的房可能只有五六万美元贵的能到六七十万。如果直接用原始价格训练一个20万美元的房子预测误差是5千它对损失的贡献和一个5万美元房子的5千误差完全一样。这会让模型把大部分容量用在拟合金额大的房子上因为同样的绝对误差在贵房子的损失函数里占比并不高反过来便宜房子稍微预测偏一点绝对误差也许不大但相对误差可能很高最终榜上的RMSE也会被拉高。解法很简单先把房价取对数y_log np.log1p(y)。在log空间里5万和50万的价格差被压缩到可以比较的尺度模型拟合的是近似相对误差。这也是为什么很多公开方案第一行代码就是np.log1p(train[SalePrice])。代价是你提交时必须记得做逆变换也就是np.expm1(model.predict(...))。忘掉这一步分数会直接崩掉。新手容易在这里栽一个跟头模型在log空间预测不会产生负值但如果你做了一些激进的变换把极端样本的log值推得很低逆变换回来可能出现负价格。所以提交前最好检验一遍预测值是否都在合理范围这个检查只需要一行代码却能在最后关头救你一命。1.2 训练集与测试集的分布差异不能想当然搞回归的人最容易忽略的一件事训练集和测试集往往不是完美同分布的。数据切分背后通常有隐藏逻辑比如按区域、按时间、按房源类型分批采集。你如果假设两边完全一致很多特征工程的假设会直接失效。最直接的检查方法是把训练和测试拼在一起按你最关心的离散特征年份、区域编号、房屋类型做交叉计数。你会发现某些年份在训练集里有一百多条在测试集里一条都没有某些区域只在测试集出现。这时候问题就来了你的One-Hot编码是按训练集的类别生成的测试集出现新类别时模型没见过这个编码只能把它当成全0向量等于把这一条样本的信息全部丢弃。处理思路有这么几种低频类别合并成其他对明显与目标相关的稀有类别单独构造指示特征或者干脆把类别频率本身作为一个计数特征喂给模型。这一阶段先记住一个原则先确认训练集和测试集的类别覆盖度再做编码。A同学当时就是没做这一步跑出了一个漂亮的CV分数提交后严重下滑最后排查到是一个只在测试集里大量出现的新区域类别被One-Hot全清零了。这种问题数据可视化阶段就能发现根本不需要等提交交学费。2. 数据预处理顺序缺失值、异常值、数据类型预处理是整个流程里最不性感的环节但也是回报率最高的环节。很多高名次方案并不用特别复杂的模型赢就赢在数据整理得干净、语义理解得透彻。这一节我把最关键的三个坑拿出来讲透。2.1 缺失值不能一把梭不同列的缺失含义完全不同很多入门教程教你缺失值用均值填充但房价数据这么干等于在粗暴抹平信息。原因很简单缺失本身往往是一个有意义的信号。比如车库完工年份这个字段缺失的大概率是那些没有车库的房子。用整列均值去填就相当于给所有没有车库的房子安了一个车库平均完工年份这个虚构的数字没有任何物理意义模型学到的是一条假规律。再比如地下室面积缺失通常也代表没有地下室而不是随机抽样时恰好漏填了。所以我的习惯是在填充之前先整理一张缺失语义表每一列写清楚缺失率、缺失的可能原因、合理的填充方式。原则是能归因于无此属性的缺失就补0同时增加一个指示变量真正随机缺失的才考虑用中位数或众数。指示变量就是那一列的缺失标志取值0/1让模型自己去学这个缺失信号有没有用。这个做法几乎无成本但对线性模型和树模型都非常友好因为模型获得了一个额外的决策维度。同时还要注意训练集和测试集要拼在一起做缺失值处理。原因有两个一是填充参数在全样本上估计更稳定二是保证两边填充策略完全一致否则训练时学到的分布和推断时遇到的分布就错位了。有朋友图省事分开处理结果训练集某列中位数是0.3测试集中位数是0.8模型上线后表现一塌糊涂排查半天才发现是这里出了问题。2.2 特征里的0值比NaN更危险预处理的时候大家都会盯着NaN但0值经常被放过。面积类特征尤其明显没有地下室的房子地下室面积写的是0没有车库的房子车库面积写的是0。这些0和真正的数值为0完全是两回事。危险在于如果你做组合特征比如总面积 × 车库面积一个0就会把乘积整个清零。一台明明有很好面积数据的好房子仅仅因为车库面积为0组合特征就变成0其他维度的信息全被吞掉了。这个现象我用过一个大白话类比一锅好汤里掉进一颗老鼠屎整锅都得倒掉0值就是那颗老鼠屎。处理方式是在构造组合特征之前先针对0值代表无的字段做二值指示变量比如是否有车库然后对真正的面积字段如果0值分布太广可以在组合时使用类似max(area, 1)的技巧把0替换为一个不影响数据尺度的占位值。这个细节对后续所有特征交互操作都有影响。我在帮A同学复盘他那份数据时就发现一个地面面积×卫生间数量的组合特征因为0值没清理把模型整体带歪好几个区域的预测都系统性偏低。2.3 数据类型声明是隐形的坑pandas读进来以后类型问题会安静地毁掉你的特征工程。最常见的情况有几种一个本来是类别编号的列因为存的是01这种带前导零的字符串被读成object这还算正常但如果存的是纯数字比如区域编号1、2、3它会被读成int于是模型就把它当成有序连续变量凭空学出一个编号越大越贵的假趋势。反过来一些真正的数值列因为存在缺失会被当作float64。表面上没问题但如果你后续做数值计算比如构造多项式特征这些float列会正常参与却忽略了它们原本的离散语义。判断标准很简单问自己一句这个特征的大小顺序有意义吗有就保留为数值没有就转成category或处理成序号编码。质量评分这种1到10的有序评分我保留为数值并在特征工程里做成平方项让模型自己学非线性街道类型这种无序类别则必须转成category不能再让它假装是连续变量。另外一个小技巧如果某些类别字段训练集和测试集拼接后类别数不一致我会先用pd.Categorical的categories参数统一类别范围避免测试集出现新类别导致编码错位。这几乎是我做表格数据必写的一步看似麻烦实则能挡住大量奇奇怪怪的运行时错误。3. 交叉验证框架能不能信赖你的分数全看它这一节我要把它称为整个比赛流程里的地基工程。没有可靠的验证方式你后面做的每一个特征、调的每一个参数都可能是在自欺欺人。很多人分数上不去不是因为模型不行而是因为验证框架本身在说谎。3.1 为什么简单随机划分会骗人很多人拿了数据用train_test_split一划训练完看一眼验证分数觉得自己模型不错提交之后发现天差地别。原因之一就是单次随机划分的方差很大换一个随机种子验证分数可能抖动好几个点你完全无法判断模型是真的好还是这次划分运气好。更隐蔽的问题是空间聚集性。房价数据天然有区域聚集同一个街区、同一个年份的房源价格高度相似。如果你随机划分恰好把一个片区的房子大部分放进了验证集而训练集里几乎没有这个片区的样本那验证集就代表了一个你没见过的分布分数自然难看反过来如果验证集恰好和训练集高度同质分数又虚高。单次划分全靠运气你不是在评估模型你是在掷骰子。这就像你只抽一次签就想知道自己期末考能排第几名一次抽中简单的题分数好看一次抽中难题分数难看。可复习方向完全没变变的只是那把签的心情。所以真正可靠的评估必须让每一个样本都有机会被验证到。3.2 10折KFold与分层思路我自己的默认配置是10折KFoldshuffleTrue随机种子固定比如42。为什么是10折而不是5折训练数据量不是特别大的时候10折意味着每次验证集只有10%训练集有90%模型每次都能在更完整的数据上学习代价是训练10次但对于几万行规模的表格数据这个成本完全可以接受换来的是更稳定的评估。固定随机种子这一点特别重要。如果你每次训练都重新划分模型分数变化里就混入了划分的变化你没法判断是特征工程带来的提升还是这次运气好。所以我会把fold索引一次性生成好存成一个字典或者CSV所有后续实验都用同一套划分。这样不同模型、不同特征组合之间的分数才具备可比性。这也是我后来做任何表格任务都坚持的流程。回归任务里还能做一个加强版StratifiedKFold本来是给分类设计的但回归也可以用。先把目标值按分位数切成若干个桶当成类别标签传进去这样每一折的目标分布都和整体接近。这个操作能明显降低每折之间的波动尤其适合房价这种右偏严重的目标变量。3.3 CV分数和LB分数量级差异的来源很多新手的焦虑都来自CV分数和公共榜单分数对不上。这里要先明白一个道理公共榜只用了测试集的一小部分你的CV是在训练集内部做划分两者天然有偏差。CV稳定且可重复LB只是抽查。反复看LB调参等于拿公共榜当验证集用次数多了就会过拟合到那部分公共测试样本上这也是公开榜分数和私人榜分数差异巨大的一个重要来源。我还遇到过一个反向情况CV分数比LB还高。这通常意味着两种可能一是训练集里恰好有一些和验证集高度同质的数据段CV被高估了二是目标分布在两个数据集里确实存在偏移。这种时候不要急着改模型先检查数据分布检查有没有数据泄露——比如某些特征在训练测试之间本不该共享却因为合并处理被悄悄带过去了。记住一个原则交叉验证的目的不是逼近LB而是给你一个稳定、可信、可复现的模型选择依据。CV比LB高不一定是好事可能只是在重复验证同一种模式。4. 特征工程第一阶段先做合法的暴力转换接下来进入这个赛题最核心的部分特征工程。这一阶段我强调合法两个字意思是先不做目标编码、不做复杂的统计编码只做数学上合理、不会明显过拟合的变换。目标编码这类方法看着漂亮但稍有不慎就会把顺序信息泄露到折叠外。4.1 偏态数值特征的log/Box-Cox变换为什么要关注偏态线性模型、尤其是Ridge/Lasso这类带正则化的模型对特征尺度高度敏感。一个严重右偏的特征比如某些面积字段可能大部分值集中在很小的区间内偶尔几个超大值落在很远的右侧。这种分布会在特征空间里留下几个孤立点模型为了让这几个点拟合得更准会把系数带到奇怪的区域其他特征的贡献就被压下去了。我的筛选办法是用偏度系数skewness阈值设在0.75。超过就对特征做变换如果特征没有0值且全部大于0直接用log1p如果有0值可以先做一个是否为0的指示变量再对非0部分取对数或者把所有值平移为正以后做Box-Cox。scipy里提供了现成的boxcox函数但它要求输入必须全部为正所以处理好0值是前提。变换之后有一个关键动作记录哪些列做过变换。后面无论是做逆变换、特征扩展还是模型解释都要回头参考这张清单。很多人在这步偷懒后面对特征做可视化、排查异常预测时才发现分不清哪些字段是原始值哪些是对数域白白浪费时间。4.2 序号编码、One-Hot与低频类别合并类别特征的处理我分成三类。低基数类别比如房屋类型这种七八个类直接用One-Hot。换成树模型其实也可以不编码但线性模型必须显式展开。中基数类别比如街道类型几十个类One-Hot会撑出几十个稀疏列对线性模型的惩罚项很不友好我会压缩一下先把计数少于某个阈值的类别合并成其他再One-Hot。高基数类别就复杂了比如区域编号上百个类One-Hot不现实目标编码又容易过拟合这一阶段我会先放着等系列后面讲更稳的方案。有序类别务必保留顺序。质量评分从1到10是一个典型的有序量表。如果你用One-Hot等于丢掉了1比10差很多这个信息如果你直接用原始数值又隐含了每差1分价格差一样多的线性假设。实际操作中大多数强方案都是保留原始数值再补一个平方项让模型自己学非线性衰减。4.3 面积乘以质量线性模型最需要的组合特征线性模型的短板是它学不到特征交互面积大、质量好两个因素一起出现时价格不是简单加法而是乘法。树模型可以自动发现这类交互但线性模型不行所以你必须显式把交互项构造出来喂给它。最稳的组合特征集中在几个方向面积类乘面积类比如总面积乘地上面积面积类乘质量类比如总面积乘总体质量评分面积类乘卫生间数量再有就是让是否有车库这种指示特征去乘车库面积。原理用一句话概括就是房价的物理直觉是体积×单价面积越大、质量越高总价越高。构造一个有效面积类的特征让线性模型一次就能学到这个乘积关系比让模型自己去猜要高效得多。构造的时候最需要注意的就是前面讲的0值问题。一定要先清理缺失和0值再做乘法否则NaN和0会把整个特征列污染掉。命名也要规范我用featureA_featureB这种格式方便回看实验记录时一眼就知道这个组合特征是基于什么构造的。5. 高级回归模型入场Ridge/Lasso/ElasticNet的正确打开方式到了这一节才真正进入高级回归的主题。之所以把正则化线性模型作为第一篇的主角而不是直接上XGBoost是因为它足够透明能帮你判断特征工程做得好不好。如果后续树模型大幅度超越它说明特征交互价值很大如果两者差不多说明你的数据清洗和特征构造已经到位了。5.1 Ridge/Lasso/ElasticNet的分工差异三兄弟的分工可以这样记Ridge用的是L2惩罚所有特征共同分担信号系数不会变成0适合特征普遍有效的场景Lasso用的是L1惩罚会把不重要特征的系数压成0相当于自动做特征选择但它有两个毛病系数估计是有偏的而且特征高度相关时它会随机挑一个代表稳定性差ElasticNet把L1和L2按比例混合用l1_ratio控制两者权重在相关特征多的回归任务里通常最稳。模型惩罚类型系数行为适合场景主要风险RidgeL2不会归零特征普遍有效相关性高全部特征都保留解释性差LassoL1部分归零特征稀疏选择高相关特征下选得随机ElasticNetL1L2折中高相关大特征集多一个超参要调我在房价数据上默认从ElasticNet开始因为面积、质量、年份这些特征之间高度纠缠Lasso选得太狠Ridge又太保守ElasticNet恰好能兼顾。l1_ratio我会从0.5附近起步再按结果微调。5.2 数据标准化不能省Pipeline防止数据泄露Ridge/Lasso/ElasticNet对特征尺度非常敏感因为惩罚项直接作用在系数上。面积这个特征如果数值是几十到几千而质量评分只有1到10两个特征的系数天然不在一个量级。如果不做标准化面积特征的系数会被惩罚压得特别小模型会误以为质量评分比面积重要得多这完全是尺度造成的假象。所以数值特征必须全部过StandardScaler让每个特征均值0、方差1。这里有个比很多教程更重要的细节scaler只能在训练折上fit再变换验证折和测试折绝不能先对全数据做标准化再划分。否则验证集的统计信息已经进入训练过程CV分数会虚高且不安全。解决方案是用Pipeline把scaler和模型串在一起每次fit都只看到当前的训练折。这不仅是代码整洁的问题是对抗数据泄露的基础设施。5.3 超参搜索从小网格开始别一上来就全自动调参之前先把搜索目标定下来以交叉验证的平均分为准不要看LB更不要看训练集分数。ElasticNet有两个核心参数alpha控制正则强度l1_ratio控制L1占比。alpha的搜索范围一般用log空间比如从0.001到1按约3倍步长取几个点l1_ratio在0.1到0.9之间按0.1或0.2步长取几个点。两个参数组合起来顶多几十组网格搜索完全跑得动。如果以后特征多了、要调的参数多了再换成随机搜索因为维度高起来以后随机采样比穷举网格高效得多。不要一开始就上贝叶斯优化之类的自动调参工具。这个阶段你的特征还没稳定调参工具再聪明也是在流沙上盖楼。先把参数固定在一个合理范围把注意力放在数据和特征质量上等整体流程稳定了再优化超参不迟。而且自动调参工具一旦用上你会不自觉地把所有精力放在参数上反而忽略了真正能拉开差距的特征工程。6. 把流程串起来一个可复现的最小工作流前面的章节都是单点技术这一节我把它们串成一个闭环给一个精简但可跑的代码骨架。这个骨架本身不会给出顶尖分数但它是一条所有后续实验都能对比的基线。没有基线你永远不知道自己每一步改进了多少。6.1 代码骨架从读取数据到10折评估import numpy as np import pandas as pd from sklearn.model_selection import KFold from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import ElasticNet from sklearn.metrics import mean_squared_error train pd.read_csv(train.csv) test pd.read_csv(test.csv) # 目标变量对数变换后续提交记得逆变换 train[SalePrice_log] np.log1p(train[SalePrice]) y train[SalePrice_log].values # 拼接训练测试集保证两边预处理完全一致 n_train len(train) full pd.concat([train.drop([SalePrice, SalePrice_log], axis1), test], axis0) # 在这里统一处理缺失值、类型转换、构造组合特征省略详细代码 # full fill_missing(full) # full encode_category(full) # full add_interaction_features(full) # 取出训练集部分 X full.iloc[:n_train] # 固定10折存下来全项目共用 kf KFold(n_splits10, shuffleTrue, random_state42) scores [] for train_idx, valid_idx in kf.split(X): X_train, X_valid X.iloc[train_idx], X.iloc[valid_idx] y_train, y_valid y[train_idx], y[valid_idx] # Pipeline保证scaler只在训练折上fit不发生数据泄露 model make_pipeline(StandardScaler(), ElasticNet(alpha0.01, l1_ratio0.5)) model.fit(X_train, y_train) pred model.predict(X_valid) score mean_squared_error(y_valid, pred, squaredFalse) # RMSE scores.append(score) print(fCV RMSE: {np.mean(scores):.4f} ± {np.std(scores):.4f})这段代码跑通以后你的基线就立住了。接下来每次实验只改一个变量加一个特征、换一个缺失填充方式、调一个超参其余全部保持不动然后记录分数变化。如果某次改动让CV均值下降了0.01那么多半是有效改动如果CV均值没动只是标准差变大那大概率是噪声不要被它带偏。6.2 实验记录表是看不见的竞争力最后想说一个很多人忽略的习惯实验记录。特征数量、做了哪些变换、用的模型和参数、CV均值和标准差、训练时间每次都记下来。没有记录你优化流程的效率极低因为同一个参数组合可能两周后又被你当成新实验重新做一遍。我的记录表长这样实验编号、特征集版本、模型、alpha、l1_ratio、CV均值、CV标准差、备注。每次改动只动一行分数是涨是跌一目了然。做过几轮之后你会发现真正让分数提升的不是某个魔法参数而是一连串小改动的叠加缺失值语义对了、0值处理了、组合特征加了、交叉验证稳了每个都能带来零点零零几的提升合起来才是一个看得见的阶跃。做比赛和写代码一样最值钱的资产不是你某个神秘的调参组合而是你对自己每一步实验的掌控力。这一篇先讲到这里覆盖了从评价指标、数据预处理、交叉验证到第一批特征工程和线性正则化模型的完整闭环。下一篇我会开始上树模型讲XGBoost和LightGBM在实际回归任务里的典型坑以及模型融合的入门写法。到时候你会看到这几个模型和线性模型互补在哪里堆叠为什么能比单一模型明显更稳。我个人做这类赛题最大的体会是别急着玩花活先把地基打实——验证可信、特征可追溯、实验可复现这三点比任何单模型魔法都值钱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑