☰
机器学习房价预测项目实战:从数据清洗到模型调优全流程
2026/9/26 18:38:58 网站建设 项目流程

简介:面向机器学习初学者与高校毕业设计、期末大作业场景的实战项目,聚焦北京新房与二手房房价预测。项目覆盖数据采集(链家、安居客爬虫)、房价分析与机器学习建模全流程,代码含详细注释,下载后简单配置即可运行,便于快速理解与二次开发。资源压缩包共26个文件,主要以15个Python脚本、2个CSV数据集、1个Jupyter Notebook及HTML分析报告构成,另附说明文档与可视化图片,整体大小仅1.29MB,轻量完整。目前已有161人学习使用;项目为作者获评98分的个人高分作业,适合用作毕业设计、课程设计或期末大作业的高分参考。通过本项目可获得从爬虫抓取、数据清洗、特征处理到模型评估的完整实践链路,搭配可视化结果与README文档,能帮助读者快速上手机器学习房价预测任务,提升项目完成度与答辩说服力。

1. 机器学习房价预测:这个题目为什么适合当“人工智能大作业”的练手项目

作为人工智能大作业,房价预测几乎是每个练手的人绕不开的题目。它不需要 GPU,不需要复杂的网络结构,核心是用机器学习方法从表格数据里拟合出房屋价格与几十个特征之间的映射关系。无论是波士顿房价数据还是二手房的挂牌记录,回归任务的完整路径都是同一套:拿数据、做清洗、构造特征、训练模型、评估解释。标题里附带的数据集、源码和文档资料,正好对应大作业评分的三个关注点:数据完整、代码可运行、报告规范。适合从听课过渡到独立做完整项目的本科生,也适合想用真实案例证明动手能力的求职者。保守估计,一台普通笔记本一周能独立做完;如果只想把源码跑通、改改参数,两三天也能交付。下面按数据准备、特征工程、模型调参、文档整理和避坑五条线拆开讲,每一步都给出能直接改用的代码。

2. 数据准备:从拿到数据集到跑起来的第一步,解析字段与隐藏陷阱

房价预测项目里最容易被低估的就是数据准备环节。很多人拿到一个 CSV 就直接扔给 RandomForest,结果分数忽高忽低,还说不清原因。实际上,房价数据集的特点决定了你必须先做一次彻底的数据体检:字段类型是否统一、缺失值分布是否随机、价格标签是否长尾、时间顺序是否影响样本划分。

以经典的波士顿房价数据为例,它包含犯罪率、平均房间数、一氧化氮浓度、房龄等 13 个特征,每个特征都有明确的物理含义和取值范围。这种数据已经高度结构化,适合快速验证机器学习算法的基本流程。但如果做二手房房价预测,公开数据集往往来自房产交易平台的挂售记录,字段会包括户型、面积、朝向、楼层、总价、单价、建筑年份、所在商圈。这类数据没有清洗好,字符串和数值混在一起,直接跑模型一定会翻车。所以第一步不是建模,而是把表格结构弄清楚。

2.1 数据导入与字段解读:这张表里有哪些列会影响价格

我一般会先写一个极短的脚本把数据读进来,打印 shape、dtypes 和 describe,先不看具体业务含义,只看统计口径。

import pandas as pd import numpy as np df = pd.read_csv('house_data.csv', encoding='utf-8') print(df.shape) # 样本数和列数,确认数据有没有被截断 print(df.dtypes) # 检查数值列、字符串列、时间列的分布 print(df.describe(percentiles=[.25, .5, .75]))

这段代码里,shape能帮你发现是不是读错了文件;dtypes决定后面哪些列需要做类型转换;describe则暴露出取值范围异常的特征,比如面积出现 3000 平方米或单价出现负数。percentiles 参数比默认的四分位更细,能看出价格分布是否集中在某个区间。

