ARTICLE DETAIL

资讯详情

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

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

机器学习房价预测实战:特征工程与回归模型完整流程 简介Python机器学习实战房价预测项目是一套面向计算机专业学生课程设计、期末大作业与实战练习的高分参考案例适合具备一定Python基础、希望系统掌握回归预测流程的读者。资源围绕北京二手房价格预测展开完整覆盖爬虫获取链家与安居客数据、数据清洗、特征选择、Scikit-learn建模、交叉验证、超参数调整与模型评估等环节并附详细代码注释、README使用说明和可视化分析结果便于复现与二次开发。全套共26个文件包含15个Python脚本、Jupyter Notebook、CSV数据文件、HTML可视化报告及多张分析图片压缩包约1.28MB目录中spiders文件夹单独提供爬虫源码结构清晰可按需取用。目前已有103人学习对需要快速搭建机器学习项目框架、理解真实数据建模全过程的学习者而言具有较高的参考价值。1. 房价预测项目在解决什么一个回归问题的高分模板房价预测是机器学习入门里少有的“一次性把完整流程走通”的项目。它不涉及图像、文本这类需要专门前置知识的输入数据就是一张表格面积、卧室数、地理位置、房龄再加一个目标列——价格。你拿到手的每件事从数据清洗到特征工程、模型选型、交叉验证、结果解释都是真实工业项目里每天在做的动作。所以它既是课程作业里的常客也是面试时最能体现“你真的动手跑过模型”的项目。本文打算按一套可复制的流程把它完整落地过程中你会看到我是怎么处理缺失值、怎么切分训练集、为什么选这个模型不选那个、以及哪些坑是数据里自带、哪些是自己踩出来的。这套流程跑通之后换任何回归类数据集都能直接复用。2. 数据清洗与特征工程房价预测里最容易偷走精度的五个环节2.1 拿到数据集先看一眼用 pandas 做一次快速体检房价预测最经典的入门数据集是波士顿房价但它已经被 scikit-learn 移除了现在更常见的选择是加州房价数据集California Housing或者从 Kaggle 上下载的 House Prices 数据集。无论用哪个第一步动作都一样用pandas把数据读进来先不急着建模先看结构。import pandas as pd df pd.read_csv(housing.csv) print(df.shape) # 行列数 print(df.info()) # 每列的非空计数和数据类型 print(df.describe()) # 数值列的均值、标准差、四分位数 print(df.isnull().sum()) # 每列缺失值数量df.shape告诉你数据规模。df.info()是最值得看的输出如果某一列的非空计数小于总行数说明有缺失如果某列显示 object 类型但实际是数值说明类型需要转换。df.describe()里重点看min和max如果某一列的最大值比 75 分位数大了几个数量级基本可以判断有离群点。这段“体检”花不了两分钟但它决定了后面每一步怎么走跳过它直接建模属于自己给自己埋雷。2.2 缺失值处理不要一上来就 dropna房价数据里最常见的缺失出现在分类特征上比如“游泳池”这一列很多房子没有记录就是空值。很多新手第一反应是df.dropna()把有缺失的行删掉但这在房价预测里往往是有害的——缺失本身可能就是信息。比如某列表示“是否有地下室”缺失大概率意味着没有地下室而不是数据录入错误。# 不推荐直接删行 # df df.dropna() # 推荐按特征含义分别处理 df[PoolQC] df[PoolQC].fillna(None) # 分类特征缺失当成“无” df[LotFrontage] df[LotFrontage].fillna(df[LotFrontage].median()) # 数值特征用中位数填充分类特征填充成None而不是删除是为了让模型知道“这一项不存在”这个事实。数值特征用中位数填充而不是均值是因为中位数对离群点不敏感如果某块地的临街宽度有个超大离群值均值会被拉偏中位数不会。填充之后记得重新检查一次isnull().sum()确保没有遗漏的列。2.3 数值特征里的离群点房价预测的隐藏刺客describe()里如果看到max值异常大比如面积最大 10000 平米而 75 分位数只有 1500 平米不要直接删掉。先确认这是真实数据还是录入错误。常见做法是画箱线图或看分布但更快的办法是用领域常识判断一个住宅的面积超过 5000 平米在大部分城市数据集里就是极端离群点它会把线性模型的系数拉偏。# 用 IQR 方法标记离群点 Q1 df[GrLivArea].quantile(0.25) Q3 df[GrLivArea].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR outliers df[(df[GrLivArea] lower_bound) | (df[GrLivArea] upper_bound)] print(f检测到 {len(outliers)} 个离群点)IQR 方法只是一个标记工具标记完之后要人工看一遍这些行而不是直接批量删除。有些“离群点”其实是豪宅区的真实样本删掉反而让模型失去对高端市场的拟合能力。我的习惯是如果离群点数量占总数 1% 以下且不符合领域常识才删除否则做 log 变换压制它。2.4 特征工程组合特征比调参更值钱Kaggle 上房价预测比赛的经验反复验证一件事前几名和后面名次的差距主要来自特征工程而不是模型选择。比如“总面积”通常比“地上面积”和“地下室面积”分开两个特征更有预测力因为房价本质上由总使用面积驱动。再比如“房龄”这个特征很多模型对线性关系不敏感但房价和房龄往往是分段关系——新房价格高、老房子价格高历史建筑、中间段便宜。# 组合特征总面积 df[TotalSF] df[TotalBsmtSF] df[1stFlrSF] df[2ndFlrSF] # 房龄分桶把连续值变成有序分类 df[HouseAgeBin] pd.cut( df[HouseAge], bins[0, 10, 30, 50, 200], labels[new, mid, old, historic] ) # 对偏态严重的数值特征做 log 变换 import numpy as np df[GrLivArea_log] np.log1p(df[GrLivArea])TotalSF这类组合特征能直接提升模型上限因为它把三个原本独立的特征合并成了一个业务上更合理的变量。pd.cut分桶的作用是让树模型更容易找到切分点线性模型也受益于分段关系。np.log1p之后记得预测完价格要np.expm1变回来这个细节最容易忘。2.5 分类特征编码One-Hot 和 Label Encoding 的选择房价数据集里的分类特征分两种无序的如房屋风格、外墙材料和有序的如质量评级 1-5、地下室完成度。无序的用 One-Hot 编码有序的用 Label Encoding 保留等级关系。把质量评级做成 One-Hot 是浪费把房屋风格做成 Label Encoding 是误导——模型会以为风格 A 和风格 B 之间有数值上的远近关系。# 无序分类特征One-Hot df pd.get_dummies(df, columns[HouseStyle, Exterior1st], drop_firstTrue) # 有序分类特征Label Encoding先映射成数值 quality_map {Ex: 5, Gd: 4, TA: 3, Fa: 2, Po: 1} df[OverallQual_num] df[OverallQual].map(quality_map)drop_firstTrue的作用是去掉 One-Hot 之后的第一列避免多重共线性——如果不 drop这组哑变量加起来恒等于 1线性模型会报奇异矩阵或产生不稳定的系数。有一个需要记住的细节如果后续要跑模型多次pd.get_dummies会产生列数变化最好在训练前一次性完成并保存列名测试集进来时用reindex对齐防止训练集和测试集特征列不一致。3. 模型选型与训练从线性回归到集成模型的完整对比3.1 为什么要先跑线性回归基线模型的诊断价值很多初学者拿到数据就直接上 XGBoost这其实跳过了最重要的一步——建立基线。线性回归跑一遍不仅能给你一个可对比的下限还能通过系数和残差暴露数据问题。比如某个特征的系数为负且不符合业务直觉说明特征之间有共线性残差如果随预测值增大而增大说明存在异方差log 变换没做够。from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score X df.drop(SalePrice, axis1) y df[SalePrice] # 先把特征矩阵转成数值类型保证能送进模型 X X.select_dtypes(include[np.number]) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) lr LinearRegression() lr.fit(X_train, y_train) y_pred lr.predict(X_test) print(fRMSE: {np.sqrt(mean_squared_error(y_test, y_pred)):.2f}) print(fMAE: {mean_absolute_error(y_test, y_pred):.2f}) print(fR2: {r2_score(y_test, y_pred):.4f})random_state42固定下来保证每次切分一致这是所有对比实验的前提——如果你每次切分都变模型之间的差距到底是模型差异还是数据差异就说不清了。select_dtypes把非数值列过滤掉是防止 One-Hot 编码漏了某些列导致报错。线性回归的 R² 在房价预测上通常能到 0.7 左右如果你跑出来远低于这个值先别急着换复杂模型回头检查数据清洗。3.2 决策树和随机森林为什么树模型适合房价这类表格数据线性回归假设特征和目标之间是线性关系但房价和很多特征的关系是非线性的。比如“地理位置”对房价的影响在某个区域边界上可能陡然变化线性模型拟合不了这种跳变。决策树天然处理非线性而随机森林通过 bagging 和多棵树的投票把单棵树的过拟合压下去。from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators200, max_depth15, min_samples_leaf3, n_jobs-1, random_state42 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) print(fRF RMSE: {np.sqrt(mean_squared_error(y_test, y_pred_rf)):.2f}) print(fRF R2: {r2_score(y_test, y_pred_rf):.4f})n_estimators200是一个性价比很高的值再大收益有限但耗时翻倍。max_depth15防止单棵树长得太深把训练集背下来。min_samples_leaf3保证叶子节点至少有 3 个样本这是一个比max_depth更有效的正则化手段。随机森林还有两个免费福利特征重要性可以直接从模型里取缺失值可以用SimpleImputer在 pipeline 里处理而不用提前填充。跑完之后对比线性回归的结果你会发现 RMSE 有明显下降这就是“更合适的模型假设”带来的收益。3.3 梯度提升树XGBoost 和 LightGBM 的实战用法梯度提升树是 Kaggle 表格类比赛的常胜将军它和随机森林的区别在于随机森林并行地建多棵树然后投票梯度提升是串行地建树每棵树拟合前一棵树的残差。这也让它在房价预测上通常比随机森林再低 5% 到 10% 的误差代价是调参空间更大起步门槛更高。from xgboost import XGBRegressor xgb XGBRegressor( n_estimators300, max_depth5, learning_rate0.05, subsample0.8, colsample_bytree0.8, random_state42 ) xgb.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse ) y_pred_xgb xgb.predict(X_test) print(fXGB RMSE: {np.sqrt(mean_squared_error(y_test, y_pred_xgb)):.2f}) print(fXGB R2: {r2_score(y_test, y_pred_xgb):.4f})learning_rate0.05配合n_estimators300是一个经典组合学习率小树的数量就要多否则欠拟合。subsample0.8让每棵树只用 80% 的样本colsample_bytree0.8让每棵树只用 80% 的特征这两个参数是 XGBoost 防过拟合的关键默认值 1 在数据量小的时候特别容易过拟合。eval_set传入测试集可以在训练过程中监控是否过拟合——如果训练误差持续下降但测试误差开始反弹就说明该 early stopping 了。XGBoost 和 LightGBM 的取舍数据量小于 1 万行、特征数几十个时两者差距不大XGBoost 设置更直观数据量到了几十万行LightGBM 的直方图算法训练速度快很多。房价预测这种几千行的数据集选哪个都行关键是参数要调到位。3.4 训练集和测试集的切分时间序列数据不能随机切房价数据如果是截面数据同一时间点上的多个样本随机切分没问题。但如果你的数据带时间戳比如记录了不同年份的交易记录随机切分就是错的——模型看到了未来数据评估结果虚高部署到真实场景立刻现原形。# 带时间戳的数据按时间切分 train df[df[YrSold] 2008] test df[df[YrSold] 2008] X_train, y_train train.drop(SalePrice, axis1), train[SalePrice] X_test, y_test test.drop(SalePrice, axis1), test[SalePrice]按时间切分保证训练集里所有样本的时间都早于测试集模型在评估时面对的是真正“没见过”的时段。一个经验准则是如果时间特征如年份在特征重要性里排进了前五那说明价格随时间有明显趋势此时更该检查切分方式是否合理。预测未来房价时市场趋势类特征如利率、区域均价往往比房屋本身属性更重要但这类特征在静态数据集里往往缺失这是数据集的天花板不是模型能突破的。4. 参数调优与交叉验证把模型从“能跑”调到“能用”4.1 交叉验证一次性把数据用好只有一份数据集既要训练又要评估最常见的尴尬是训练集上 RMSE 很低测试集上很高也就是过拟合。交叉验证把数据切成 K 份每次用 K-1 份训练、1 份验证轮流 K 次最后的分数是 K 次平均值。它比单次切分更稳定不会因为你运气差切到了难样本而误判模型好坏。from sklearn.model_selection import cross_val_score # 5 折交叉验证 scores cross_val_score( xgb, X, y, cv5, scoringneg_root_mean_squared_error ) print(fCV RMSE 均值: {-scores.mean():.2f}) print(fCV RMSE 标准差: {scores.std():.2f})scoringneg_root_mean_squared_error里为什么是负值因为 sklearn 的评分函数统一是“越大越好”RMSE 本身是越小越好所以取负号转换。如果你关心的是“模型在不同数据子集上表现是否稳定”看标准差——标准差太大说明模型对数据敏感可能需要正则化或更多数据。K 的选择K5 是默认值K10 方差更小但耗时翻倍。数据量小于 500 行时建议用 K10因为每个验证集样本太少单次评估噪声太大。数据量上万后 K5 足够。还有一种极端情况数据集类别极其不平衡此时用分层交叉验证StratifiedKFold保证每折类别比例一致不过房价预测是回归问题用不上分层直接用普通 K 折。4.2 网格搜索和随机搜索什么时候用哪个网格搜索是穷举参数组合随机搜索是在参数分布里采样。网格搜索的优势是确定性强缺点是维数爆炸——5 个参数每个 5 个候选值就是 3125 种组合每种组合要跑 5 折交叉验证等于训练 15625 次模型。随机搜索在参数空间更大时更划算它不保证找到全局最优但通常能用少得多的训练次数找到“足够好”的参数。from sklearn.model_selection import GridSearchCV param_grid { max_depth: [3, 5, 7], learning_rate: [0.01, 0.05, 0.1], subsample: [0.7, 0.8, 0.9] } grid_search GridSearchCV( XGBRegressor(random_state42), param_grid, cv5, scoringneg_root_mean_squared_error, n_jobs-1 ) grid_search.fit(X_train, y_train) print(f最佳参数: {grid_search.best_params_}) print(f最佳分数: {-grid_search.best_score_:.2f})n_jobs-1让所有 CPU 核心并行工作网格搜索是天然并行的不用白不用。搜索结束后用best_params_里的参数重新在完整训练集上训练一次模型而不是直接用grid_search.best_estimator_——虽然两者结果接近但自己重新 fit 一次逻辑更清晰心智负担更小。另外注意网格搜索的scoring和最终评估的指标必须一致否则你选出的“最佳”参数可能在你真正关心的指标上并不是最佳。4.3 学习曲线分清欠拟合还是过拟合调参调了半天模型分数上不去此时最该画一张学习曲线。横轴是训练样本数纵轴是误差画两条线训练误差和验证误差。如果两条线在高处平行且差距很小说明欠拟合加数据没用换更复杂的模型或加特征。如果训练误差很低但验证误差高说明过拟合加数据有用或者增强正则化。from sklearn.model_selection import learning_curve train_sizes, train_scores, valid_scores learning_curve( xgb, X, y, train_sizes[100, 300, 500, 800, 1000], cv5, scoringneg_root_mean_squared_error ) train_rmse -train_scores.mean(axis1) valid_rmse -valid_scores.mean(axis1) for size, tr, va in zip(train_sizes, train_rmse, valid_rmse): print(f样本数 {size:4d} | 训练 RMSE {tr:.2f} | 验证 RMSE {va:.2f})一个典型输出是样本数从 100 增加到 1000训练 RMSE 从 20000 涨到 30000验证 RMSE 从 50000 降到 35000。这说明当前瓶颈是数据量不够而不是模型不够复杂继续堆参数意义不大去收集更多数据或做更狠的特征工程才有用。学习曲线能帮你把调参时间花在正确的地方这是我觉得整个调参环节里最值得先做的一步。4.4 特征重要性模型告诉你的和你想的往往不一样树模型训练完之后feature_importances_是一个免费的分析工具。它告诉你模型在分裂时主要依赖哪些特征。这一步的意义不只是“看看哪些特征重要”更重要的是发现数据里潜在的问题比如某个业务上很重要的特征重要性很低可能原因是编码方式不对或缺失严重。importances xgb.feature_importances_ feature_names X_train.columns # 按重要性排序打印前 15 个 indices np.argsort(importances)[::-1][:15] for i in indices: print(f{feature_names[i]}: {importances[i]:.4f})拿到之后先做一件事把 top 5 特征和你的业务直觉对照。如果“总体质量”和“总面积”排在前列正常如果某个无关紧要的分类哑变量排第一你就要检查是不是 One-Hot 之后某个类别占的比例极不均衡导致树模型总在这个特征上分裂。特征重要性还有一个实际用途如果删掉重要性后 50% 的特征后模型分数下降不超过 2%就可以删换来更快的训练速度和更简单的模型。5. 避坑房价预测项目的高频翻车现场与排查手册5.1 训练集和测试集特征列不一致现象训练时一切正常测试时predict直接报错ValueError: Number of features of the model must match the input。原因训练集和测试集做 One-Hot 编码时某个分类特征的类别不完全一致——测试集里出现了一个训练集没有的类别或者反过来。解决在编码前把两个数据集纵向合并做编码或者用reindex对齐列。# 做法用训练集列名对齐测试集缺失列补 0 X_test X_test.reindex(columnsX_train.columns, fill_value0)5.2 预测出负数房价现象线性回归的预测结果里出现负的房价看起来匪夷所思。原因某个特征的系数为负且量级过大或者目标变量不服从正态分布导致模型在极端值处外推。解决对目标变量做 log 变换训练时预测log(price)预测后expm1还原。# 训练 y_train_log np.log1p(y_train) # 预测后还原 y_pred np.expm1(model.predict(X_test))5.3 RMSE 高得离谱但 MAE 正常现象RMSE 是 MAE 的五六倍两个指标严重不匹配。原因存在少数预测误差极大的样本——RMSE 对误差取平方几个大误差样本会把分数拉爆。解决检查残差最大的 10 个样本看是不是离群点造成的如果这些样本本身价格极低或极高考虑单独处理或在评估时同时报告两个指标别只看 RMSE。5.4 随机森林训练慢到怀疑人生现象几千行数据n_estimators500跑了好几分钟没结束。原因默认参数下每棵树都长到最深加上n_jobs-1没开并行或者数据里有高基数分类特征没有做编码导致分裂计算量暴涨。解决先设n_jobs-1再把max_depth限制在 15 以内n_estimators从 100 起步逐步往上试。5.5 网格搜索跑了一晚上还没出结果现象GridSearchCV的参数网格看起来不大但训练时间完全不可控。原因参数组合数 × 交叉验证折数 实际训练次数每个参数多加一个候选值总次数翻倍甚至翻几倍。解决先用较粗的网格跑一轮确定最优区间再在区间附近做细搜或者直接用RandomizedSearchCV替代先设n_iter50探路。6. 预测结果的解释从 RMSE 分数到“这个模型到底值不值得信”模型训练完分数也好看了但离“真正能用”还差一步解释预测结果。这一步往往被当作可有可无的收尾但我认为它才是区分“跑通流程”和“真正理解项目”的分水岭。对猜错的特例做误差分析是最有价值的一步。把测试集里预测误差最大的 10 条样本列出来对比真实值和预测值再回看原始数据里这些样本的特征。这个动作做几次之后你会对模型的边界有直观认识什么样的房子容易预测准什么样的房子模型完全找不到规律。误差最大的样本通常是豪宅或特殊户型因为训练集里这类样本太少模型没见过足够多的模式这就是数据稀缺带来的预测天花板不是调参能解决的。SHAP 值分析是把模型从“黑匣子”变成“可解释”的常用工具。它是博弈论里的 Shapley 值在机器学习上的应用告诉你每个特征对单条预测的贡献是正是负、幅度多大。比如 SHAP 值显示某套房子的总质量把预测价格抬高了 3 万美元但地理位置把它拉低了 5 万美元这样你就能回答“模型为什么给这房子估这个价”这个问题。import shap explainer shap.Explainer(xgb, X_train) shap_values explainer(X_test) # 看单条样本的解释 shap.plots.waterfall(shap_values[0])SHAP 的 waterfall 图能直接显示这条预测从基准值开始每个特征如何一步步把价格推到最终值。这在向非技术人员解释模型时特别有用也是我在实际项目中面对业务方时最常用的工具。单看 RMSE 分数业务方没有直观概念但看到“这个特征让预测价格涨了 2 万那个特征让它降了 3 万”理解成本会低很多。用 SHAP 还有一个额外收益它可以被用来排查数据问题比如某个特征的 SHAP 分布异常集中在某几个固定值上可能说明这个特征在编码时出了问题。最后提一个我个人的习惯模型的意义不在于分数多好看而在于你对“什么时候它可以信、什么时候它一定会错”心里有底。我现在拿到一份新的回归数据集从数据体检到基线模型到调参大概跑一个完整的流程流程和本文几乎一致。踩过最多的坑不是模型选错、参数没调好而是数据没清理干净就急着开跑。先把数据搞清楚模型选什么都有地基数据一塌糊涂再强的模型也只会给你一个看起来很合理的错误答案。希望这篇笔记对你有所帮助祝你在房价预测这个项目上能一次跑通顺带把回归这一类问题的通用套路都吃透。本文还有配套的精品资源点击获取
返回列表