☰
房价预测AI大作业全攻略:从数据处理到模型调优
2026/9/26 17:16:22 网站建设 项目流程

简介:一套面向高校人工智能课程设计与毕业设计的机器学习房价预测项目,以二手房和新房房价为分析对象,覆盖数据采集、清洗、特征工程、建模评估与可视化全流程。项目内含链家、安居客平台爬虫采集脚本与清洗后的CSV房价数据集,附有带详细注释的Python源码、Notebook分析文档及说明材料,新手可对照注释逐步理解并快速部署复现。资源压缩包共26个文件,以脚本、分析笔记、数据表格和可视化图片等格式为主,配合说明文档,整体仅1.29MB,轻量且结构清晰。目前已有161人学习下载,特别适合用于期末大作业、课程设计或毕业设计;可从数据预处理、模型比较和可视化展示中直接借鉴,并按需修改扩展为独立项目,是易上手、可落地的高分参考方案,既能满足技能训练,也便于答辩演示。

1. 人工智能大作业选房价预测:一个回归项目背后的五个得分点

每年到了人工智能大作业季,总有学生抱着“房价预测数据集”来找我,说课上讲了波士顿房价,但自己拿到的是一堆二手房源的 CSV,总价、单价、朝向、楼层、装修、房龄什么都有,不知道从哪里下手。其实房价预测是机器学习里最标准的回归问题,特征是表格数据、目标是连续数值、评价指标也固定,很适合用来检验特征工程和模型调参的基本功。这个方向能解决的不只是“交一份大作业”,而是把数据清洗、特征构造、模型对比、过拟合诊断这一整条流水线跑通。适合正在学机器学习入门、想用项目源码做完整实践、又不想在选题上浪费时间的同学。下面按我平时给项目做技术评审的顺序,把这个方案的每一步拆开讲。

2. 房价预测数据集怎么用:先看数据字典,再定项目结构

拿到任何一个房价预测数据集,第一件事不是训练模型,而是把字段含义弄清楚。很多大作业翻车,都是栽在数据字典没看明白,把不该当特征的列放进去了。

2.1 先从数据字典下手:区分连续特征、离散特征和目标列

常见房价数据集里,字段一般分三类。第一类是连续数值型,比如面积、房龄、总价、单价;第二类是离散类别型,比如朝向、楼层、装修程度、所在区域;第三类是标识型,比如房源编号、小区名称。标识型字段不能进模型,小区名称如果类别数太多,直接做编码会把训练集撑爆,而且容易过拟合。

我一般会先写一段最基础的数据读取代码,把每一列的类型和缺失情况摸清楚:

import pandas as pd df = pd.read_csv("house_data.csv", encoding="utf-8") print(df.shape) print(df.info()) print(df.describe().T)

这段代码的核心作用有三个。df.shape看样本量和特征数,确认数据规模是否支持后面的交叉验证;df.info()列出每列的非空数量和数据类型,缺失值在哪、哪些列被读成了 object,一眼就能看到;df.describe().T对数值列做统计摘要,如果某一列的 min 和 max 差了几个数量级,说明有异常值或者单位不统一。

这里要注意两个点。第一,目标列一定是连续数值,比如总价或单价,不能是分类。第二,如果原始数据里同时有总价和单价,你只能选一个做目标,另一个要么丢弃,要么小心处理,否则就是数据泄漏。后面避坑章节会专门说这个问题。

2.2 二手房价数据的特殊字段:朝向、楼层、装修、学区怎么编码

与波士顿房价这类经典数据集不同,二手房数据集里会出现大量中文类别字段。朝向有南、北、东、西、东南、西南、东西等;楼层有低楼层、中楼层、高楼层、顶层;装修有精装、简装、毛坯。这些字段不能直接丢给模型,因为在 sklearn 里,字符串是不能参与距离计算的。

处理顺序有讲究。先判断这个类别是有序还是无序。装修程度天然有顺序:毛坯最差、简装中等、精装最好,这种可以用数值映射。朝向和区域没有明确顺序,更适合用独热编码,或者按领域经验做手动分组。我一般这样处理:

df["装修"] = df["装修"].map({"毛坯": 0, "简装": 1, "精装": 2}) df["楼层"] = df["楼层"].map({"低": 0, "中": 1, "高": 2, "顶层": 2}) df = pd.get_dummies(df, columns=["区域", "朝向"], drop_first=True)

map操作是把离散文本变成带大小关系的数值,模型能直接理解;get_dummies生成多列 0/1 标志,避免模型误判类别之间有远近关系。drop_first=True会丢掉每个类别第一列,作用是防止多重共线性,对线性回归尤其重要,因为如果不丢,区域有几个类别就会生成几列,总和恒为 1,X 矩阵变成奇异矩阵。

很多同学在这个环节会纠结“朝向的分数怎么定”。说实话,我们自己是拍不出合理权重的,与其硬编码一个“南向=1, 北向=0”的规则,不如把它当成无序类别做独热编码,让模型自己去学。大作业文档里写清楚这个取舍,反而能拿到特征工程部分的分数。

2.3 最小可运行的项目结构:源码、数据和文档怎么组织

我见过太多大作业是一个 main.py 文件从头写到尾,几千行代码堆在一起,老师打开都头疼。房价预测项目虽然不是工程级别,但至少要有清晰的分层。

常见的做法是一个目录下放三个子目录:data/放原始数据集和清洗后的数据,src/放数据处理、特征工程、模型训练、评估四个脚本,docs/放大作业文档和实验结果截图。训练脚本里用一行主函数把流程串起来,其他功能拆成函数。

需要注意,数据集如果是别人提供的,保留原始文件不要修改,清洗后的版本单独存一份。文档里写清楚“原始数据有 8686 条,清洗后剩 8214 条,删除了哪些异常值”,这就是项目源码之外最有说服力的过程记录。数据集本身如果包含时间字段,保存前先按时间排序,后面做时间切分才不用返工。

3. 特征工程是分数分水岭:从缺失值到归一化的完整流水线

很多机器学习入门教程喜欢把重点放在模型上,但实际做房价预测,特征工程才是拉开分数的地方。模型选线性回归还是随机森林,最后 RMSE 可能只差几百;但特征处理好坏,可能直接决定结果的量级对不对。

3.1 缺失值处理:均值填充为什么在这里可用

二手房数据集里常见的缺失是户型字段的空值、装修程度为空、楼层信息缺失。处理前先统计缺失比例,如果某一列缺失超过 30%,要慎重考虑这列还能不能用;如果只是零星缺失,则看字段类型。

连续数值型缺失,我一般用中位数填充而不是均值,因为房价和面积这类字段分布是偏态的,少数几套超大平层会把均值拉高,中位数更有代表性。类别型缺失,最安全的是填众数,或者单独加一列“缺失标记”让模型自己学习:

num_cols = df.select_dtypes(include=["float64", "int64"]).columns for col in num_cols: df[col] = df[col].fillna(df[col].median()) cat_cols = df.select_dtypes(include=["object"]).columns for col in cat_cols: df[col] = df[col].fillna(df[col].mode()[0])

这段代码先对数值列按列填充中位数,再对文本列按众数填充。用循环而不是fillna(method="ffill")就是为了避免前向填充把无关行之间建立起伪关联。模式值可能出现多个,所以取mode()[0]。

还有一类缺失要注意:面积为空、总价不为空的情况,面积缺失没法通过其他列推出来,只能删掉整行。房龄缺失如果项目名称存在,可以尝试从项目名称里解析建造年份,这是比较进阶的做法,大作业里时间不够可以不做。

3.2 类别特征编码:Label Encoding 与 One-Hot 的取舍

顺序编码我前面已经演示过了,这里要单独说 Label Encoding 的坑。sklearn.preprocessing.LabelEncoder会把字符串按字母表顺序映射成 0、1、2,如果你的特征有顺序含义,它完全不管;你的特征没有顺序含义,它反而强行制造了顺序。

一个常见的反面案例:把“区域”字段做 Label Encoding 后,结果变成“朝阳区=0, 海淀区=1, 丰台区=2”,线性回归会学出一个“每增加一个区,房价增加多少”的荒谬规律。正确做法是用pd.get_dummies或OneHotEncoder处理区域、板块、朝向这类无序字段。

那么什么时候可以用 Label Encoding?答案是对树模型。随机森林和 XGBoost 对特征的单调变换不敏感,而且它们做的是“按某个阈值分裂”,编号本身不影响分裂结果。如果数据维度太大,比如区域有 30 多个类,独热编码会生成 30 多列,对线性回归没问题,对树模型反而会拖慢训练。这一节的大作业报告里,建议把自己试过的两种编码的结果对比白纸黑字写出来。

3.3 特征组合与归一化:把面积单价、房龄结构做成有效特征

原始数据只给你面积、户型、总价、房龄,但真正有用的信息往往藏在组合里。常见的做法是构造“单价 = 总价 / 面积”这种领域特征,还有“卧室平均面积 = 面积 / 居室数”“楼龄新度 = 1 / (房龄 + 1)”。这个环节最依赖领域经验,也是最容易出彩的地方。

归一化要注意使用场景。线性回归、KNN、SVM 这类基于距离或梯度的模型,必须归一化,否则面积以平方米为单位数值上千,朝向那一列是 0/1,量级相差太大,梯度下降会在小数值维度上来回震荡。树模型不依赖距离,归一化反而没有必要,做了也不会带来提升,因为分裂节点只看阈值比较。

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)

注意fit_transform和transform的搭配,这是考试和实践中都反复出现的考点:归一化参数只能从训练集学,测试集要沿用训练集的均值和标准差,不能自己再算一遍。如果先对整个数据集做归一化再切分,严格来说就已经发生了信息泄漏,交叉验证的结果会偏乐观。我把它也算进了避坑清单,因为它真的能看出一个人有没有真正做过项目。

4. 模型选型与参数调整:线性回归、随机森林与 XGBoost 的对比落地

模型部分是大作业里最好写、也最容易显得空洞的部分。很多人的做法是每个模型跑一遍,挑一个分数最高的写进报告,却说不清为什么这个模型在这个数据集上更好。下面按我的习惯给出一套能自圆其说的思路。

4.1 三个基线模型:为什么先跑线性回归再看树模型

我从不鼓励直接上集成模型。第一个要跑的是线性回归,它的意义不是拿冠军,而是给后面所有复杂模型定一个“及格线”。如果线性回归 R² 有 0.82,随机森林只提升到 0.84,说明特征工程已经做得差不多了,模型复杂度没必要继续加;如果线性回归 R² 只有 0.55,随机森林跑到 0.83,说明数据里有明显的非线性关系,这才是你该花篇幅分析的地方。

三件套代码是标准流程:

from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor from sklearn.metrics import mean_squared_error, r2_score X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) models = { "linear": LinearRegression(), "rf": RandomForestRegressor(n_estimators=300, max_depth=10, min_samples_leaf=3, random_state=42), "xgb": XGBRegressor(n_estimators=300, learning_rate=0.05, max_depth=5, subsample=0.8, random_state=42) } for name, model in models.items(): model.fit(X_train, y_train) y_pred = model.predict(X_test) rmse = mean_squared_error(y_test, y_pred, squared=False) r2 = r2_score(y_test, y_pred) print(f"{name:>6} RMSE={rmse:.2f} R2={r2:.4f}")

random_state=42保证每次运行结果可复现,是交作业的强制要求;squared=False直接拿到 RMSE 而不是 MSE,数值单位与房价一致,报告里好说明。三个模型用同一份切分好的数据,保证可比性。线性回归不用归一化也能跑,但如果前面做了缩放,记得把测试集也同步缩放。

4.2 随机森林的三个必调参数:n_estimators、max_depth、min_samples_leaf

随机森林在大作业数据集上表现一般都不错,原因是对异常值和缺失值包容度好,也不用做太多特征缩放。但默认参数不是为你这个数据集定制的,三个参数必须调。

n_estimators是树的数量。我一般从 100 试到 500,如果 300 以后误差基本不变,就不再往上加。不是树越多越好,训练时间线性增长,收益却收敛,大作业没必要烧机器。max_depth限制树深度,不设的话树可能长到把训练集的每一条都记住,测试集分数就会很难看。min_samples_leaf是叶节点最少样本数,设成 3 或 5 能强拉伸制过拟合。