看字段时不要只盯着数值特征。二手房数据里的“户型”“朝向”“装修情况”都是字符串,但它们对价格影响很大。比如同样面积,三室一厅通常比两室一厅贵;南北通透比朝北的房源单价高。这些字段在后续特征工程里会变成编码特征,所以现在就要记录下来,哪些是主键、哪些是业务标签、哪些是噪音列。我的习惯是先给每一列写一句业务含义,再标注数据处理方式,相当于给自己留一份字段说明。这一步看似多余,却能在后期写文档资料时直接复用。

2.2 数据清洗与划分:缺失值、异常值和时间顺序的三个处理顺序

数据清洗最忌讳“拿到就填,填完就跑”。我总结了一套固定顺序:先看缺失值,再处理异常值,最后才是划分训练集和验证集。

# 1. 统计缺失率,低于5%的列直接删除行,高于30%的列考虑删列 missing_rate = df.isnull().mean() print(missing_rate[missing_rate > 0]) # 2. 异常值裁剪:面积超过三个标准差以上的记录可能是录入错误 area_mean = df['area'].mean() area_std = df['area'].std() df = df[(df['area'] > area_mean - 3 * area_std) & (df['area'] < area_mean + 3 * area_std)] # 3. 按时间排序后再切分,防止未来信息泄露 df = df.sort_values('listing_date').reset_index(drop=True) split_idx = int(len(df) * 0.8) train_df = df.iloc[:split_idx] test_df = df.iloc[split_idx:]

缺失率的统计是第一步,因为不同列的缺失机制完全不同。价格、面积这类关键字段缺失,直接删行最安全;朝向、装修这类分类字段缺失,可以单独建一个“未知”类别,不要用均值去填充。异常值裁剪的 3 倍标准差是一个经验值,具体项目里建议先用df['area'].hist()看一眼分布,如果明显是长尾,改用百分位数裁剪更稳妥,比如只保留 1% 到 99% 分位内的样本。最后的时间排序非常关键,二手房价格受市场波动影响,同一个小区 2023 年的成交价和 2020 年差异很大,随机切分会让模型偷偷用未来的数据“复习”过去的价格,造成验证集分数虚高。

3. 特征工程与基线模型:先把原始表格变成机器学习能吃的格式

数据清洗完成后,进入整个项目里最花时间的特征工程阶段。机器学习算法本质上只能处理数值,所以要把“户型”“朝向”“所在商圈”这类信息转成数值,同时把面积、房龄、单价这些原始数值变成更有表达力的特征。这里的原则是:先做单位统一,再做变换,最后考虑组合特征。

很多初学者跳过了特征工程,直接拿原始列去训练,模型也能跑通,但 R2 往往停留在 0.5 到 0.6 之间。原因很简单:房价和面积之间通常不是线性增长关系,面积从 60 平涨到 90 平的边际价格变化,和从 150 平涨到 180 平完全不同。应对方式是对面积和总价做对数化或平方变换,把这种非线性关系“拉直”。

3.1 数值特征变换与组合:从平方项到面积单价

数值特征变换最常用的方法有三个:取对数、构造平方项、计算派生比例。取对数适合价格、面积这类右偏分布;平方项适合捕捉“面积并非越大越好”的倒 U 型关系;比例特征则能体现房源的综合性价比。

import numpy as np # 价格标签存在明显右偏,做对数变换让分布更接近正态 df['price_log'] = np.log1p(df['price']) # 面积平方项:捕捉大面积房源的边际递减效应 df['area_squared'] = df['area'] ** 2 # 单位面积价格,常被当作房价预测的强特征 df['unit_price'] = df['price'] / df['area'] # 房龄=当前年份-建筑年份,注意老破小的特殊处理 df['house_age'] = 2025 - df['build_year'] df.loc[df['house_age'] < 0, 'house_age'] = 0

log1p是log(1+x),好处是当 x 为 0 时结果也是 0,不会产生负无穷。价格取对数后,训练目标从“预测原价”变成“预测 log 价”,在评估时要记得做np.expm1还原,否则 RMSE 的数值会非常小,容易让人误以为模型效果极好。area_squared平方项是否有效,可以在做完后面线性回归后看它的系数显著性。unit_price是房价预测项目里一个“性价比很高”的特征,能直接把区域差异带进模型,但它和价格标签高度相关,如果用它去预测总价,要留意是否造成数据泄漏。

3.2 类别特征编码与缺失值处理:让机器学习算法能理解“户型”和“朝向”

数据集里的“户型”“朝向”“所在商圈”都属于类别特征。常见的处理方式是把低频类别归并成“其他”,再做 one-hot 编码或标签编码。LightGBM、XGBoost 这类树模型可以直接处理类别特征,线性回归则必须要 one-hot。对于大作业来说,最稳妥的做法是统一用 one-hot,因为后续换模型不用重新处理数据。

# 把低频类别合并,避免 one-hot 后维度爆炸 for col in ['layout', 'orientation']: value_counts = df[col].value_counts() rare_categories = value_counts[value_counts < 30].index df.loc[df[col].isin(rare_categories), col] = 'other' # 缺失的朝向单独作为一类,不让算法把缺失当成某种固定朝向 df['orientation'] = df['orientation'].fillna('unknown') # 用 pandas 一次性完成 one-hot 编码 df_encoded = pd.get_dummies(df, columns=['layout', 'orientation', 'district'], drop_first=True)

合并低频类别是防止 one-hot 后训练集和验证集出现不一致的关键。假设一个“五室三厅”的户型只在训练集里出现一次,验证集里没有,模型就会把这个维度当成全零,影响不大;但反过来如果是验证集里有、训练集里没有,预测时就会出现维度不匹配的报错。把出现次数少于 30 的类别归入“other”能避免这种问题。fillna('unknown')则是在告诉模型:朝向信息缺失本身就是一个特征,很多老房源不会填写朝向,但这不代表它毫无价值。drop_first=True是为了消除多重共线性,在线性回归里尤其重要,对树模型则没有太大区别。

3.3 训练一个可解释的基线模型:先用线性回归定下限,再用随机森林找非线性

特征工程做完后,我会立刻训练一个简单模型作为基线,而不是一上来就调参。基线的意义不是追求最高分数,而是给你后续所有操作提供一个对照值。

from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score # 这里用随机切分做基线对比,后续做正式实验再换成按时间切分 feature_cols = [c for c in df_encoded.columns if c not in ['price', 'price_log', 'listing_date']] X = df_encoded[feature_cols].values y = df_encoded['price_log'].values X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 线性回归基线 lr = LinearRegression() lr.fit(X_train, y_train) y_pred = lr.predict(X_test) print('Linear R2:', r2_score(y_test, y_pred)) print('Linear RMSE:', np.sqrt(mean_squared_error(y_test, y_pred)))

线性回归在这个任务里表现通常不会太差,因为它能给出一个可解释的基准。如果线性回归的 R2 都无限接近 1,那就要怀疑是不是把价格相关的字段直接放进特征里了;如果 R2 只有 0.3 到 0.4,说明特征与目标之间存在明显的非线性关系,这时可以再补一个随机森林对比。

# 随机森林基线,max_depth 设置为 10,限制单棵树复杂度 rf = RandomForestRegressor(n_estimators=300, max_depth=10, min_samples_leaf=5, random_state=42) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) print('RandomForest R2:', r2_score(y_test, y_pred)) print('RandomForest RMSE:', np.sqrt(mean_squared_error(y_test, y_pred)))

这里的max_depth和min_samples_leaf都是控制过拟合的参数。房价数据集通常只有几千条样本,树太深会记住训练集里每一条记录的噪声。用 300 棵树已经足够稳定,更多树并不会带来显著提升,只会拖慢网格搜索的速度。

4. 模型选择与调参:从线性回归到集成学习,代码、源码与文档怎么配合

当基线建立起来之后,就要开始面对一个问题:该选哪个模型作为最终方案。房价预测任务里,线性回归、决策树、随机森林、XGBoost、LightGBM 都是常用选项,但它们的适用条件和训练速度差异很大。一个大作业如果想要好看的评价指标,通常会落在集成模型上;如果想要好写文档,线性回归和随机森林的组合可以讲得更清楚。

