ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

机器学习大作业房价预测:从数据清洗到模型评估的完整流程

2026/10/2 4:37:04 拓冰建站 浏览量
机器学习大作业房价预测:从数据清洗到模型评估的完整流程 简介面向机器学习课程期末大作业的房价预测项目包基于Python实现二手房数据采集、清洗、建模与可视化适合计算机相关专业学生完成大作业或项目实战。项目采用爬虫抓取链家、安居客等平台数据配套CSV数据集与特征工程、模型训练脚本内容经导师审核且本地运行通过评审成绩98分可放心参考。压缩包为zip格式共26个文件、约1.28MB主要包括Python源码、Jupyter分析笔记本、CSV数据集、可视化图片与使用说明。py脚本覆盖数据爬取、预处理与模型评估ipynb提供完整分析流程read/md文档帮助快速上手目录结构清晰便于按模块取用。目前已有111人在线学习下载可直接复用其中爬虫框架和建模代码来节省调试时间同时可学习房价特征选择、回归模型训练与误差验证的完整思路对课程设计、毕业设计或机器学习入门练习都有较高价值。1. 机器学习大作业选房价预测为什么这个题目最容易拿高分房价预测几乎是每所高校机器学习课程期末大作业里出现频率最高的题目原因很现实数据公开、任务清晰、模型对比空间大。但多数人把这题做成“导入波士顿房价→跑个线性回归→输出RMSE”就交差了结果分数平平。实际上房价预测大作业的评分点从来不在于模型多深而在于你有没有把数据处理、特征工程、模型对比、评估分析这一整条流水线走完整。这份源码包对应的就是一条完整流水线从二手房源数据清洗开始到特征构造、多种模型训练与对比、交叉验证评估最后输出预测结果和可视化图表是一份可以直接提交的课程设计作业也适合想完整跑一遍机器学习流程的入门者。它解决的核心问题是你在几周内可能没时间从零写全套代码而这份源码把繁琐的数据处理细节全部封装好你需要的只是读懂每一段在做什么、为什么这么做然后替换成自己的数据集跑通。适合三类人正在赶机器学习或人工智能期末大作业的学生、想用房价数据练手 Kaggle 式流程的初学者、以及需要一份代码基线来快速验证自己想法的从业者。接下来我按实际拆项目时的顺序把这份资源从头到尾捋一遍。2. 数据预处理拿到二手房数据先处理这四个脏点2.1 缺失值不是无脑删先分类型再决定策略绝大多数房价数据集里的缺失值分三类数值型字段缺失、类别型字段缺失、目标变量缺失。很多初学者一看到缺失值就dropna()清场这在数据量大时勉强能用但大作业答辩时老师一定会问“为什么删删了多少影响分布吗”——答不上来就是扣分点。这份源码里的处理逻辑是分层处理的。数值型字段如面积、卧室数优先用中位数填充而不是均值。原因很简单房价数据里面积、总价这类字段往往右偏个别豪宅会把均值拉得很高用均值填充会让普通房源的预测整体偏移。中位数对偏态分布更稳健。类别型字段如朝向、装修情况则用众数填充缺失太多才考虑单独标记一列“是否缺失”作为特征。import pandas as pd import numpy as np df pd.read_csv(house_data.csv) # 数值列中位数填充 num_cols [area, bedrooms, bathrooms, floor] for col in num_cols: df[col] df[col].fillna(df[col].median()) # 类别列众数填充并记录缺失标记 cat_cols [orientation, decoration] for col in cat_cols: df[col _is_missing] df[col].isna().astype(int) df[col] df[col].fillna(df[col].mode()[0])逻辑说明先分离数值列和类别列分别用不同策略填充同时为类别列保留缺失标记让模型自己学习“缺失”这个信息是否有预测力。参数说明fillna(df[col].median())里如果你面对的数据分布接近正态可以换成mean()mode()[0]取第一个众数防止类别列有多个众数时 pandas 返回数组导致赋值失败。2.2 离群点处理房价数据里“豪宅”不是噪声这是最容易翻车的一步。很多教程教人用 3σ 准则删离群点直接套在房价上会把真正的豪宅删掉。比如某城市二手房源里一套独栋别墅总价 5000 万在普通住宅数据集里按 3σ 判断就是妥妥的离群点但它在现实中是真实存在的样本删了反而让模型学不到高端市场的规律。正确做法是先看分布再做判断。源码里用的是 IQR四分位距检测但只在目标变量 y 上做而且不是删除是截断处理——把超出边界的值压缩到边界值而不是直接丢弃。Q1 df[total_price].quantile(0.25) Q3 df[total_price].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR df[total_price] df[total_price].clip(lower_bound, upper_bound)逻辑说明quantile(0.25)和quantile(0.75)算出下四分位和上四分位IQR衡量数据中间 50% 的散布范围。clip()把超出上下界的值强制截断到边界值保留样本行数不变。参数说明1.5是 IQR 法的标准系数但对房价这种尾部极重的分布如果你发现截断比例超过 5%可以放宽到3.0。我一般会在截断后打印截断比例确认一下超过 5% 就说明边界值得商榷。3. 特征工程让模型看到数字背后的含义3.1 单价特征与总价特征必须分开构造原始数据里往往同时存在总价和面积但直接丢给模型模型学到的可能是“面积大→总价高”这个显然的线性关系而忽略了“单价”这个更细腻的信号。同一套房子单价高可能意味着地段好、楼层佳、朝向优。源码里专门构造了unit_price total_price / area作为独立特征同时保留总价作为目标变量或辅助特征。另外楼层信息如果是“高/中/低”这类文本不能直接编码成 1、2、3——模型会误认为“高楼层3倍低楼层”。源码里做了独热编码或者降维成“是否高层”“是否中层”两个布尔特征。朝向也是同理“南北通透”这种优质朝向单独拆一列而不是笼统地塞进多分类里。df[unit_price] df[total_price] / df[area] # 楼层文本转有序数值 独热编码双向并行走 floor_map {低: 1, 中: 2, 高: 3} df[floor_num] df[floor].map(floor_map) df pd.get_dummies(df, columns[floor], prefixfloor)逻辑说明unit_price是计算得到的派生特征把总价归一化到单位面积维度消除面积差异带来的混淆。floor_num保留楼层的顺序信息get_dummies同时做独热编码保留类别独立性两者并行输入模型。参数说明prefix参数控制生成的列名前缀避免和原列名冲突。如果你的特征列数膨胀太多可以只保留floor_num而不用独热树模型对有序编码容忍度较高。3.2 时间特征从日期字符串里榨出周期信号如果数据里有挂牌时间或成交时间别只当成字符串丢掉。房价数据里“月份”“星期几”“是否节假日”往往暗含周期性规律——年初挂牌的房子可能因为学位房政策集中年底挂牌可能因为房主急售。源码里把日期拆成年、月、日、星期几以及一个“距年初天数”的周期特征。df[deal_date] pd.to_datetime(df[deal_date]) df[deal_year] df[deal_date].dt.year df[deal_month] df[deal_date].dt.month df[deal_weekday] df[deal_date].dt.weekday df[deal_dayofyear] df[deal_date].dt.dayofyear逻辑说明pd.to_datetime先把字符串统一转成 datetime 类型避免混合格式报错。然后用.dt访问器逐层拆出时间分量。weekday返回 0-6 的整数周一到周日dayofyear返回 1-365 的累计天数这两个特征能让模型捕捉“金九银十”这类季节效应。参数说明如果你的原始数据里时间列不是标准格式需要在to_datetime里加format%Y%m%d这类参数指定解析格式否则 pandas 会按默认格式猜碰到 2024/1/5 和 2024-01-05 混用时会报错或解析错误。3.3 特征相关性检查删掉冗余特征防止多重共线性构造了这么多特征之后必须做一次相关性检查。比如total_price和unit_price之间必然强相关如果两个都喂给线性回归会放大共线性问题导致系数解释失真。源码里用皮尔逊相关系数矩阵筛查阈值定在 0.9超过就把其中一个特征丢弃或合并。corr_matrix df.corr() high_corr_pairs [] for i in range(len(corr_matrix.columns)): for j in range(i): if abs(corr_matrix.iloc[i, j]) 0.9: col_i corr_matrix.columns[i] col_j corr_matrix.columns[j] high_corr_pairs.append((col_i, col_j, corr_matrix.iloc[i, j]))逻辑说明双重循环遍历相关系数矩阵的下三角部分range(i)避免重复计算找出所有相关系数绝对值超过 0.9 的特征对。corr_matrix.iloc[i, j]的值越接近 1 说明两个特征几乎携带相同信息。参数说明0.9 这个阈值不是死的——线性模型用 0.7 就该警惕了树模型对共线性不敏感可以放宽到 0.95。遇到高相关对时我一般保留和业务强相关更强的那一个比如总价和面积强相关时保留总价因为它是模型要预测的目标附近的信息。4. 模型训练与对比线性回归、岭回归、随机森林、XGBoost 怎么选4.1 先跑一个线性基线再逐级增加复杂度很多初学者直接上 XGBoost结果调参调到怀疑人生。正确的做法是先跑一个最简单的线性回归拿到一个 RMSE 基线然后逐步加复杂度岭回归 → 随机森林 → XGBoost。每一步的收益都能清晰看到答辩时你也能说清楚“我为每一步模型选择付出的理由”。源码里训练测试集划分用的是 8:2同时用random_state42固定随机种子。分割时注意一点房价数据如果按地区聚类明显直接随机划分会让训练集和测试集包含同一小区的样本模型“记忆”了小区特征导致评估虚高。更严谨的做法是按小区分组划分但大作业里 8:2 随机划分通常够用答辩时提到这个局限反而加分。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression, Ridge from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score import xgboost as xgb X df.drop(total_price, axis1) y df[total_price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) models { linear: LinearRegression(), ridge: Ridge(alpha1.0), rf: RandomForestRegressor(n_estimators200, max_depth12, random_state42), xgb: xgb.XGBRegressor(n_estimators300, max_depth6, learning_rate0.05, random_state42) } 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, squaredFalse) r2 r2_score(y_test, y_pred) print(f{name}: RMSE{rmse:.2f}, R2{r2:.4f})逻辑说明把四个模型放进同一个字典里循环训练保证所有模型使用完全相同的训练集和测试集这样对比才公平。mean_squared_error的squaredFalse参数直接返回 RMSE均方根误差单位和房价一致方便业务解读——比如 RMSE 是 35.6 万说明平均预测偏差约 35.6 万元。r2_score是决定系数越接近 1 越好。参数说明Ridge(alpha1.0)里的 alpha 是正则化强度越大系数越被压缩RandomForestRegressor的n_estimators200表示 200 棵树max_depth12限制树深防止过拟合XGBoost 的learning_rate0.05是收缩步长越小越稳但要更多树来补偿。4.2 网格搜索调参注意验证策略别用测试集调参这是大作业里最容易被抓的学术规范问题。有些同学直接拿测试集的 RMSE 来调参调完再用同一个测试集报分数这相当于考试时先看答案再做题分数严重虚高。正确做法是把训练集再切一份验证集出来或者用交叉验证。源码里用的是GridSearchCV配合 5 折交叉验证调完参数后再用完全没见过的测试集做最终评估。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200, 300], max_depth: [8, 12, 16], min_samples_split: [2, 5, 10] } rf RandomForestRegressor(random_state42) grid GridSearchCV( rf, param_grid, cv5, scoringneg_root_mean_squared_error, n_jobs-1, verbose1 ) grid.fit(X_train, y_train) best_rf grid.best_estimator_ print(fbest params: {grid.best_params_}) print(fbest CV RMSE: {-grid.best_score_:.2f})逻辑说明GridSearchCV对参数组合做 5 折交叉验证——每次用其中 4 折训练、1 折验证轮换 5 次取平均分。scoring参数用neg_root_mean_squared_error注意 sklearn 的惯例是“负的 RMSE”所以要取负号才是真实 RMSE。n_jobs-1让所有 CPU 核心并行跑参数组合越多加速越明显。参数说明min_samples_split是节点继续分裂所需的最小样本数调大它能让树更保守抑制过拟合。网格搜索完不要直接结束用best_rf在测试集上跑一次predict那个分数才是作业里该报的分数。4.3 特征重要性分析给模型找“能说出口”的依据大作业答辩时老师必问的问题是“你的模型为什么选这些特征”随机森林和 XGBoost 都自带特征重要性属性直接打印排名但要注意这个重要性是基于“分裂收益”算的不是因果解释。你要做的就是把排名最高的几个特征和业务逻辑对齐——比如“面积”“单价”“所在商圈”排在前三就能自圆其说如果某个莫名其妙的特征排第一就要回去查数据是不是出了问题。import matplotlib.pyplot as plt importance best_rf.feature_importances_ feat_names X_train.columns.tolist() feat_imp sorted(zip(feat_names, importance), keylambda x: x[1], reverseTrue) for name, imp in feat_imp[:10]: print(f{name}: {imp:.4f}) # 可视化前10特征 top_n feat_imp[:10] plt.figure(figsize(10, 6)) plt.barh([x[0] for x in top_n][::-1], [x[1] for x in top_n][::-1]) plt.xlabel(Importance) plt.tight_layout() plt.savefig(feature_importance.png, dpi150)逻辑说明feature_importances_返回每个特征的归一化重要性分数总和为 1。sorted降序排列后取前 10 名用barh画水平条形图逆向切片让最重要的特征显示在最上方。参数说明dpi150控制导出图清晰度论文或报告里插图 150 够了PPT 演示可以调 200。如果你用的是 XGBoost重要性可用xgb.plot_importance(model)一步画图但列名需要提前设置feature_names。5. 常见问题排查跑源码时最常踩的五个坑5.1 文件路径报错相对路径和绝对路径的玄学现象代码报FileNotFoundError: [Errno 2] No such file or directory: house_data.csv但你确认文件就在代码同目录下。原因多数脚本用相对路径读取文件但 vscode 或 PyCharm 的工作目录working directory不一定是脚本所在目录而是你打开项目时的根目录。所以相对路径找不到文件。解决统一改成绝对路径或者用os.path动态拼接脚本所在目录源码包里就是后者。import os base_dir os.path.dirname(os.path.abspath(__file__)) file_path os.path.join(base_dir, data, house_data.csv) df pd.read_csv(file_path)参数说明__file__是当前脚本的完整路径os.path.dirname取所在目录os.path.join用操作系统的路径分隔符拼接子目录。这样不管从哪个目录启动项目都不会报错。5.2 pandas 版本差异导致 get_dummies 后列数不一致现象自己跑源码时训练集和测试集经过get_dummies后列数不同模型预测时报ValueError: feature shape mismatch。原因训练集和测试集里类别字段的取值集合不一样。比如训练集所有房子都有“朝向-南”这个类别测试集里恰好没有独热编码后测试集少一列。解决先用pd.get_dummies同时处理两份数据或者用reindex对齐列。源码里的做法是合并处理后再拆分。X_all pd.concat([X_train, X_test]) X_all pd.get_dummies(X_all, columnscat_cols) X_train X_all.iloc[:len(X_train)] X_test X_all.iloc[len(X_train):]逻辑说明先把训练集和测试集纵向拼接统一做独热编码再按原始行数切回去。这样保证两边列完全一致。参数说明注意pd.concat后索引会重置iloc是按位置切分的不受索引影响。5.3 中文路径或中文列名的编码问题现象读取含中文列名的 CSV 时出现乱码或者pyplot画图时中文标签变成方块。原因Windows 下 pandas 默认编码可能是gbk而 CSV 文件是utf-8保存的matplotlib 默认字体不支持中文显示。解决读取时显式指定编码画图前设置中文字体。df pd.read_csv(file_path, encodingutf-8-sig) plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False参数说明utf-8-sig会去除文件头部的 BOM 标记防止第一列列名出现乱码。SimHei是 Windows 自带黑体Microsoft YaHei是微软雅黑Linux 环境改成WenQuanYi Zen Hei。axes.unicode_minusFalse是防止负号显示成方块。5.4 目标变量泄露不小心把未来信息喂给模型现象模型评估分数特别高R2 达到 0.99但实际预测新数据时完全崩塌。原因特征里混入了目标变量的“未来对应值”或强派生字段。比如直接用了“成交总价/面积”生成的单价但测试时根本没有成交总价可用或者数据里同时存在“挂牌价”和“成交价”把挂牌价当成特征输入了。解决检查所有特征列凡是计算逻辑依赖目标变量的直接删掉。单价特征只能在训练阶段构造预测时无法获得真实总价去算单价——除非你用预测出来的总价去回代这就循环引用了属于逻辑硬伤。5.5 网格搜索跑太久参数组合数爆炸现象GridSearchCV跑了半小时还没出结果机器风扇狂转。原因n_estimators取了 5 个值max_depth取 5 个值min_samples_split取 4 个值5折交叉验证下共 5×5×4×5500 次完整模型训练每次训练 200 棵树总计算量巨大。解决先用小规模参数粗调锁定好区域再细调或者用RandomizedSearchCV做随机搜索用更少的组合覆盖更大的参数空间。from sklearn.model_selection import RandomizedSearchCV import scipy.stats as stats param_dist { n_estimators: stats.randint(100, 500), max_depth: stats.randint(5, 20), min_samples_split: stats.randint(2, 20) } random_search RandomizedSearchCV( rf, param_dist, n_iter20, cv5, scoringneg_root_mean_squared_error, random_state42 )逻辑说明stats.randint生成连续整数分布n_iter20只采样 20 组参数组合计算量是网格搜索的零头。random_state42保证结果可复现。参数说明如果你的数据量超过 5 万行建议先用 1 万行子样本粗调锁定超参范围后再全量训练这是实际项目里省时间的常规操作。6. 验证不止看 RMSE用残差图发现模型盲区6.1 残差分布比单一指标更有说服力RMSE 和 R2 只能告诉你模型整体“平均表现”但平均值会掩盖局部系统性偏差。比如模型对低价房预测偏保守、对高价房预测偏激进两者的误差互相抵消RMSE 看着不错实际业务场景里高价房的预测误差会让你亏大钱。残差分析就是看y_true - y_pred的分布。源码里画了两张图残差 vs 预测值的散点图以及残差的直方图。前者如果呈现喇叭形左窄右宽说明数据存在异方差性预测区间在高价区间越来越大。后者如果明显偏离正态分布且均值不为 0说明模型存在系统性偏差。import numpy as np y_pred best_rf.predict(X_test) residuals y_test.values - y_pred print(fMean residual: {np.mean(residuals):.2f}) print(fStd residual: {np.std(residuals):.2f}) # 分价位段看误差表现 bins [0, 200, 500, 1000, np.inf] labels [0-200万, 200-500万, 500-1000万, 1000万] segments pd.cut(y_test.values, binsbins, labelslabels) for seg in labels: mask segments seg seg_rmse np.sqrt(np.mean(residuals[mask] ** 2)) print(f{seg}: RMSE{seg_rmse:.2f}万)逻辑说明residuals是真实值减预测值正值代表低估、负值代表高估。np.mean如果明显偏离 0比如均值是 -8 万说明模型整体高估了 8 万。pd.cut把测试集的真实房价切成四个价格段分别计算每段的 RMSE——你会发现 1000 万以上价位的 RMSE 可能是低价段的几十倍这就是模型的盲区。6.2 分价位段误差是答辩杀手锏把上面的分价位 RMSE 做成柱状图答辩时直接展示“模型在 200 万以下的预测误差控制在 15 万左右但 1000 万以上误差放大到 80 万原因是高端房源样本量少、特征区分度不足”。这句话的价值比任何调参技巧都高因为它表明你不仅知道模型表现好还知道它在哪里表现不好、为什么不好。从那以后我每次跑完模型不会先去看 R2而是强制自己先做这一遍残差和分段误差分析。R2 好看只能说明整体趋势拟合到位但交作业或交付项目时“哪里不行”往往比“哪里行”更能扛住追问。这份源码包里已经把这段分析和可视化都写好了你拿到后第一件事应该是把自己的数据跑进去先看残差图再决定要不要调参——这比盲目优化模型参数有用得多。希望帮到你。本文还有配套的精品资源点击获取