先固定n_estimators=300,然后对max_depth和min_samples_leaf做网格搜索,是更省时间的顺序。先调树的规模,再调复杂度限制,不要一开始就把两个参数同时扔进 GridSearchCV,维度太多,搜索空间爆炸,而且大作业的计算时间有限。

4.3 XGBoost 在房价数据上的调参顺序:先学率后树深

XGBoost 效果好,但参数更多,也更容易玄学。我的调整顺序是固定的:先定learning_rate,再调max_depth,最后看subsample和colsample_bytree。

learning_rate默认是 0.3,训练速度快但容易震荡;降到 0.05 之后,每一步更小,需要更多树来补偿,但最终精度更高。注意它和n_estimators耦合,学习率减半,最优树数量通常会翻倍,所以两者要一起看。max_depth在房价这种表格数据上,3 到 6 之间就够用了,太深会开始抓噪声。subsample控制每棵树用多少比例的样本,0.8 是个保守的默认值,能增加模型多样性,降低方差。

调参不是越复杂越好。大作业的加分项是画出“不同 max_depth 下训练集和测试集 RMSE 的曲线”,直接证明你理解了欠拟合和过拟合的分界线,比单纯丢出一个最优参数有说服力得多。

4.4 评估指标怎么定:RMSE 和 R² 哪个更能看出模型翻车

回归任务最常用的两个指标各有分工。RMSE 单位与房价一致,比如“RMSE = 12.3 万元”,对不懂机器学习的老师来说也很直观,它就是“平均预测偏差”。R² 是一个相对得分,0.85 意味着模型解释了 85% 的房价方差,但看不出来误差绝对大小,而且当数据本身方差很小时,R² 会显得很高却没有实际意义。

我会两个都打印,但报告里以 RMSE 为主。还要补一个指标:MAE,也就是平均绝对误差。RMSE 对被预测偏远的少数样本放大了惩罚,MAE 能反映常规样本的误差水平。如果 RMSE 很高但 MAE 不高,说明问题集中在少数异常房价上,这时候可以去看看是不是测试集里混进了几套特别贵的豪宅,把它们单独分析,而不是急着改模型。

5. 常见问题与避坑清单:数据泄漏、时间穿越和二手房的坑

这一章的价值在于,下面每一条都是我见过真实翻车的场景。把这些坑提前避掉,等于给你的大作业买了后悔药,不用等跑出离谱结果再回头一行行查。

5.1 数据泄漏:把目标列当特征用,训练分数虚高

现象:训练时 R² 高达 0.98,特征重要性里却出现了一个不合理的字段;一到测试集或答辩现场手动输入几套房源,预测结果完全不对。

原因:原始数据同时存在“总价”和“单价”这两个字段,单价本来就是总价除以面积算出来的,如果目标列是总价,把单价放进特征矩阵,模型等于直接抄答案。这种情况下 R² 再高也是假的。

解决:进入模型的特征必须只保留“预测时能拿到”的信息。总价是目标,单价丢弃;挂牌价如果和成交价高度重合,也要做同样处理。报告里要主动说明这一点,老师一看就知道你懂什么叫数据泄漏。

5.2 时间穿越:用未来数据预测过去

现象:数据集包含近五年的二手房交易记录,直接按行随机切分训练集和测试集,测试集的 RMSE 看起来不错,但模型一上真实场景就失效。

原因:房价和年份强相关,随机切分会让同一年份的数据同时出现在训练集和测试集里,模型其实记住了年份规律。你的评估因此被美化,真实场景是只能用过去预测未来。

解决:严格按时间做切分,比如前四年做训练,最后一年做验证。用train_test_split之前,先按时间排序,再用iloc直接按位置切分。大作业报告里画出训练年份和测试年份的分布图,这个动作能直接把你的项目从“会跑模型”提升到“会做评估”。

5.3 二手房朝向和楼层的编码:顺序搞错模型跟着错

现象:把朝向LabelEncoder之后,随机森林 RMSE 反而升高了,特征重要性里“朝向”几乎为 0。