4.1 场景化选模型:什么时候该用线性回归,什么时候换 LightGBM

选模型的依据不是“哪个比较新”,而是特征规模和样本量。我通常按以下方式判断:

模型优势劣势典型使用场景
线性回归可解释性强,训练快无法自动处理非线性特征数量少,且与价格呈线性关系
决策树非线性,可解释易过拟合,单个模型不稳定用于快速分析特征重要性
随机森林抗过拟合,调参空间小预测值不能超出训练集范围样本量适中,特征类别多
LightGBM精度高,支持类别特征调参复杂,容易过拟合样本量大,特征交互关系复杂

波士顿房价这类小数据集用线性回归就够了,但二手房价格受区位、楼层、朝向的交叉影响,线性模型很难学到“同一商圈里高层朝南比低层朝北贵多少”这种规则。随机森林是性价比最高的选择,树模型自动处理特征交互,不需要额外构造交叉项。LightGBM 在几千条样本上也能跑,但需要更仔细地设置学习率和叶节点数,否则测试集分数会明显低于训练集。

import lightgbm as lgb train_data = lgb.Dataset(X_train, label=y_train) valid_data = lgb.Dataset(X_test, label=y_test, reference=train_data) params = { 'objective': 'regression', 'metric': 'rmse', 'learning_rate': 0.05, 'num_leaves': 31, 'min_data_in_leaf': 20, 'feature_fraction': 0.8, 'verbose': -1 } model = lgb.train(params, train_data, num_boost_round=1000, valid_sets=[valid_data], callbacks=[lgb.early_stopping(50)])

代码里num_leaves=31对应 LightGBM 的默认树复杂度,min_data_in_leaf=20是防止某个叶子节点上样本太少导致过拟合。early_stopping(50)表示如果验证集 RMSE 连续 50 轮没有下降就停止训练,这是集成模型里最实用的防过拟合手段。

4.2 参数调优与过拟合监控:先管学习率,再管树复杂度

调参的顺序决定了你是在有效搜索还是纯粹碰运气。我一般先固定学习率,再用网格搜索找树的复杂度,最后回到学习率做一次精调。千万不要一上来就把所有参数丢进 GridSearchCV,组合数太多,跑一晚上也未必收敛。

from sklearn.model_selection import GridSearchCV from lightgbm import LGBMRegressor lgb_model = LGBMRegressor(objective='regression', learning_rate=0.05, random_state=42) param_grid = { 'num_leaves': [15, 31, 63], 'min_data_in_leaf': [10, 20, 50], 'feature_fraction': [0.7, 0.8, 1.0] } grid = GridSearchCV(lgb_model, param_grid, cv=5, scoring='neg_root_mean_squared_error', n_jobs=-1) grid.fit(X_train, y_train) print('best params:', grid.best_params_) print('best cv score:', -grid.best_score_)

这里的neg_root_mean_squared_error是 sklearn 里“负 RMSE”的写法,因为 GridSearchCV 默认要求分数越大越好,所以 RMSE 取相反数。五折交叉验证可以帮助判断参数是不是只在某一个随机划分上表现好。如果最优参数落在了搜索边界上,比如num_leaves=63,说明模型还有提升空间,应该扩大搜索范围;如果落在中间值,说明当前范围合理。

4.3 项目源码结构与文档资料怎么用:一份完整大作业的交付目录

拿到别人整理好的项目源码后,第一件事不是读模型代码,而是看目录结构。一个合格的房价预测大作业源码,应该至少包含data/、src/、notebooks/、docs/这四个目录。data/里是清洗前后的 CSV;src/按功能拆分成数据加载、特征工程、模型训练、评估预测几个模块;notebooks/适合做探索性分析和中间结果展示;docs/放文档资料和答辩 PPT。

