尧图精选

Python机器学习房价预测实战:从数据清洗到模型部署全流程

🕒 发布时间:2026/10/1 5:42:50 📁 来源:尧图网络
简介这是一份面向计算机相关专业学生的Python机器学习实战资料以房价预测为完整案例适用于课程设计、期末大作业与实战练习。项目曾获导师认可评分达98分可作为高分项目参考。资源包共26个文件约1.28MB包含15个py脚本、1个ipynb笔记本、2个csv数据集、1个html分析报告及若干jpg可视化图片另有read、md说明与gitignore配置覆盖数据爬取、处理到建模的完整链路。数据源通过爬虫从链家和安居客获取爬虫代码位于spiders文件夹仅供学习用途。读者可据此掌握数据清洗、特征工程、模型选择、交叉验证与超参数调整等流程并借助Scikit-learn、Pandas、NumPy与Matplotlib完成回归分析与可视化同时学习准确率、均方误差等评估指标的计算与解读。目前已有103人学习适合希望系统走通机器学习项目全流程的初学者与进阶者。1. 房价预测项目为什么成了机器学习入门的分水岭很多人学 Python 机器学习卡在“看完教程还是不会做项目”这一步。房价预测恰好是那道分水岭它比鸢尾花分类复杂又比图像检测轻量数据能自己造、模型能自己调、结果能自己解释。波士顿房价预测是经典入口但原始数据集有伦理争议现在更推荐用 California Housing 或自己爬取的二手房数据。这个项目的核心不是“预测房价”四个字而是让你走完一条完整链路数据获取、清洗、特征工程、模型选型、调参、评估、部署。适合刚学完 Python 基础语法、装好 sklearn、想拿一个能写进简历的机器学习项目练手的人。下面按我实际带新人的顺序把这条链路拆开讲。2. 数据获取与清洗从 CSV 到能喂给模型的矩阵2.1 房价预测数据集怎么选、怎么读常见做法是先用 sklearn 自带的 California Housing 数据集跑通流程再换成真实场景的 CSV。California Housing 只有 20640 条样本、8 个特征加载一行代码适合验证代码逻辑。真实项目里你拿到的往往是 Excel 或数据库导出的 CSV列名混乱、缺失值散落、类别字段没编码。我一般先用 pandas 做一次“体检”import pandas as pd import numpy as np from sklearn.datasets import fetch_california_housing # 方式一加载内置数据集快速验证流程 housing fetch_california_housing() df pd.DataFrame(housing.data, columnshousing.feature_names) df[MedHouseVal] housing.target # 目标列房价中位数 # 方式二读取自己的 CSV真实项目常用 # df pd.read_csv(house_prices.csv, encodingutf-8) print(df.shape) print(df.isnull().sum()) # 每列缺失值数量 print(df.describe().T) # 数值列分布重点看 min/max/mean print(df.dtypes) # 字段类型object 列需要编码这段代码做了三件事确认数据规模、定位缺失值、观察数值分布。isnull().sum()返回每列缺失数量如果某列缺失超过 30%直接考虑丢弃低于 5% 可以填充。describe()里如果发现某列 max 远大于 75% 分位数说明有极端值后面要做截断或对数变换。dtypes里 object 类型的列就是需要编码的类别特征比如“区域”“房型”。参数上注意encoding参数中文 CSV 常用gbk或utf-8-sig读进来乱码就先试这两个。如果数据量超过内存用chunksize分块读但房价预测一般几万到几十万行直接读没问题。2.2 缺失值、异常值和类别编码的处理顺序处理顺序错了后面全白做。我的习惯是先删无用列再处理缺失值再处理异常值最后编码。删列看两点缺失率超过 30%或者该列与目标相关性极低用df.corr()看。缺失值填充数值列用中位数类别列用众数别用均值——房价分布通常右偏均值会被高价房拉偏。# 1. 删除缺失率过高的列 threshold 0.3 df df.drop(columnsdf.columns[df.isnull().mean() threshold]) # 2. 数值列中位数填充类别列众数填充 num_cols df.select_dtypes(include[np.number]).columns cat_cols df.select_dtypes(include[object]).columns for col in num_cols: df[col] df[col].fillna(df[col].median()) for col in cat_cols: df[col] df[col].fillna(df[col].mode()[0]) # 3. 异常值处理用 IQR 截断避免直接删样本 for col in num_cols: if col MedHouseVal: continue Q1 df[col].quantile(0.25) Q3 df[col].quantile(0.75) IQR Q3 - Q1 lower Q1 - 1.5 * IQR upper Q3 1.5 * IQR df[col] df[col].clip(lower, upper) # 4. 类别编码低基数用 one-hot高基数用目标编码或频率编码 df pd.get_dummies(df, columnscat_cols, drop_firstTrue)IQR 截断比直接删除温和保留样本量的同时压制极端值。clip把超出上下界的值拉到边界不会产生 NaN。get_dummies的drop_firstTrue避免虚拟变量陷阱线性模型必须加树模型可加可不加。如果类别列基数很高比如小区名有几百个one-hot 会炸维度改用频率编码把类别替换成出现次数。注意所有清洗步骤都要在训练集上算参数中位数、IQR 边界再应用到测试集。如果全量数据一起算测试集信息泄露评估结果虚高。3. 特征工程与模型选型把原始列变成有预测力的信号3.1 三个必做的特征构造与缩放原始特征直接喂模型也能跑但特征工程决定上限。房价预测里最有效的构造是“房间数/家庭人数”“卧室数/房间数”“经纬度到市中心距离”。California Housing 有AveRooms、AveBedrms、Population、AveOccup直接算比值# 构造比值特征 df[rooms_per_household] df[AveRooms] / df[AveOccup] df[bedrooms_per_room] df[AveBedrms] / df[AveRooms] df[population_per_household] df[Population] / df[AveOccup] # 对数变换右偏特征取 log1p压缩大值 df[MedInc_log] np.log1p(df[MedInc]) # 特征缩放线性模型必须做树模型不需要 from sklearn.preprocessing import StandardScaler scaler StandardScaler() scale_cols [MedInc, HouseAge, AveRooms, AveOccup] df[scale_cols] scaler.fit_transform(df[scale_cols])rooms_per_household比单独的AveRooms更能反映居住密度bedrooms_per_room高说明小户型多。log1p处理收入这类长尾分布log1p(x) log(1x)避免 x0 时报错。StandardScaler 把均值变 0、方差变 1线性回归和 SVM 对尺度敏感不缩放会导致系数不可比、收敛慢。树模型随机森林、XGBoost对尺度不敏感但缩放也不会有坏处。参数上StandardScaler的fit只能在训练集调用transform分别作用于训练集和测试集。如果用了Pipeline这一步会自动处理后面会讲。3.2 线性回归、随机森林、XGBoost 怎么选选型看数据量和特征类型。数据量小于 1 万、特征线性关系明显先跑线性回归当基线。数据量几万、有非线性交互随机森林稳。追求最高精度且愿意调参上 XGBoost 或 LightGBM。我一般三个都跑用交叉验证比 RMSE。from sklearn.model_selection import train_test_split, cross_val_score from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor from sklearn.metrics import mean_squared_error import numpy as np X df.drop(columns[MedHouseVal]) y df[MedHouseVal] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) models { LinearRegression: LinearRegression(), RandomForest: RandomForestRegressor(n_estimators100, random_state42), XGBoost: XGBRegressor(n_estimators100, learning_rate0.1, random_state42) } for name, model in models.items(): scores cross_val_score(model, X_train, y_train, cv5, scoringneg_mean_squared_error) rmse np.sqrt(-scores.mean()) print(f{name} CV RMSE: {rmse:.4f})cross_val_score的scoringneg_mean_squared_error返回负 MSE取负再开方得到 RMSE。cv5是 5 折交叉验证比单次划分稳定。随机森林的n_estimators100是树的数量太少欠拟合太多训练慢100 到 300 之间通常够用。XGBoost 的learning_rate0.1控制每棵树贡献调小到 0.05 需要增加n_estimators补偿。如果线性回归 RMSE 比随机森林高很多说明特征间有非线性关系优先树模型。如果随机森林和 XGBoost 差距在 5% 以内选随机森林因为调参少、不容易过拟合。提示random_state固定后结果可复现调参时不要改它否则每次比较基准不一致。4. 调参与评估别让 RMSE 骗了你4.1 随机森林和 XGBoost 的关键参数怎么调调参不是网格越密越好。随机森林重点调n_estimators、max_depth、min_samples_split。XGBoost 重点调n_estimators、learning_rate、max_depth、subsample。我一般用RandomizedSearchCV随机搜 30 到 50 组比网格搜快效果差不了多少。from sklearn.model_selection import RandomizedSearchCV param_dist { n_estimators: [100, 200, 300, 500], max_depth: [None, 10, 20, 30], min_samples_split: [2, 5, 10], min_samples_leaf: [1, 2, 4] } rf RandomForestRegressor(random_state42) search RandomizedSearchCV(rf, param_dist, n_iter30, cv5, scoringneg_mean_squared_error, random_state42, n_jobs-1) search.fit(X_train, y_train) print(search.best_params_) print(np.sqrt(-search.best_score_))n_iter30是随机采样 30 组参数组合n_jobs-1用满 CPU 核。max_depthNone表示树完全生长数据量大时容易过拟合可以限制到 20 左右。min_samples_split是节点分裂最小样本数调大能防过拟合。min_samples_leaf是叶子节点最小样本数同样防过拟合。XGBoost 调参类似但注意learning_rate和n_estimators要联动学习率减半树数量翻倍。subsample0.8表示每棵树用 80% 样本训练增加随机性防过拟合。colsample_bytree0.8类似每棵树用 80% 特征。4.2 用 RMSE、MAE、R² 三个指标交叉验证只看 RMSE 会漏掉很多东西。RMSE 对大误差敏感MAE 更稳健R² 看解释方差比例。三个一起看才能判断模型是整体偏差还是个别样本预测离谱。from sklearn.metrics import mean_absolute_error, r2_score best_model search.best_estimator_ y_pred best_model.predict(X_test) rmse np.sqrt(mean_squared_error(y_test, y_pred)) mae mean_absolute_error(y_test, y_pred) r2 r2_score(y_test, y_pred) print(fRMSE: {rmse:.4f}) print(fMAE: {mae:.4f}) print(fR2: {r2:.4f}) # 看残差分布如果残差有规律说明模型漏了重要特征 residuals y_test - y_pred print(pd.Series(residuals).describe())RMSE 和 MAE 差距大说明有极端误差样本回去检查异常值处理。R² 低于 0.6模型解释力不够加特征或换模型。残差均值接近 0 且标准差稳定说明模型没系统性偏差。如果残差和某个特征相关比如残差随收入增大而增大说明该特征和目标是非线性关系需要做分箱或多项式变换。注意测试集只能用一次。调参用交叉验证在训练集上做最终评估才用测试集。反复用测试集调参测试集就变成了训练集评估结果不可信。5. 避坑与排查房价预测项目里最容易翻车的五件事5.1 数据泄露为什么你的 R² 高得离谱现象交叉验证 R² 0.95测试集 R² 0.6。原因在划分训练测试集之前做了全局标准化或填充测试集统计量泄露到训练过程。解决所有 fit 操作只在训练集做用 Pipeline 封装。from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler pipe Pipeline([ (imputer, SimpleImputer(strategymedian)), (scaler, StandardScaler()), (model, RandomForestRegressor(random_state42)) ]) # 这样 cross_val_score 内部会自动在每折训练集上 fit5.2 目标泄露特征里混进了答案现象某个特征重要性异常高去掉后模型崩了。原因特征里包含了目标信息比如“成交价”误入特征列或者用未来数据预测过去。解决检查列名确认每个特征在预测时点可获取。房价预测里“挂牌价”不能作为特征预测“成交价”。5.3 类别编码后维度爆炸现象one-hot 后特征从 10 列变成 5000 列训练极慢。原因高基数类别列直接 one-hot。解决改用频率编码或目标编码或者把稀有类别合并成“其他”。# 频率编码把类别替换成出现次数 freq df[neighborhood].value_counts() / len(df) df[neighborhood_freq] df[neighborhood].map(freq) df df.drop(columns[neighborhood])5.4 随机森林 n_jobs 设了 -1 反而更慢现象n_jobs-1训练时间比单核还长。原因数据量小或树数量少时多进程通信开销大于计算收益。解决数据量小于 1 万行时用n_jobs1大于 10 万行再用-1。5.5 用 accuracy 评估回归模型现象模型准确率 0.0 或报错。原因回归问题用了分类指标。解决回归用 RMSE、MAE、R²分类才用 accuracy、precision、recall。sklearn 的cross_val_score默认 scoring 是 accuracy回归必须显式指定scoringneg_mean_squared_error。6. 把模型跑成可复现的脚本从 notebook 到命令行notebook 适合探索但项目要交付得写成脚本。我习惯把流程拆成data_loader.py、feature_engineer.py、train.py、predict.py四个文件用argparse传参数用joblib保存模型。这样换数据集只改配置不用动代码。# train.py import argparse import joblib import pandas as pd from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error import numpy as np def main(data_path, model_path): df pd.read_csv(data_path) X df.drop(columns[MedHouseVal]) y df[MedHouseVal] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42) pipe Pipeline([ (model, RandomForestRegressor( n_estimators300, max_depth20, random_state42, n_jobs-1)) ]) pipe.fit(X_train, y_train) y_pred pipe.predict(X_test) rmse np.sqrt(mean_squared_error(y_test, y_pred)) print(fTest RMSE: {rmse:.4f}) joblib.dump(pipe, model_path) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--data, defaulthousing.csv) parser.add_argument(--model, defaultrf_model.pkl) args parser.parse_args() main(args.data, args.model)joblib.dump保存整个 Pipeline包括预处理和模型预测时直接joblib.load调用predict不会出现预处理不一致的问题。argparse让脚本能配不同数据路径和模型输出路径方便做实验对比。验证方法跑两次train.pyRMSE 完全一致说明随机种子固定生效。换n_estimators500再跑RMSE 应该略有下降或持平如果反而上升说明过拟合回去调max_depth。我自己的习惯是每次实验改一个参数记录 RMSE 和耗时攒够 20 组再画趋势图。别一次改三个参数否则不知道哪个起了作用。这个项目跑通后换成真实二手房数据把特征工程做细RMSE 能压到可用的范围。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →