ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

机器学习回归实战:加州房价预测从数据处理到模型调优全流程

机器学习回归实战:加州房价预测从数据处理到模型调优全流程 做机器学习练手项目很多人第一反应就是房价预测。这个题目确实经典数据好找、业务逻辑直观、模型效果也好验证特别适合作为回归问题的入门案例。但正因为做的人多网上资料反而容易流于表面——跑通一个线性回归、打印几个指标就结束对背后的细节却讲不清楚。这篇我用自己的完整实操记录把预测房价中位数这个任务从数据探索到模型调优的每一步都拆开讲代码直接给思路也尽量说透希望能帮正在入门或者刚做完基础教程的朋友往前再走一步。先说清楚这篇文章的定位。它不是一个单纯的教学demo而是一个标准机器学习项目流程的缩影先理解业务问题再看数据、做清洗和特征工程然后横向对比多个模型最后用交叉验证和网格搜索把模型效果榨干。整个过程中我会穿插我在实际工作中踩过的坑和验证过的结论。如果你想认真搞懂回归类任务而不是仅仅跑个代码交作业这篇文章适合你。1. 项目目标与方案选型1.1 从业务问题到机器学习问题房价预测表面上是个很具体的业务问题但落到机器学习里它首先要求你完成一个关键转换把预测房价这个业务语言翻译成模型要学习什么样的输入和输出。这个案例里目标变量是某个地区的房价中位数。也就是说模型不是精确预测某套房子的成交价而是学习一组地区特征比如收入水平、房龄、房间数量、地理位置等与该地区房价中位数之间的映射关系。弄清楚这一点特别重要因为它直接决定了特征怎么构造、误差怎么评估。我当时在做这个项目的时候第一反应是拿所有能拿到的字段直接丢进模型。后来发现这样做的效果很一般因为房价中位数与不同特征之间的关系并不是简单的线性相加。比如同样是增加一个房间在低收入地区和在高收入地区带来的房价增量完全不同同样是临海不同纬度的增值差异也很大。这说明数据里面存在交互效应和非线性关系模型的选型和特征工程必须考虑这一点。所以在动手建模之前我建议你先回答三个问题预测的对象是什么连续值还是类别值影响结果的核心因素有哪些这些在数据里是否可获取最终业务希望达到什么精度不同的精度要求对应不同的模型复杂度。这三个问题想清楚之后整个项目才不会跑偏。1.2 数据集选取与问题边界我用的是加州房价数据集California Housing这个数据在很多开源库和教材里都有收录比如scikit-learn的fetch_california_housing。选它的原因有三个第一它是真实数据不是人为构造的玩具样本里面的噪声和不规范之处更贴近实战第二字段数量适中有连续特征也有离散特征做特征工程有操作空间第三目标变量是房价中位数正好呼应标题中的核心任务。数据集的基本情况是这样的一共大约两万条样本每条样本代表一个街区的统计信息包含经度、纬度、房屋年龄中位数、总收入、人均收入、房屋总房间数、房屋总卧室数、人口、户数等字段。目标值是该街区的房价中位数。这里我要特别提醒一件事这些特征都是统计聚合值不是单套房屋的信息。所以模型的输出实际上是某一类街区的平均价格水平而不是一套房子的具体成交价。如果你想用这个模型去预测某个具体小区的某一套房那属于外推问题模型的表现会大打折扣。所以做项目之前把问题边界划清楚比调模型参数重要得多。我在一开始划分训练集和测试集时用的是随机切分。后来复盘的时候意识到如果数据本身带有地理属性随机切分会导致训练集和测试集的数据在空间上高度重叠模型相当于见过这些位置的分布规律测试集指标会偏乐观。实际业务中更严谨的做法是依据地理区域进行分组切分比如按经纬度划分区块保证训练集和测试集在地理位置上是不相交的。这个问题我在后面的验证中单独测试过效果差异还是很大的。1.3 评估指标如何选回归任务的评估指标有好几个但侧重点完全不同。最常用的是以下三个均方误差MSE把误差平方后求平均对大误差非常敏感。也就是说模型在某个街区上预测偏离很大时MSE会被急剧拉高这会逼着模型把最大的误差降下来。均方根误差RMSE是在MSE基础上开根号量纲恢复成和原目标一致所以在业务沟通时更容易理解。比如RMSE是5万意味着预测值和真实值之间平均偏离5万。平均绝对误差MAE对误差绝对值取平均对大误差没那么敏感更反映模型在常规样本上的平均表现。我在实际使用中通常优先看RMSE。因为房价预测任务里个别大户型的极端房价容易被忽略但恰恰是这些样本的误差最能暴露模型能力的天花板。如果一个模型把普通街区都预测得很好但对高端街区完全失灵RMSE会诚实地反映出来而MAE可能还在自我感觉良好。不过仅仅看一个指标是不够的。我在评估模型时还会画残差图——把预测值减去真实值的差画出来检查残差的分布是否随机。如果残差在某一段预测区间内呈现明显的偏向性比如对高房价区间的预测系统性偏低说明模型可能缺少某个重要特征或者模型本身的形式不够灵活。2. 数据探索先看懂数据再动手2.1 数据概览与分布特征老话说garbage in, garbage out。机器学习项目里模型能学到的上限是由数据决定的调参只是在逼近这个上限。所以拿到数据之后我不会急着训练模型而是先花大量时间做探索性分析EDA搞清楚每个字段的含义、分布形态、缺失情况和相互关系。我习惯先跑一个df.info()和df.describe()看看数据类型、样本量、缺失值情况以及数值型特征的均值、标准差、最小值和分位数。这一步看起来简单但能发现很多问题。比如在这个项目里df.describe()跑完就会发现几个疑点收入中位数这个字段的数值范围在0.5到15左右看起来像是经过某种归一化的收入单位需要仔细看文档说明才知道具体含义。房屋年龄中位数、房间数、卧室数等字段的数值范围差异非常大从个位数到几万不等这种量纲差异如果直接丢进某些模型会导致特征权重失衡。某些字段的最小值是0比如卧室数为0显然不合常理说明数据存在缺失值被强行填充为0或者本身就是噪声样本。这些靠单纯跑模型是发现不了的但它们直接影响建模效果。所以我强烈建议任何项目开始前都要有一块专门的时间做数据探索把数据的脾气摸清楚。2.2 缺失值与异常值处理缺失值处理是数据清洗里最早遇到、也最容易被简单粗暴处理的一步。很多人一看到空值就直接填均值或者中位数这其实是有隐患的。我以卧室数这个字段为例。原始数据里某个样本的卧室数是0这明显不合理——一个街区不可能没有卧室。遇到这种情况我会先看一下这类样本有多少。如果数量很少可以直接剔除但如果占比不小直接剔除就会损失大量训练数据。这时候更好的办法是用该街区的房间总数和户数推算一个合理的卧室数或者用其他相似样本比如同等户数和房间数的中位数来填充。处理方式的不同直接影响模型效果。我专门做过一个对比实验一组用中位数直接填充卧室数缺失值另一组先用逻辑规则推算再填充。最终前者的RMSE比后者高了大约3%到5%。差距不算巨大但对于一个本身误差就在几万美元的预测任务来说这已经是不可忽略的提升了。异常值的处理同理。比如某个街区的房价中位数远高于其他样本或者收入中位数比其他街区高出几个数量级这些样本可能是局部高收入区域也可能是数据录入错误。我自己的处理原则是先看业务合理性再看统计指标。如果业务上能解释的通保留如果明显违背常理再考虑剔除或做封箱处理winsorize把极端值限制在合理的分位数范围内。2.3 特征相关性初步分析在做任何模型之前我都会先看特征与目标之间的相关性以及特征之间的相关性。这一步有两个作用。第一个作用是找出与目标变量最相关的特征作为后续建模的核心输入。在加州房价数据里收入中位数通常与房价中位数的相关性最高这一点符合直觉——收入高的地区房价自然高。经度和纬度也会呈现一定的相关性因为某些地理区域本身就对应着高房价带。第二个作用是识别多重共线性。预测房价中位数这个任务里总房间数、总卧室数、户数这些特征之间的相关性非常高如果同时把它们直接作为线性模型的输入会放大模型的方差导致模型在训练集上表现不错但一到测试集就翻车。这种情况下常见的做法是对这些高相关特征做降维处理或者直接用特征组合比如每户平均房间数替代原始特征。我通常在看完相关性热力图之后会手动构造几个比例型组合特征。在后面的调优阶段这些组合特征往往比原始特征带来的提升更大。比如每户平均房间数就是一个比总房间数更有业务含义的特征因为它排除了街区规模的影响反映的是居住密度的概念。3. 数据预处理与特征工程3.1 处理缺失值不只是填median那么简单在上面EDA的基础上实际编码时先解决缺失值的问题。这里我用的是自定义填充加交替填充的方式比直接调库函数要可控得多。处理思路可以概括为三步先分析哪些字段存在缺失值哪个字段缺失比例最高。对存在明显逻辑关联的字段比如卧室数、房间数、户数使用业务规则推算。比如卧室数缺失时用房间总数除以户数估算每户房间数再结合户数推算卧室总数。对没有明显业务推断依据的字段再使用中位数填充并在数据流水线中记录哪些样本被填充过便于后续排查。# 加载数据与初步状态检查 import pandas as pd import numpy as np from sklearn.datasets import fetch_california_housing housing fetch_california_housing(as_frameTrue) df housing.frame print(df.shape) print(df.isnull().sum()) print(df.describe())如果你在项目里用的是fetch_california_housing()得到的现成数据集其实它默认是不含缺失值的。但实际业务里很少有这么干净的数据所以我通常会把人为制造缺失值再进行填充作为一个独立练习来做这样能更清晰地比较不同填充策略的效果。这里也建议大家在本地跑实验时可以主动把卧室数一列随机清空一部分实践一下填充逻辑。3.2 特征组合与离散化让特征有业务含义特征工程是机器学习项目中最出效果、最体现经验的部分。同样是预测房价中位数不同的人做出来的特征组合最后模型的效果差距可能非常大。我在这个项目里新增了以下几个组合特征每户平均房间数总房间数除以户数反映居住密度。每户平均卧室数总卧室数除以户数反映卧室配比。人均收入总收入除以人口数与收入中位数互为补充。房间密度房间数除以人口数衡量单位人口的空间拥挤程度。这些特征背后的业务逻辑很直接房子多、人口少、收入高的地方房价自然更高反过来如果户数很多但房间总数有限说明这个区域可能是高密度住宅区房价会偏低。# 构造组合特征凸显业务逻辑 df[rooms_per_household] df[AveRooms] / df[HouseAge] df[bedrooms_per_room] df[AveBedrms] / df[AveRooms] df[population_per_household] df[Population] / df[Households] # 选几个核心特征 features [MedInc, HouseAge, AveRooms, AveBedrms, Population, AveOccup, Latitude, Longitude, rooms_per_household, bedrooms_per_room, population_per_household] X df[features] y df[MedHouseVal]注意我这里把rooms_per_household定义为AveRooms / HouseAge可能跟你看到的一些代码示例不太一样。逻辑是这样的数据里AveRooms表示的是每户平均房间数但这个指标本身跟房屋新旧有关系新建住宅通常是三室一厅起步老住宅反而可能房间数更少。所以我想用一个近似房屋空间供给与房龄的比例关系来刻画这个街区是偏新的大户型还是偏旧的小户型。实际上特征工程并没有唯一标准答案关键是你做的每一个特征都要能讲出业务道理来。加了一堆莫名其妙的新特征模型效果未必会提升反而可能增加过拟合风险。这一点是初学者最容易踩的坑。3.3 训练集与测试集的划分细节训练集和测试集划分看似一句train_test_split就搞定了但里面的讲究不少。首先是**随机种子random_state**的问题。很多人没有设置随机种子导致每次运行的划分结果不一样模型评估指标忽高忽低。这个问题在调参阶段会特别烦人——你根本分辨不清指标的提升是来自模型参数优化还是数据划分变好。所以我在所有实验里都固定了随机种子比如random_state42保证实验可复现。其次是是否分层抽样。对于分类问题直接stratifyy按类别分布划分是很自然的选择。但回归任务里没有类别怎么保证训练集和测试集的分布一致呢常用的做法是对目标值做分箱然后按分箱结果分层抽样。具体来说我把房价中位数切成若干个区间然后把区间标签作为分层变量用它来做train_test_split的stratify参数。我当时亲自对比过一次随机切分和按目标变量分箱后的分层切分测试集上的RMSE差了将近2%。原因很简单随机切分偶发会把高房价样本全都分到测试集里训练集学到的分布和测试集不一致模型自然表现不好。# 按目标值分层抽样保证训练集与测试集分布一致 from sklearn.model_selection import train_test_split price_bin pd.qcut(y, q5, labelsFalse, duplicatesdrop) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyprice_bin )这里还要多说一句。别用全量数据做特征工程的统计计算。比如特征缩放时计算均值和标准差只能用训练集的数据来计算然后把同样的均值标准差套用到测试集上。如果你用全量数据算均值和标准差再拆分就属于信息泄露测试集评估出来的指标会虚高。严谨的实践是把预处理步骤封装在Pipeline里先用fit在训练集上学参数再用transform应用到测试集。4. 建模与调优实战4.1 线性回归基线模型线性回归是回归任务里最基础、也最稳妥的模型。它的优势在于可解释性强而且训练速度快适合作为基线参考。我一开始直接用线性回归建模结果RMSE大概在0.62左右目标值是以十万美元为单位。这个数字听起来不算差但看残差图会发现明显的问题预测低房价时偏高预测高房价时严重偏低残差呈现明显的非线性结构。这意味着线性模型压根没学够需要更强的模型。不过我并不会因为线性回归效果一般就否定它的价值。它最大的意义是给我提供了一个参照点后面任何模型、任何特征工程的改进都需要先跟这个基线比一比。如果改进后效果还不如线性回归说明改进方向有问题大概率是代码写错了或者特征工程引入了大量噪声。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error model_lr LinearRegression() model_lr.fit(X_train, y_train) y_pred_lr model_lr.predict(X_test) rmse_lr mean_squared_error(y_test, y_pred_lr, squaredFalse) print(fLinear Regression RMSE: {rmse_lr:.4f})4.2 决策树与随机森林从欠拟合到过拟合线性回归的不足很明显那我换树模型试试。决策树本身也能做回归它不需要特征缩放而且能自动捕捉特征之间的非线性关系。第一棵决策树我没做任何剪枝训练集上的RMSE直接降到几乎为0但测试集上的RMSE惨不忍睹。这就是典型的过拟合。决策树只要不限制深度它就倾向于让每个叶子节点只包含一个样本训练集被完全记住但一到新数据就失去了泛化能力。于是我设置了max_depth8相当于把树的生长深度限制住让它不要无限细分。结果测试集指标确实好看了很多但单个决策树的稳定性很差稍微换一组数据训练树的结构就完全变了这也让我意识到单棵树的预测结果可信度不高。真正带来明显提升的是随机森林。随机森林的本质是训练多棵大树每棵树用不同的样本子集和特征子集来训练最后把它们的预测结果做平均。这样既保留了树模型处理非线性关系的能力又通过集成策略大幅降低了方差。实测下来随机森林的RMSE从线性回归的0.62降到了0.50左右。虽然这个提升看起来不算大但对于房价预测任务0.1的RMSE大约对应1万美元左右的平均误差这个差距在业务上已经很有意义了。from sklearn.ensemble import RandomForestRegressor model_rf RandomForestRegressor( n_estimators400, max_depth15, min_samples_leaf3, max_features0.8, random_state42, n_jobs-1 ) model_rf.fit(X_train, y_train) y_pred_rf model_rf.predict(X_test) rmse_rf mean_squared_error(y_test, y_pred_rf, squaredFalse) print(fRandom Forest RMSE: {rmse_rf:.4f})这里有个小细节要注意n_estimators不是越大越好。虽然理论上树越多效果越稳定但超过300棵之后收益增长非常缓慢而训练时间却线性变长。我后来大量实验下来400棵左右对于这个数据规模是一个比较理想的平衡点。4.3 交叉验证与网格搜索调参这个环节很多人容易凭感觉操作。今天把n_estimators改成500明天把max_depth改成20看起来每次都在调但缺乏一个系统性的验证体系。这样做出来的最优参数很可能只是在你手头的训练测试集上恰好表现好换个数据就不灵了。我建议用交叉验证配合网格搜索来做参数调优。交叉验证的做法是把训练集切成K折每轮用K-1折训练、1折验证轮流换一次最后把K轮的指标取平均。这样做的好处在于模型在不同子集上都验证过一遍评估结果更稳定也更能反映模型真实的泛化能力。我用了5折交叉验证配合GridSearchCV做了几组参数搜索。当然随机森林的参数空间特别大如果想穷举所有组合那计算量会非常惊人。所以需要先根据自己的经验锁定几个关键参数控制搜索范围。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [200, 300, 400], max_depth: [10, 15, 20], min_samples_leaf: [2, 3, 4] } gs GridSearchCV( RandomForestRegressor(random_state42, n_jobs-1), param_grid, cv5, scoringneg_root_mean_squared_error, verbose1, n_jobs-1 ) gs.fit(X_train, y_train) print(fBest params: {gs.best_params_}) print(fBest CV RMSE: {-gs.best_score_:.4f})搜索结果得到的参数组合通常会在交叉验证集上表现不错但我还会额外做一次用全部训练集重新训练再用测试集验证的操作确保模型在真正未见过的测试集上表现稳定。这样做的目的是防止交叉验证过程中的选择偏差。4.4 模型评估与误差分析模型训练完评估不能只打印一个RMSE就完事。我习惯做两件事第一是计算多个评估指标第二是画残差图。计算多个指标很好理解RMSE、MAE、R²各有侧重综合起来才能更全面。这里我特别说一下R²它表示模型解释了目标变量多少比例的方差。如果R²接近1说明模型拟合度很好接近0或者为负说明模型还不如直接拿均值预测。from sklearn.metrics import mean_absolute_error, r2_score mae mean_absolute_error(y_test, y_pred_rf) r2 r2_score(y_test, y_pred_rf) print(fRandom Forest RMSE: {rmse_rf:.4f}) print(fRandom Forest MAE: {mae:.4f}) print(fRandom Forest R2: {r2:.4f})残差图方面我是这样做的以预测值为横轴残差真实值减去预测值为纵轴画散点图。理想状态下残差应该均匀分布在0刻度线上下呈随机噪声状。如果残差呈现出明显的形态比如随着预测值增大残差系统性上偏或下偏说明模型可能漏掉了某个关键特征或者数据中存在未处理的非线性关系。在我用随机森林做完预测后发现残差图整体比线性回归好很多但在高房价区域依然存在轻微的负偏差。也就是说模型对极高房价的街区倾向于低估。这个结论给了我一个业务上的解读数据里高房价街区的样本量偏少模型学习不充分后续如果有机会补充更多高端区域的数据模型表现还能进一步提升。5. 常见问题与避坑指南5.1 数据泄露模型表现好的假象我见过很多初学者犯这个毛病用全量数据做特征缩放做完再拆分训练集和测试集结果测试集上的成绩非常漂亮一部署到线上就崩百思不得其解。问题就出在数据泄露上——测试集的信息在训练时已经无意中被模型看到了。以特征缩放为例如果先用全量数据算出均值和标准差那这个均值和标准差实际上包含了测试集样本的信息。模型在训练时虽然没直接看到测试集的标签但在特征变换时已经借助了测试集数据的分布信息等于作弊。正确的做法是把特征缩放步骤封装好让它在训练集上学习参数在训练集和测试集上分别应用。5.2 特征缩放的必要性线性回归、SVM等模型对特征尺度敏感特征数值范围差异大将导致权重学习不稳定。但随机森林这类树模型天然不受特征缩放影响因为树的切分只依赖排序不依赖距离。问题来了既然随机森林不需要特征缩放是不是数据预处理里就可以省掉这一步在纯树模型里可以但如果你在pipeline里同时跑了线性模型和树模型来做对比那就不能省。所以我通常的习惯是同一份数据上如果既有线性模型又有树模型统一做标准化保证对比的公平性。from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline pipeline Pipeline([ (scaler, StandardScaler()), (model, RandomForestRegressor(random_state42, n_jobs-1)) ]) pipeline.fit(X_train, y_train)5.3 随机种子的作用与复现问题随机种子在机器学习项目里可以说是老生常谈了但实际中还是经常被忽略。不设置随机种子意味着每次运行代码时数据的拆分顺序、模型的随机初始化状态都不同实验就不可复现。我在调参阶段吃过这个亏。有一阵子我为了调随机森林的max_depth改完参数跑一遍指标忽上忽下完全找不到规律。后来才发现是train_test_split里没设置random_state。加上之后所有实验才变得可比。这里也建议你养成习惯所有涉及随机过程的代码都显式地设置随机种子。包括数据拆分、模型初始化、交叉验证的shuffle过程。这不是没事找事而是保证你在长时间调试时每一次实验的结果都能追溯到具体的代码和参数。5.4 解释模型时要小心过度解读前面提到随机森林的RMSE比线性回归低了0.1左右看起来效果好了一些但具体到业务层面意味着什么呢我需要提醒一点RMSE是平均意义上的指标它掩盖了不同价位段模型表现的巨大差异。我之前也说了模型在低房价区域的预测比较准在高房价区域偏差较大。如果你只给业务方看一个总体RMSE对方可能觉得模型相当不错但如果按房价区间拆分评估就会发现高价位段的误差是低价位段的几倍。对于主要关注高端市场的业务来说这个模型可能就不够用。所以评估模型时一定要按关键业务维度做分层评估比如按收入水平、按地理区域、按房价区间分别看误差。这样才能发现模型隐藏的短板也才能让业务方对模型的能力边界有一个清醒的认知。写在最后的一点体会这个项目做完之后我的感受是房价预测本身并不难真正的难点在于把每一个看似简单的环节做扎实。数据探索花的时间足够多特征工程做得有业务逻辑模型评估不只看单一指标最后的结果自然不差。如果你正在学机器学习建议别只停留在跑通代码。试着把数据换掉、把特征工程改一下、把评估指标多算几个整个过程多问自己为什么收获会大得多。这个案例虽然只是回归任务的入门但它涵盖的思路——问题定义、数据清洗、特征工程、模型对比、交叉验证——放到任何一个机器学习项目里都适用。把这套流程跑熟了后面遇到再复杂的问题你都不至于发怵。
返回列表