如果你的大作业要求提交“数据集+项目源码+文档资料”,我建议把文档写成五个部分:选题背景与问题定义、数据说明与探索性分析、特征工程方法与代码索引、模型对比与参数选择、结论与改进方向。其中“模型对比”是最容易拿分的位置,用表格把三个模型的 R2、RMSE、训练时间列出来,每行配一两句分析,说明为什么随机森林比线性回归好了几个点。答辩时老师最喜欢问的问题通常是“你觉得哪个特征最重要”,所以文档里最好包含一张特征重要性图,这个在第六章会展开讲。

5. 避坑指南:房价预测项目中反复出现的 5 个常见错误与自查方法

这一章单独拎出来,是因为房价预测项目里太多人栽在看起来不起眼的数据处理细节上。每一组错误我都会按“现象、原因、解决”来写。如果你照着做还是分数异常,先看这里。

5.1 把“标签”当普通特征做填充,造成“未来数据”混进训练集

现象:训练集 RMSE 很低,但验证集 RMSE 突然变得非常大,而且特征重要性排名里总价相关字段排在第一位。

原因:数据准备阶段用整张表的均值填充了缺失值,而填充过程发生在 train/test 划分之前。如果price的缺失值是用全量均值填充,相当于把验证集的价格信息泄漏给了训练集。模型在训练时见过“全局平均水平”,到验证集上自然能猜得八九不离十。

解决:先把缺失行拆开,用训练集的统计量填充验证集。正确做法是fill_value = train_df['price'].median(),然后用这个值分别填充训练集和验证集。更稳妥的是对需要填充的列单独建模预测,但大作业场景下没必要。

5.2 不做时间切分,随机打乱数据集导致验证分数虚高

现象:模型在测试集上 R2 达到 0.92,但换到新一批房源数据上预测,误差直接翻倍。

原因:房价数据是典型的时间序列。同一个小区 2024 年的成交价受当年市场供需影响,和 2021 年的价格差可能在 20% 以上。随机切分会让训练集和验证集都包含不同年份的样本,模型学到的是“年份平均值”,而不是真正的价格规律。

解决:按照挂牌时间或成交时间排序,用前面的 80% 做训练,后面的 20% 做验证。如果数据里没有时间字段,至少要把城市、商圈、小区作为分组条件,用GroupShuffleSplit按小区划分,否则同一个小区的房源会同时出现在训练和验证里。

5.3 特征缩放时统一 fit 整个数据集,验证集信息泄漏到训练

现象:线性回归模型的特征系数非常大,预测结果对某一个特征极其敏感。

原因:许多人习惯先对整个特征矩阵做StandardScaler().fit_transform(X),再做训练测试划分。fit_transform会计算全量数据的均值和标准差,验证集的分布信息从源头上混进了训练阶段。这不算最严重的泄漏,但会让模型对验证集有“偏好”。

解决:先划分,再在训练集上scaler.fit(X_train),然后分别transform训练集和测试集。随机森林和 LightGBM 对特征缩放不敏感,但线性回归、岭回归和 SVM 必须按这个顺序处理。

5.4 对长尾价格直接建模,R2 永远上不去

现象:模型对普通房源预测很准,但对高价豪宅的预测严重偏低,整体 R2 只有 0.6 左右。

原因:房价分布是典型的右偏长尾。绝大多数样本集中在 100 万到 300 万之间,少数豪宅在千万以上。模型在 RMSE 的驱动下会把全部注意力放在“主流样本”上,豪宅的偏差对总损失的贡献被平均掉了。

解决:对价格标签做log1p变换,然后在评估指标里同样使用 log 空间的 RMSE,最后在报告里用expm1还原成真实价格再展示。加了 log 变换后,模型对高低价样本的误差权重更均匀,R2 通常能提升 0.05 到 0.1。

5.5 网格搜索不看评估指标,跟着默认 score 走

现象:GridSearchCV 跑完,best_score_很大,但手动计算测试集 R2 时却对不上。

