简介这是一个面向数据科学学习者的房价预测完整实践项目基于房产数据集围绕随机森林与支持向量机回归器展开建模并对房价对数预测和价格直接预测进行对比分析同时加入网格搜索超参数调优帮助读者系统掌握从数据预处理到模型评估的机器学习全流程。资源包共8个文件约2.62MB包含Python代码、Jupyter Notebook、PDF报告、CSV数据集、说明文档及配套附赠资料可满足代码复现、结果查看与理论研读多种需求。目前已有167人学习内容兼具理论与实践既适合初学者入门也可作为专业人员快速上手同类回归任务的参考。通过学习该案例读者能直观理解不同回归策略的差异学会用网格搜索优化模型并掌握误差分析与可视化评估方法是一份可直接用于动手练习的优质案例资源。1. 房价回归为什么绕不开对数变换先看分布再谈模型房产数据集的房价预测案例几乎每个数据科学学习者都做过但大多数人的第一版模型会翻车而且翻得很一致RMSE 大得离谱预测高价房偏差几十万低价房反而拟合得不错。原因不在随机森林或支持向量机回归器的参数上而在目标变量本身——房价的分布是严重右偏的长尾分布直接拿原始价格做回归模型会把大部分容量花在拟合少数千万级豪宅上忽略了几万到几十万的主流样本。这个案例的核心动作就是对数预测与价格直接预测的对比分析把目标变量做log1p变换再训练预测后expm1还原往往在 RMSE 上明显胜过直接预测。如果你正在准备数据科学方向的练习项目或者想搞清楚网格搜索超参数调优到底怎么设这个案例值得完整跑一遍。2. 把房产数据收拾到能训练缺失值、类别编码与不泄露未来的切分建模之前的所有工作都围绕一件事让X变成干净的数值矩阵让y保持原始价格。这一步没做扎实后面随机森林和支持向量机回归器再调参也救不回来。2.1 缺失值不能无脑填中位数先看缺失比例和字段含义先读数据用info()和isnull().mean()看整体情况。我见过不少练习者跳过这一步直接dropna()结果丢掉了几百条样本模型泛化能力明显变差也有人不管三七二十一全部填 0让面积、房龄这些字段出现大量无意义的值。import pandas as pd import numpy as np df pd.read_csv(housing.csv) print(df.shape) print(df.info()) print(df.isnull().mean().sort_values(ascendingFalse))这段代码的输出会告诉你三件事数据规模、字段类型、每个字段的缺失比例。缺失比例低于 5% 的数值列用中位数填充基本安全缺失比例高但字段本身有业务含义比如部分老房源没有装修年份可以单独生成一个“是否缺失”的辅助特征。num_cols df.select_dtypes(include[np.number]).columns cat_cols df.select_dtypes(include[object]).columns for col in num_cols: if df[col].isnull().mean() 0.3: df[col _isna] df[col].isnull().astype(int) df[col] df[col].fillna(df[col].median()) for col in cat_cols: df[col] df[col].fillna(df[col].mode()[0])这里对缺失超过 30% 的数值列额外生成_isna标记是树模型友好的做法。中位数填充的好处是抗离群值——房价数据的面积字段如果出现几个异常大值均值会被拉偏中位数不会。类别列用众数填充简单可靠。2.2 类别特征编码按基数选择 OrdinalEncoder 还是 OneHotEncoder房产数据里常见的类别字段有户型、朝向、所在城区。像“所在城区”这种基数可能十几甚至几十全量 OneHotEncoder 会让特征矩阵膨胀到几百列SVR 的训练时间会明显上升。from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder low_card_cols [house_type, orientation] high_card_cols [district] ord_enc OrdinalEncoder(handle_unknownuse_encoded_value, unknown_value-1) df[low_card_cols] ord_enc.fit_transform(df[low_card_cols]) one_hot OneHotEncoder(handle_unknownignore, sparse_outputFalse) district_encoded one_hot.fit_transform(df[[high_card_cols[0]]]) district_df pd.DataFrame( district_encoded, columnsone_hot.get_feature_names_out([high_card_cols[0]]) ) df pd.concat([df, district_df], axis1).drop(columnshigh_card_cols)一个常见误区是随机森林不需要 OneHotEncoder用 OrdinalEncoder 就够了。这个说法不完全对——树模型确实能从序数编码中做切分但如果类别之间没有天然的序关系比如 A、B、C 三个城区OrdinalEncoder 会强行引入“B 比 A 大”的假序。我的习惯是低基数类别用 OneHotEncoder高基数类别先看样本量样本够就用 OneHotEncoder样本不够就只用 OrdinalEncoder 并关掉树模型的max_depth上限。2.3 切分训练集与测试集回归任务也要防信息泄露这里有个容易被忽略的坑数据如果是按时间收集的比如二手房挂牌记录一定要关闭shuffle。否则随机打散后测试集里会出现“过去半年”的样本而训练集里有“未来半年”的样本模型等于偷看了答案。from sklearn.model_selection import train_test_split X df.drop(columns[price]) y df[price].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, shuffleFalse ) print(X_train.shape, X_test.shape, y_train.mean(), y_test.mean())shuffleFalse在时间序列语义的数据上是必须写的。如果数据没有时间列只是横截面数据可以打开shuffleTrue。切分后打印一下训练集和测试集的y均值如果相差超过 30%说明切分可能有问题或者数据本身有趋势性这种情况建议改用时间序列的切分方式而不是随机切分。3. 对数预测 vs 直接预测目标变量分布决定了模型在学什么这一章是整个案例的核心逻辑所在。标题里的“对数预测”不是简单的数据变换而是改变了回归器内部的误差函数结构。3.1 画直方图看右偏长尾均值和众数的差距就是问题本身先把价格分布画出来这一步值得做因为很多练习者到调完参都没看过目标变量的长什么样。import matplotlib.pyplot as plt import seaborn as sns plt.figure(figsize(10, 3)) sns.histplot(df[price], bins80, kdeTrue) plt.title(Raw price distribution) plt.show() price_log np.log1p(df[price]) plt.figure(figsize(10, 3)) sns.histplot(price_log, bins80, kdeTrue) plt.title(Log1p price distribution) plt.show() print(原始价格均值:, df[price].mean()) print(原始价格中位数:, df[price].median()) print(对数空间均值:, price_log.mean()) print(对数空间中位数:, price_log.median())如果原始价格的均值明显大于中位数说明存在少数超高房价把均值拉高。这种右偏分布下直接用 RMSE 做评估那些几百万的高价房会主导误差模型为了降低 RMSE 会拼命去拟合高价房而不是去拟合大多数普通房源。对数变换之后分布接近正态均值和中位数靠拢此时模型学的是相对误差。3.2 log1p 让回归从学绝对误差变成学相对误差为什么 SVR 和随机森林在变换后的目标上表现更好看误差函数的本质。原始空间里真实价 100 万、预测 120 万误差平方是 400 亿真实价 100 万、预测 110 万误差平方是 100 亿。模型会认为“差 20 万”比“差 10 万”严重四倍这在房价业务里不一定成立——低价房的 10 万误差可能意味着 20% 的偏差高价房的 20 万误差可能只有 10%。对数空间里log1p(100万) ≈ 13.8log1p(120万) ≈ 14.0差的平方在相对意义上被均匀化。这就是 RMSLERoot Mean Squared Logarithmic Error之所以适合房价预测的原因——它惩罚的是相对比例误差。3.3 expm1 还原价格编码与解码函数务必配对使用初学者最常见的翻车点在最后一步训练时用了np.log1p(y)预测完忘记np.expm1直接把对数结果当价格交出去导致预测值全部在个位数。def rmsle(y_true, y_pred_log): return np.sqrt(mean_squared_error(np.log1p(y_true), y_pred_log)) def rmse_original_space(y_true, y_pred_log): return np.sqrt(mean_squared_error(y_true, np.expm1(y_pred_log))) # 示例假设某回归器输出了对数空间的预测值 y_true_sample np.array([500000, 1000000, 2500000]) y_pred_log_sample np.array([13.1, 13.9, 14.5]) print(RMSLE:, rmsle(y_true_sample, y_pred_log_sample)) print(还原后 RMSE:, rmse_original_space(y_true_sample, y_pred_log_sample))log1p(x) log(x 1)对应expm1(x) exp(x) - 1中间那个1/-1是为了让函数在 0 附近数值稳定。房价不为负但如果新数据里出现 0 值log(0)会直接崩溃log1p不会。养成习惯训练前y_log np.log1p(y)预测后y_pred np.expm1(pred_log)两端函数必须配对。4. 随机森林回归基线网格搜索超参数调优与特征重要性随机森林是这类表格数据回归的首选基线模型原因很直接不需要特征缩放能捕捉非线性关系对离群值相对鲁棒。这一章直接预测原始价格拿一组可复现的网格搜索参数做调优。4.1 用默认参数先跑一版基线记录 RMSE调参前必须先有一个默认参数的结果否则网格搜索搜出来的提升你感受不到。随机森林的默认参数在几千样本的房产数据上通常表现尚可但max_depth不限制容易过拟合。from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error baseline_rf RandomForestRegressor( n_estimators200, max_depthNone, min_samples_leaf1, random_state42, n_jobs-1 ) baseline_rf.fit(X_train, y_train) y_pred_base baseline_rf.predict(X_test) rmse_base np.sqrt(mean_squared_error(y_test, y_pred_base)) print(默认参数随机森林 RMSE:, round(rmse_base, 2))n_estimators200是步数max_depthNone让树自由生长同时用min_samples_leaf1完全不过滤叶子这一版是“高方差”的基线。n_jobs-1用满所有 CPU 核心。记录下这个 RMSE后续网格搜索的结果必须明显低于它才有意义。4.2 网格搜索参数设定从粗到细控制在能等完的范围内网格搜索超参数调优的常见错误是参数网格太大导致几个小时跑不完。我的经验是先定三到四个关键参数每个参数给两三个候选值第一次跑粗网格找到最优区域后再在最优值附近做第二次细网格。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [200, 300], max_depth: [10, 20, None], min_samples_leaf: [1, 2, 4], max_features: [sqrt, 0.6] } rf RandomForestRegressor(random_state42, n_jobs-1) grid_search GridSearchCV( estimatorrf, param_gridparam_grid, scoringneg_root_mean_squared_error, cv5, n_jobs-1, verbose1 ) grid_search.fit(X_train, y_train) print(最优参数:, grid_search.best_params_) print(最优5折RMSE:, round(-grid_search.best_score_, 2))scoringneg_root_mean_squared_error是这里的关键。GridSearchCV 默认用r2分数但房价回归的实际业务关心的是误差绝对值用 RMSE 做评分更贴合业务目标。注意neg_前缀是因为 sklearn 的 scoring 遵循“越大越好”的约定RMSE 越小越好所以要取负。cv5是五折交叉验证verbose1会打印进度方便估算总耗时时长。粗网格搜索结束后我一般会把最优参数的邻居再跑一轮。比如max_depth20是最优那就再试[15, 20, 25]min_samples_leaf2是最优就试[2, 3, 5]。这轮细网格建议只调一到两个参数固定其他参数否则总组合数会再次爆炸。4.3 顺带输出特征重要性看看哪些字段在主导价格网格搜索完成后grid_search.best_estimator_就是调优后的模型。随机森林自带feature_importances_这是练习项目效果展示里最直观的一张表。feature_importance pd.Series( grid_search.best_estimator_.feature_importances_, indexX_train.columns ).sort_values(ascendingFalse) print(feature_importance.head(10).to_string())特征重要性之和为 1越靠前的字段对价格影响越大。这个输出的用处不只是展示还能帮你发现特征工程的问题如果某个业务上明显不重要的字段排名第一说明它可能泄露了价格信息比如“挂牌价”要回头检查数据。5. SVR 调参避坑指南三大翻车现场与排查顺序支持向量机回归器SVR在房价这种中等规模数据集上调好参数后精度往往不输随机森林但它对数据预处理和参数范围极其敏感。这一章既是 SVR 的完整实践也是一份避坑记录。5.1 翻车现场一没做特征缩放直接训 SVRRMSE 离谱现象SVR 在X_train上训练完毕后预测值几乎是一个常数RMSE 比随机森林高一到两个数量级。原因SVR 依赖样本之间的欧氏距离而房价数据里“面积”的量纲是几十到几百“卧室数”是 1 到 10“年份”是 1900 到 2025。这些量纲差异导致距离计算被“年份”这类大数值字段主导模型退化成只看单一维度的简单规则。解决训练前先做特征缩放。SVR 的标准做法是把所有数值特征缩放到均值为 0、方差为 1。from sklearn.svm import SVR from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline svr_pipeline Pipeline([ (scaler, StandardScaler()), (svr, SVR(kernelrbf)) ]) svr_pipeline.fit(X_train, y_train) y_pred_svr svr_pipeline.predict(X_test) print(SVR RMSE:, round(np.sqrt(mean_squared_error(y_test, y_pred_svr)), 2))把StandardScaler放进Pipeline而不是先单独fit_transform再喂给 SVR是为了防止数据泄露。如果用整个X去fit缩放器缩放器的均值和方差就包含了测试集的信息交叉验证时会虚高。管道会自动保证每一折交叉验证里缩放器只在训练折上拟合。5.2 翻车现场二C、epsilon、gamma 网格范围没设好结果没法用现象GridSearchCV 跑完了最优参数恰好落在网格的边界上比如C1000是上限gamma1是上限。这说明真实最优值在搜索范围之外当前网格就像一个没盖好的房子你刚好站在边缘。原因SVR 有三个关键超参数C 是正则化系数epsilon 是误差管的宽度gamma 是 RBF 核的宽度。它们的有效范围差异很大C 可以从 0.1 到 1000 甚至更大epsilon 通常只在 0.01 到 1 之间gamma 常见范围是 0.001 到 1。解决把三个参数分开设网格做一次先验估计。param_grid_svr { svr__kernel: [rbf], svr__C: [10, 50, 100, 200], svr__epsilon: [0.01, 0.05, 0.1, 0.2], svr__gamma: [0.01, 0.05, 0.1, 0.5] } svr_grid GridSearchCV( svr_pipeline, param_gridparam_grid_svr, scoringneg_root_mean_squared_error, cv5, n_jobs-1, verbose1 ) svr_grid.fit(X_train, y_train) print(SVR最优参数:, svr_grid.best_params_) print(SVR最优5折RMSE:, round(-svr_grid.best_score_, 2))svr__前缀是管道里步骤名的固定写法管道中用(svr, SVR())注册所以参数名就是svr__C。C 越大越容易过拟合C 越小越平滑epsilon 越大模型对微小误差越不在乎预测曲线越平滑但可能忽略真实变化gamma 越大核函数作用范围越小模型越复杂。第一次网格优先覆盖C: 10~200、epsilon: 0.01~0.2、gamma: 0.01~0.5这个先验区间。如果最优值又顶到边界再扩展但每次只扩一个参数。5.3 翻车现场三数据量一大GridSearchCV 时间爆炸现象样本量到一两万参数量稍微一多网格搜索直接跑数小时。原因SVR 的训练复杂度近似 O(n²) 到 O(n³)和随机森林完全不同。1 万样本的 RBF 核 SVR内部需要计算两两样本之间的核矩阵内存和时间开销都非常大。5 折交叉验证等于训练了 5 个 SVR再乘上网格里的组合数时间自然失控。解决先用小样本或随机搜索探路再全量训练。from sklearn.model_selection import RandomizedSearchCV import scipy.stats as stats param_dist_svr { svr__C: stats.expon(scale100), svr__epsilon: stats.uniform(0.01, 0.5), svr__gamma: stats.expon(scale0.5) } random_search_svr RandomizedSearchCV( svr_pipeline, param_distributionsparam_dist_svr, n_iter30, scoringneg_root_mean_squared_error, cv3, n_jobs-1, random_state42 ) X_sample, _, y_sample, _ train_test_split( X_train, y_train, test_size0.5, random_state42 ) random_search_svr.fit(X_sample, y_sample) print(随机搜索最佳参数:, random_search_svr.best_params_)先用一半的X_train跑随机搜索n_iter30表示随机尝试 30 组参数组合比全网格穷举快得多。找到大致区域后再围绕这个区域用小网格做精细搜索。另一个可选思路是换kernellinear线性核的计算复杂度低一个量级在样本量大、特征维度中等的情况下linear 核 SVR 的精度损失通常可以接受。5.4 排查顺序先缩放、再小样本试参数、最后上全量网格综合上面三个坑SVR 的完整排查顺序可以固定成一套流程。第一步检查特征缩放是否在管道内且跟随交叉验证第二步在原始价格或对数价格上先缩小样本量验证管道本身能工作看 RMSE 是否在一个合理量级第三步用随机搜索或粗网格确定参数最佳区域再用细网格精调。这套顺序每步都能快速暴露问题避免最后一刻才发现前面的缩放写错了。SVR 的高方差特性使得它在小样本上表现不稳定如果样本量只有几百条建议直接把kernel换成linear不要强行上 RBF。6. 最终效果展示四组实验在同一指标下的裁决随机森林直接预测、随机森林对数预测、SVR 直接预测、SVR 对数预测四组实验放到同一把尺子下量。这个环节是整个案例的收尾也是判断“对数预测 vs 直接预测”哪个更好的依据。先看评估指标的选择。直接预测的模型用原始价格空间的 RMSE对数预测的模型要把预测值expm1还原到价格空间再算 RMSE同时保留 RMSLE 作为对数空间的参考。表格按模型、目标变量、5 折 CV RMSE、RMSLE 四列展示。补上剩余两组实验from sklearn.ensemble import RandomForestRegressor from sklearn.svm import SVR from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_predict from sklearn.metrics import mean_squared_error # 随机森林 对数预测 rf_log RandomForestRegressor( n_estimators300, max_depth20, min_samples_leaf2, random_state42, n_jobs-1 ) y_log np.log1p(y) rf_log.fit(X_train, np.log1p(y_train)) pred_rf_log np.expm1(rf_log.predict(X_test)) # SVR 直接预测 svr_direct make_pipeline( StandardScaler(), SVR(kernelrbf, C100, epsilon0.1, gamma0.05) ) svr_direct.fit(X_train, y_train) pred_svr_direct svr_direct.predict(X_test)逻辑说明随机森林对log1p(y_train)拟合输出必须经过expm1还原SVR 做直接预测则不需要还原。这里固定了随机森林的网格搜索结果参数SVR 采用了上一轮随机搜索找到的近似最优值。关键点在于四组实验都使用同一个X_train/X_test切分RMSE 才能互相比较。最后画残差图这是效果展示里最直观的一部分plt.figure(figsize(8, 4)) plt.scatter( y_test, pred_rf_log, s8, alpha0.5, labelRF log1p ) plt.scatter( y_test, pred_svr_direct, s8, alpha0.5, labelSVR direct ) plt.plot( [y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--, linewidth1, labely x ) plt.xlabel(真实价格) plt.ylabel(预测价格) plt.legend() plt.title(预测值与真实值散点图) plt.show()散点越贴近红色对角线模型越准。低价段如果散点在对角线下方说明模型系统性低估高价段如果散点分散厉害说明模型对豪宅拟合不足。残差图能一眼看出对数预测模型在高价段的尾部表现是否比直接预测更稳。整个案例跑完我的习惯是保留每一个中间结果从默认基线到网格搜索最优参数再到最终对比全部记录成表格。这样以后换数据集做练习时可以直接对照旧结果判断新模型到底是真提升了还是随机波动。希望这篇笔记能帮你在之后自己跑这个练习项目时少走几步弯路把网格搜索超参数调优和对数预测这两件事真正用到自己的模型里。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?