☰
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, columns=housing.feature_names) df['MedHouseVal'] = housing.target # 目标列:房价中位数 # 方式二:读取自己的 CSV(真实项目常用) # df = pd.read_csv('house_prices.csv', encoding='utf-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(columns=df.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, columns=cat_cols, drop_first=True)

IQR 截断比直接删除温和,保留样本量的同时压制极端值。clip把超出上下界的值拉到边界,不会产生 NaN。get_dummies的drop_first=True避免虚拟变量陷阱,线性模型必须加,树模型可加可不加。如果类别列基数很高(比如小区名有几百个),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(1+x)避免 x=0 时报错。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_size=0.2, random_state=42) models = { 'LinearRegression': LinearRegression(), 'RandomForest': RandomForestRegressor(n_estimators=100, random_state=42), 'XGBoost': XGBRegressor(n_estimators=100, learning_rate=0.1, random_state=42) } for name, model in models.items(): scores = cross_val_score(model, X_train, y_train, cv=5, scoring='neg_mean_squared_error') rmse = np.sqrt(-scores.mean()) print(f'{name} CV RMSE: {rmse:.4f}')

cross_val_score的scoring='neg_mean_squared_error'返回负 MSE,取负再开方得到 RMSE。cv=5是 5 折交叉验证,比单次划分稳定。随机森林的n_estimators=100是树的数量,太少欠拟合,太多训练慢,100 到 300 之间通常够用。XGBoost 的learning_rate=0.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_state=42) search = RandomizedSearchCV(rf, param_dist, n_iter=30, cv=5, scoring='neg_mean_squared_error', random_state=42, n_jobs=-1) search.fit(X_train, y_train) print(search.best_params_) print(np.sqrt(-search.best_score_))

n_iter=30是随机采样 30 组参数组合,n_jobs=-1用满 CPU 核。max_depth=None表示树完全生长,数据量大时容易过拟合,可以限制到 20 左右。min_samples_split是节点分裂最小样本数,调大能防过拟合。min_samples_leaf是叶子节点最小样本数,同样防过拟合。

XGBoost 调参类似,但注意learning_rate和n_estimators要联动:学习率减半,树数量翻倍。subsample=0.8表示每棵树用 80% 样本训练,增加随机性防过拟合。colsample_bytree=0.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(f'RMSE: {rmse:.4f}') print(f'MAE: {mae:.4f}') print(f'R2: {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(strategy='median')), ('scaler', StandardScaler()), ('model', RandomForestRegressor(random_state=42)) ]) # 这样 cross_val_score 内部会自动在每折训练集上 fit

5.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_jobs=1,大于 10 万行再用-1。

5.5 用 accuracy 评估回归模型

现象:模型准确率 0.0 或报错。原因:回归问题用了分类指标。解决:回归用 RMSE、MAE、R²,分类才用 accuracy、precision、recall。sklearn 的cross_val_score默认 scoring 是 accuracy,回归必须显式指定scoring='neg_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_size=0.2, random_state=42) pipe = Pipeline([ ('model', RandomForestRegressor( n_estimators=300, max_depth=20, random_state=42, 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(f'Test RMSE: {rmse:.4f}') joblib.dump(pipe, model_path) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--data', default='housing.csv') parser.add_argument('--model', default='rf_model.pkl') args = parser.parse_args() main(args.data, args.model)

joblib.dump保存整个 Pipeline,包括预处理和模型,预测时直接joblib.load调用predict,不会出现预处理不一致的问题。argparse让脚本能配不同数据路径和模型输出路径,方便做实验对比。

验证方法:跑两次train.py,RMSE 完全一致,说明随机种子固定生效。换n_estimators=500再跑,RMSE 应该略有下降或持平,如果反而上升,说明过拟合,回去调max_depth。

我自己的习惯是每次实验改一个参数,记录 RMSE 和耗时,攒够 20 组再画趋势图。别一次改三个参数,否则不知道哪个起了作用。这个项目跑通后,换成真实二手房数据,把特征工程做细,RMSE 能压到可用的范围。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询