原因:回归模型的默认 score 是 R2,但如果你在构造LGBMRegressor时没指定eval_metric,LightGBM 的默认目标函数可能是 RMSE。网格搜索内部的得分和你自己的评估函数不一致,导致选出来的参数不是针对你真正关心的指标。

解决:在 GridSearchCV 的scoring参数里显式写成neg_root_mean_squared_error或r2,并保证模型自身的metric与之一致。每次跑完网格搜索,不要直接采用best_params_,要用它重新在验证集上跑一遍,记录最终的 R2 和 RMSE。

6. 进阶实战:从能跑通到答辩能讲好,用 SHAP 解释和模型融合拉满完成度

一个房价预测大作业如果只是把模型跑出来,老师大概率只能给一个基础分。真正拉开差距的部分在于:你能不能解释模型为什么这样预测,以及有没有尝试多种方案做融合。这一章我写两个常用技巧,都是在答辩现场能让老师眼睛一亮的东西。

6.1 用 SHAP 把黑匣子打开:哪个特征在推动价格上浮

树模型最被诟病的地方是解释性差,但 SHAP 可以解决这个问题。它会为每一个样本的每一个特征计算一个贡献值,贡献值为正代表把价格往上推,为负代表往下压。

import shap shap_explainer = shap.TreeExplainer(model) shap_values = shap_explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test, feature_names=feature_cols, max_display=15)

summary_plot输出的是每个特征的全局重要性分布。在文档里放这张图,再配一段话:比如“面积和单位价格对预测结果影响最大,且面积较大时边际贡献递减”,这句话直接把特征工程阶段的思考串起来了。答辩时如果老师问“你觉得哪个特征最重要”,不要只回答“面积”,要说“从 SHAP 图来看,面积是主要驱动因素,但它在 120 平之后的边际贡献明显放缓,因此模型并没有简单地按面积线性外推”。

6.2 模型融合与残差图:加权平均不是玄学,是最后一层提分

模型融合是很多大作业里被忽视的部分。简单做法是把随机森林和 LightGBM 的预测结果做加权平均,权重用网格搜索确定。这样做的原理是不同模型的误差分布不同,融合后可以抵消一部分偏差。

from scipy.optimize import minimize def loss_func(weight): return np.mean((weight * pred_rf + (1 - weight) * pred_lgb - y_test) ** 2) result = minimize(loss_func, x0=0.5, bounds=[(0, 1)]) best_w = result.x[0] print('optimal weight for RF:', best_w)

这段代码用 scipy 的minimize找最优权重,目标函数是最小化融合预测的均方误差。注意只把测试集当融合权重的选择依据,不要再继续调模型参数,否则又过拟合了。融合后我会画一张残差图,横轴是真实价格,纵轴是预测值减去真实值。如果残差图呈现明显的扇形,说明高价位样本的方差更大,这本身又是一个可写进文档里的发现。

6.3 把项目改造成“新题目”的三个方向:换城市、换目标、换窗口

如果你的大作业要求不能直接套用现成数据集,我通常建议往三个方向改。第一个方向是换成另一个城市的二手房数据集,特征大体相似但房价分布不同,模型和文档框架可以直接迁移。第二个方向是把回归任务改成分类任务,比如预测“未来三个月房价是涨是跌”,这样模型从回归器换成分类器,文档内容也会焕然一新。第三个方向是改变预测窗口,比如不做点预测而做区间预测,用分位数回归输出价格的 90% 置信区间。这三个方向都在原项目的骨架里,改动不大,但能明显增加项目的原创性。

做完这六章的内容,这个项目就不再是“跑通别人的代码”,而是一套能讲清楚前因后果的完整作品。我自己的习惯是每个大作业都先花一天时间把数据里的字段含义写进文档,再开始建模;最后的 SHAP 图一定截图保存,因为答辩 PPT 里它是比所有理论文字都更有说服力的证据。希望这些经验能帮你少踩几个坑,把这份房价预测大作业做成你简历上拿得出手的东西,也希望每个正在赶作业的人都能有惊无险地顺利提交。

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

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

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

立即咨询