原因:LabelEncoder 按字母序给朝向编号,东南西北的顺序全乱了,而且被误当成有序数值。树模型虽然能忍受乱序,但被强制赋予顺序信息的特征会干扰分裂点的选择,尤其是朝向本身对房价只有弱影响时。

解决:朝向、所在区域一律用独热编码;装修、楼层这种有天然递进关系的,才用自定义映射。判断标准很简单:你能不能说清楚这个类别的 0 和 2 比 0 和 1 差距更大?说不清就不该用数值映射。

5.4 归一化放在交叉验证外面:验证分数被污染

现象:把整个数据集先StandardScaler().fit_transform(),再做train_test_split,交叉验证结果异常稳定,换随机种子结果都不变。

原因:缩放时已经用到了测试集的数据分布,严格讲测试集的信息混进了训练流程,这就是一个典型的预处理泄漏。虽然对随机森林这种树模型影响不大,但如果你同时用了线性回归,线性回归的分数会被明显抬高。

解决:先切分,再在训练集上fit_transform,测试集上只transform。如果用了cross_val_score,预处理步骤要放进Pipeline里,让每一折都独立做缩放。

5.5 中文列名与编码问题:数据还没进模型就报错

现象:pd.read_csv一运行就报编码错误,或者打印 DataFrame 时中文全是乱码,df["小区"]直接 KeyError。

原因:Windows 下默认的 CSV 编码是 gbk,而 Python 默认按 utf-8 读;另一部分数据集文件来自网络,可能是 gbk、utf-8、utf-8-sig 三种之一,含 BOM 的文件还会在列名前面多一个看不见的字符。

解决:读取时显式指定编码。乱码或报错就换一种试,或直接用encoding="gbk"读,再不行就用encoding="utf-8-sig"。读进来之后立刻把所有列名里的空格、换行符替换掉:df.columns = [c.strip().replace(" ", "_") for c in df.columns]。别嫌这些小问题低级,每年都有项目源码挂在这上面,技术难度为零但确实浪费时间。

6. 交叉验证与学习曲线:交作业前最后确认模型真的能扛

模型跑完、参数调完,不要急着写文档。我习惯再做两件事来确认结果不是偶然:五折交叉验证和一两条学习曲线。交叉验证能告诉你模型在不同的数据划分下表现是否稳定,而学习曲线能看出当前模型是卡在数据量不够,还是已经过拟合到训练集上。

from sklearn.model_selection import cross_val_score import numpy as np cv_scores = cross_val_score(model, X, y, cv=5, scoring="neg_root_mean_squared_error") print("CV RMSE:", -cv_scores.mean(), "+/-", cv_scores.std())

这里scoring="neg_root_mean_squared_error"是因为 sklearn 的评分规则是“越大越好”,RMSE 越小越好,所以要取负号。五折的结果如果标准差很大,说明你的模型在部分数据子集上翻了车,很可能是有个别异常小区或户型导致的。接着画学习曲线:

from sklearn.model_selection import learning_curve train_sizes, train_scores, val_scores = learning_curve( model, X, y, cv=5, train_sizes=np.linspace(0.1, 1.0, 6), scoring="neg_root_mean_squared_error" )

把结果用 Matplotlib 画出来,横轴是训练样本量,纵轴是 RMSE,两条线分别是训练集和测试集。如果训练集误差低、测试集误差高,中间有明显沟,就继续加数据或加强限制;如果两条线都偏高且靠得近,说明模型容量不够,该换更强的模型。这个图几乎不用解释,老师看了就知道你的调参方向不是拍脑袋。

最后还有一个值得做的事:打印随机森林或 XGBoost 的特征重要性,把前五名特征写进报告里,结合房价直觉说一段“为什么面积排在第一位,朝向排在最后一位”的解读。哪怕你不解释,这个输出也能证明你是认真做了特征分析的人。这是我带项目时一直保持的习惯:不拿第一次跑出来的数字交差,交叉验证和学习曲线都看过、确认稳定了,才敢说这个模型能落地。这个习惯也建议你带进以后的项目里,希望帮到你。

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

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

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

立即咨询