首页 / 资讯中心 / 文章详情

机器学习房价预测实战:回归模型与特征工程全流程解析

机器学习房价预测实战:回归模型与特征工程全流程解析 ★ FEATURED ARTICLE
简介房价预测是机器学习回归任务中最典型的应用场景之一涉及从数据清洗、特征构造到模型训练与评估的完整流程。在真实项目中原始数据往往存在缺失值、异常值和分类编码问题需要通过中位数填充、One-Hot编码等预处理手段保证数据质量。针对高维特征和共线性问题岭回归通过L2正则化控制系数膨胀Lasso借助L1正则化实现特征选择而随机森林则能够捕捉非线性交互关系三者各有所长。进一步利用交叉验证和网格搜索可以稳定评估模型泛化能力并自动寻找最优参数。本文以一套可运行的房价预测项目源码为例系统演示回归建模的关键步骤与避坑经验适合需要快速构建回归预测原型的开发者参考。1. 机器学习房价预测一个能直接跑通的回归项目源码做房地产评估或者金融风控的同学大概率都遇到过同一个需求给你一批房屋特征让你快速估算出市场价格区间。手写回归公式不现实调接口又拿不到细粒度数据最好的办法就是自己训练一个模型。这套基于机器学习的房价预测系统源码就是干这个用的——它包含house_price_predict.py主脚本、train.csv训练集、test.csv预测集和一份README.md完整覆盖从数据预处理、特征工程到模型选型与预测输出的全流程而且项目本身就预设了岭回归、Lasso 和随机森林三条技术路线。适合正在做课程设计的学生、刚入门机器学习想找真实数据集练手的开发者以及需要快速搭建房价评估原型的业务方。代码基于 Python 和 scikit-learn 生态结构不复杂跑通之后改造成其他回归任务也很快。2. 数据预处理与特征工程先把 train.csv 里的脏数据变干净2.1 加载数据与字段探查别急着训练先看数据长什么样很多人拿到源码第一件事就是跑python house_price_predict.py结果要么报错要么预测结果离谱。我一般会先打开 train.csv 看一眼字段结构。以常见房价数据集为例通常包含面积、卧室数、楼层、房龄、朝向、所在区域这类字段还有最终的价格标签列。先探查再动手能省掉后面大量排错时间。import pandas as pd train_df pd.read_csv(train.csv, encodingutf-8) test_df pd.read_csv(test.csv, encodingutf-8) print(train_df.shape) print(train_df.info()) print(train_df.describe(includeall).T) print(train_df[价格].describe())这段代码做了三件事输出训练集维度、逐列打印数据类型和非空计数、给出数值列的统计分布以及价格标签的分布概况。train_df.info()能快速暴露缺失值——如果某个字段的Non-Null Count明显小于总行数说明该列存在空值describe(includeall)则能让你一眼看到哪些列是数值型、哪些是对象型也就是需要编码的分类特征。价格列的min和max异常悬殊时要警惕极端值对模型的影响。2.2 缺失值、异常值处理与分类特征编码按列类型选策略探查完之后进入数据清洗。缺失值的填充策略不能一刀切数值型字段用中位数或均值填充分类字段用众数填充异常值则要结合业务逻辑判断——比如面积字段出现 3 平方米的记录那明显是录入错误直接剔除比硬留着训练更安全。import numpy as np for col in train_df.columns: if train_df[col].isnull().sum() 0: if train_df[col].dtype in [int64, float64]: median_val train_df[col].median() train_df[col].fillna(median_val, inplaceTrue) test_df[col].fillna(median_val, inplaceTrue) else: mode_val train_df[col].mode()[0] train_df[col].fillna(mode_val, inplaceTrue) test_df[col].fillna(mode_val, inplaceTrue) train_df train_df[(train_df[面积] 5) (train_df[面积] 1000)] train_df train_df[train_df[价格] 0]这里的关键点是填充缺失值时训练集和测试集必须用同一个统计值否则模型在预测阶段会看到分布不同的数据性能直接打折。异常值过滤我用的是面积上下界和价格正数约束实际项目中你需要根据数据集的业务含义调整阈值而不是盲目套用。分类特征编码我放在下一步统一处理因为编码前需要先统一训练集和测试集的类别集合。分类特征的编码常见做法是LabelEncoder或OneHotEncoder。对于房价预测这种场景像朝向、所在区域这类无序分类字段用 One-Hot 编码更合适因为LabelEncoder会给类别强加大小关系模型会以为数字大的朝向更值钱这就是典型的编码陷阱。from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer categorical_cols train_df.select_dtypes(include[object]).columns.tolist() numeric_cols train_df.select_dtypes(include[int64, float64]).columns.tolist() if 价格 in numeric_cols: numeric_cols.remove(价格) preprocessor ColumnTransformer([ (num, passthrough, numeric_cols), (cat, OneHotEncoder(handle_unknownignore), categorical_cols) ])ColumnTransformer的好处是把数值列原样保留、分类列自动做 One-Hot整个预处理流程封装成一个对象后面可以直接塞进 Pipeline。handle_unknownignore这个参数很重要当 test.csv 里出现训练集中没见过的类别时模型不会崩溃而是把这个未知类别全置为 0。如果你的项目里测试集和训练集来自不同时间批次这个参数几乎是必选的。2.3 构造新特征把面积单价和房龄权重加进去特征工程是房价预测项目里性价比最高的环节。原始字段直接喂给模型也能跑但效果一般。我常用的两个特征衍生方向一是组合特征比如单价 价格 / 面积这能消除面积对总价的干扰让模型学到地段和品质的真实权重二是对房龄做分段处理因为房龄对房价的影响不是线性的——新盘和 20 年老破小的价格差远比 5 年和 10 年的差距大。train_df[房龄_分段] pd.cut(train_df[房龄], bins[0, 5, 15, 30, 100], labels[0, 1, 2, 3]) train_df[面积_单价] train_df[价格] / train_df[面积] test_df[房龄_分段] pd.cut(test_df[房龄], bins[0, 5, 15, 30, 100], labels[0, 1, 2, 3]) test_df[面积_单价] test_df[价格] / test_df[面积]这里的pd.cut把房龄离散成 4 个档位让模型能捕捉到非线性关系——比如 0~5 年的次新房溢价最高15~30 年的老房价格趋平。需要提醒的是面积_单价这个特征在训练阶段用了价格标签如果你打算用同样的流程处理 test.csv那 test.csv 里没有价格列这个特征得用「成交总价 / 面积」的均值替代或者直接不构造这个特征只把房龄_分段送入模型。特征构造的原则是训练和预测时的特征集必须完全一致否则模型 inference 阶段直接报 feature mismatch 错误。3. 三个回归模型选型岭回归、Lasso 与随机森林的取舍3.1 为什么要在这三个模型之间做选择房价预测本质上是一个高维回归问题特征之间往往存在共线性——面积和卧室数强相关地段等级和单价强相关。线性回归在这种场景下容易过拟合所以项目源码选型时用了两个正则化线性模型加一个树模型的组合岭回归用 L2 正则化收缩系数适合特征间相关性高的情况Lasso 用 L1 正则化把不重要的特征系数压到 0相当于内置了特征选择随机森林则不假设线性关系能捕捉特征交互和高阶非线性。三个模型覆盖了线性、稀疏、非线性三条路线对比之后选最优这个思路值得借鉴。模型正则化类型擅长场景主要超参数岭回归L2特征共线性高、特征数中等alphaLassoL1特征稀疏、希望自动选特征alpha随机森林无基于树非线性关系、特征交互复杂n_estimators, max_depth, max_features3.2 训练与评估流程用 Pipeline 串起流程避免数据泄露源码里三个模型共用一个数据预处理 训练 评估的框架我这里用一个统一的 Pipeline 实现。Pipeline 的核心价值在于预处理器的 fit 只在训练集上执行然后原样应用到测试集不会因为手写代码的顺序问题导致验证集信息泄露进训练过程。from sklearn.pipeline import Pipeline from sklearn.linear_model import Ridge, Lasso from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, r2_score X train_df.drop(价格, axis1) y train_df[价格] X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42) models { ridge: Pipeline([ (preprocessor, preprocessor), (regressor, Ridge(alpha1.0)) ]), lasso: Pipeline([ (preprocessor, preprocessor), (regressor, Lasso(alpha0.1)) ]), rf: Pipeline([ (preprocessor, preprocessor), (regressor, RandomForestRegressor(n_estimators100, max_depth10, random_state42)) ]) } for name, model in models.items(): model.fit(X_train, y_train) y_pred model.predict(X_val) rmse mean_squared_error(y_val, y_pred, squaredFalse) r2 r2_score(y_val, y_pred) print(f{name:6s} RMSE{rmse:.2f} R2{r2:.4f})test_size0.2表示从训练集里切出 20% 作为验证集用来评估模型泛化能力。random_state42固定随机种子保证每次运行切分结果一致这是复现实验结果的基本要求。RMSE 的单位和价格一致能直观看出预测误差在什么量级R2 越接近 1 说明模型解释力越强。注意 Lasso 的 alpha 默认情况下如果设得比较大系数会被压得过狠模型可能把所有特征都忽略掉R2 直接变成负值——遇到这种情况优先调小 alpha。3.3 参数怎么调先粗后细网格搜索兜底三个模型跑完你大概率能看到随机森林在验证集上表现最好Lasso 最差。这不代表 Lasso 没用——如果训练集特征维度上千Lasso 反而可能是最稳的。调参的思路是先定范围再做网格搜索。岭回归的alpha从[0.01, 0.1, 1, 10, 100]里选Lasso 的alpha从更小的量级开始试因为 L1 正则化对 alpha 更敏感随机森林先固定n_estimators500然后调max_depth和max_features树的数量不是越大越好到一定规模后收益递减训练时间却线性增长。4. 交叉验证与网格搜索调参别拿 test.csv 当调参工具4.1 用 KFold 做交叉验证单一验证集的结果不够稳上一章用train_test_split切了一次验证集这只能说明模型在当前这 20% 数据上表现好换个切分方式结果可能漂移很大。更稳的做法是 K 折交叉验证把训练集均分成 5 份轮流拿 1 份做验证、其余 4 份训练最终取 5 次 RMSE 的均值。这样每个样本都被验证过一次评估结果更接近真实泛化水平。from sklearn.model_selection import KFold, cross_val_score kfold KFold(n_splits5, shuffleTrue, random_state42) cv_scores cross_val_score(models[rf], X, y, cvkfold, scoringneg_mean_squared_error) cv_rmse np.sqrt(-cv_scores) print(fRF CV RMSE: {cv_rmse.mean():.2f} (/- {cv_rmse.std():.2f}))这里用neg_mean_squared_error作为 scoring 是因为 scikit-learn 的交叉验证约定「得分越高越好」所以把 MSE 取负号。np.sqrt(-cv_scores)把负的 MSE 转回正的 RMSE。输出格式里的/-是标准差如果标准差比均值还大说明模型在不同数据子集上表现极不稳定这时候优先检查特征是否包含高杠杆异常点而不是急着调参。4.2 GridSearchCV 找最优参数把调参做成自动化手动一组一组试参数效率太低源码场景下最合适的工具是GridSearchCV。它会自动遍历所有参数组合在每一折交叉验证上评估最后返回最优参数和最优点对应的得分。from sklearn.model_selection import GridSearchCV param_grid { regressor__n_estimators: [100, 300, 500], regressor__max_depth: [5, 10, 20], regressor__max_features: [0.5, 0.7, sqrt] } grid_search GridSearchCV( models[rf], param_grid, cv5, scoringneg_mean_squared_error, n_jobs-1 ) grid_search.fit(X_train, y_train) print(grid_search.best_params_) print(np.sqrt(-grid_search.best_score_))注意参数名里的regressor__前缀这是 Pipeline 的命名规则——regressor是 Pipeline 里第二步的名称后面跟参数名。如果写成n_estimatorsGridSearchCV 会报错找不到参数。n_jobs-1表示用全部 CPU 核心并行训练节省时间。max_features设为 0.5 表示每棵树的特征采样比例是 50%设为sqrt则表示取特征总数的平方根这两种策略能有效降低树之间的相关性让随机森林的集成效果更好。提示网格搜索得到的最优参数是在验证集上选出来的最后需要用全部训练数据重新拟合一次模型再去做真正的预测否则会低估新数据上的误差。5. 避坑记录房价预测项目最常见的五个翻车点5.1 数据读取与编码阶段的坑坑一CSV 文件读进来中文乱码。现象pd.read_csv(train.csv)后字段名变成一串乱码或者数据里的中文区域名全部显示为???。原因CSV 文件实际编码不是 UTF-8可能是 GBK 或 GB2312。解决先用open(train.csv, rb).read()看二进制开头或者直接尝试encodinggbk、encodinggb18030。血泪经验Windows 下从 Excel 导出的 CSV 绝大多数是 GBK 编码。坑二OneHotEncoder 报错Found unknown categories。现象训练集拟合正常处理 test.csv 报错。原因test.csv 里存在训练集没出现过的类别比如训练集里只有「东城、西城」测试集里冒出一个「南城」。解决给 OneHotEncoder 加handle_unknownignore或者在预处理前用pd.concat把训练集和测试集的分类列合并后统一取类别集合。坑三pd.cut的区间边界对不上。现象训练集构造房龄_分段顺利预测时报ValueError: bins must increase monotonically或标签数量不一致。原因pd.cut在不指定labels时会自动生成区间训练集和测试集的最小/最大值不同生成的区间数量可能不一样。解决像 2.3 节那样显式指定bins和labels两边用同一套边界。5.2 模型训练与评估阶段的坑坑四随机森林不设random_state每次结果都不一样。现象同一个脚本跑两次RMSE 和 R2 差距明显。原因随机森林的树生成过程、训练集切分都依赖随机数不固定种子等价于每次用不同的随机序列。解决在所有涉及随机过程的环节显式传random_state42——train_test_split、KFold、RandomForestRegressor、GridSearchCV都要设。这不只是复现问题更是你调参时判断参数效果是否真的变好的前提。坑五拿 test.csv 当验证集反复调参。现象模型在训练集和验证集上 R2 都很高一上测试集就崩。原因你已经在测试集上演化了很多次模型参数间接记住了测试集的信息测试集失去评估意义——这就是数据泄露的一种隐蔽形式。解决test.csv 只允许在最终预测阶段使用一次调参过程全部用交叉验证或者从 train.csv 切出的验证集。我见过很多课程设计翻车都是栽在这一条上。6. 进阶验证用学习曲线和特征重要性把模型调到能交付6.1 学习曲线判断欠拟合还是过拟合网格搜索找到最优参数后别急着收工。先用学习曲线判断当前模型的状态随着训练样本量增加训练集得分和验证集得分是否收敛到一起。如果两条曲线之间始终有一条宽沟说明模型过拟合要增加正则化强度或减少特征如果两条曲线贴在一起但得分都不高说明欠拟合要增加模型复杂度或构造更多特征。from sklearn.model_selection import learning_curve train_sizes, train_scores, val_scores learning_curve( models[rf], X, y, cv5, train_sizesnp.linspace(0.2, 1.0, 5), scoringneg_mean_squared_error ) train_rmse np.sqrt(-train_scores.mean(axis1)) val_rmse np.sqrt(-val_scores.mean(axis1)) print(train sizes:, train_sizes) print(train RMSE:, np.round(train_rmse, 2)) print(val RMSE:, np.round(val_rmse, 2))train_sizes从 20% 到 100% 分 5 档逐步增加训练数据观察两条 RMSE 曲线的变化趋势。如果训练 RMSE 低、验证 RMSE 高且随数据量增加缓慢收敛属于典型高方差可以砍特征或加大alpha如果两条线都高且平行属于高偏差需要换更强的模型或加特征。这一步能帮你区分「调参没用」和「还没调到位」。6.2 特征重要性排序砍掉噪音特征随机森林自带feature_importances_属性能直接输出每个特征对预测的贡献度。这一步的实际价值是把重要性接近 0 的特征从预处理流程里删掉重新训练模型。特征少了模型方差降低训练时间缩短线上推理也更快。但注意线性模型的系数不能直接当重要性用——岭回归和 Lasso 的系数大小受特征量纲影响要先标准化再比较。rf_model models[rf].named_steps[regressor] encoder models[rf].named_steps[preprocessor] cat_features encoder.named_transformers_[cat].get_feature_names_out() all_features np.r_[numeric_cols, cat_features] importance pd.Series(rf_model.feature_importances_, indexall_features) print(importance.sort_values(ascendingFalse).head(10))这里有个细节预处理后的特征顺序是ColumnTransformer里拼接的——先是数值列后面是 One-Hot 展开的分类列。直接用get_feature_names_out()能拿到编码后的完整特征名避免特征名对不上重要性排序的情况。删特征时可以只保留累计重要性达到 95% 的前几个特征重新跑一遍交叉验证你会发现 RMSE 可能不升反降。6.3 用最终模型批量预测并输出结果验证工作全部完成后用最优参数在完整训练集上重新拟合然后对 test.csv 做预测输出成标准的提交格式。这里要注意test.csv 没有价格列前面构造面积_单价特征时需要特殊处理或者直接丢弃这个特征。final_model grid_search.best_estimator_ final_model.fit(X, y) X_test test_df.copy() if 价格 in X_test.columns: X_test X_test.drop(价格, axis1) y_test_pred final_model.predict(X_test) output_df pd.DataFrame({ id: range(len(y_test_pred)), predicted_price: y_test_pred }) output_df.to_csv(submission.csv, indexFalse, encodingutf-8-sig) print(output_df.head(10))utf-8-sig编码会在文件开头写入 BOM 标记这样用 Excel 打开 CSV 时中文不会乱码这是从踩坑里总结出来的习惯。输出结果里如果出现负的预测价格说明模型的训练数据或者特征构造有问题——房价不可能是负数检查是否有异常值在预处理阶段漏掉了。从那以后我每次跑完预测第一件事就是看predicted_price的最小值和分布确认没有负价格或者极端异常值再走下一版迭代。整个流程从头到尾强制走一遍数据探查、交叉验证、学习曲线、特征重要性排序确认没问题再交付希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站