☰
二手房价格预测系统实战:从数据清洗到PyQt5界面开发全流程
2026/9/28 16:02:17 网站建设 项目流程

简介:这是一份面向Python数据分析与GUI开发学习者的二手房价分析与预测期末大作业项目包,基于Python和PyQt5完成,覆盖数据读取、清洗统计、特征分析、模型训练、房价预测和可视化展示等完整环节,适合期末答辩、课程设计及自学进阶。压缩包共17个文件,包含6个Python源码文件、6张PNG界面素材、2个pyc缓存、1个Qt界面文件、1个CSV数据集和1个说明文档,整体体积仅132KB,目录结构清晰,便于按模块阅读与复用。项目中利用Pandas完成数据清洗与统计摘要,Scikit-Learn构建预测模型,Matplotlib生成分布与结果图表,并通过PyQt5搭建交互界面,实现加载数据、选择模型、查看结果的一站式操作。源码附带详细注释,可将Pandas、Scikit-Learn、Matplotlib、PyQt5各技术点与工程实现一一对应,同时内置可用的CSV数据集与界面素材,省去自行准备数据的麻烦。目前已有343人学习,适合需要完整案例参考的在校学生和初级开发人员。

1. 期末大作业的二手房价分析与预测系统:它到底解决了什么问题

期末项目选“二手房价分析与预测”这个题目,几乎是每年 Python 课程设计的保留曲目。需求看起来很简单:拿到一批二手房数据,训练一个能预测价格的模型,再用图形界面把结果展示出来。但真做起来,数据清洗、特征构造、模型调参、界面联调每一环都够折腾几天。这套系统就是把这些环节串成一个完整的闭环:用 Python 处理数据、训练模型,用 PyQt5 做桌面界面,用户在界面上输入户型、面积、楼层、朝向等条件,就能看到预测价格和周边小区对比。适合正在做期末大作业、毕业设计,或者想快速搭一个数据展示型桌面应用的读者。想用最短路径把作业跑通,同时把每个模块的坑提前踩掉,这篇就是按这个思路写的。

2. 二手房价数据集的清洗与特征工程:喂给模型之前先做这几步

2.1 先看数据长什么样:字段类型与缺失值排查

打开二手房价数据集的 CSV 文件,常见字段大概是这几类:小区名称、区域、户型(室厅卫)、面积、朝向、楼层、装修、建筑年代、总价、单价。第一件事不是直接训练,而是把数据读进来逐字段看一眼。很多二手房价数据在采集时就有问题,比如“朝向”列出现“南北通透”“南 北”两种写法,“楼层”列混着“低楼层/共6层”这种文本,这类字段直接丢给模型会变成灾难。

我一般会把数据读取和初步体检写成一个脚本,先跑一遍再决定怎么做特征:

import pandas as pd df = pd.read_csv("house_data.csv", encoding="utf-8") print(df.shape) print(df.dtypes) print(df.isnull().sum()) # 抽样看几行,确认文本字段的写法 print(df.head(10).to_string())

这里有几个关键点。encoding参数特别容易踩坑,很多爬下来的 CSV 是gbk编码,直接用utf-8读会抛UnicodeDecodeError。报错时先尝试encoding="gbk"或encoding="gb18030"。isnull().sum()是看缺失值的入口,先知道哪些列缺得厉害,再决定是删行、填值还是拆特征。打印dtypes则是看数据类型的隐患,比如面积列被读成object,说明里面有类似“89.5㎡”这种带单位的脏数据。

2.2 把文本特征变成数值特征:面积、楼层、朝向的处理方式

文本特征不处理,模型就训不了。目标是把每一列都收拾成干净的数值或类别编码。先说面积列,如果里面混了单位,先用正则把数字提取出来;楼层列比较典型,常见写法是“低楼层/共6层”或“3/6层”,这里可以拆成两个特征:当前楼层数、总楼层数,再用总楼层数算出一个相对位置。朝向这类文本列,用get_dummies转成哑变量,或者用LabelEncoder编码。我的做法是能拆出数值就拆数值,朝向这种没有大小关系的列才做哑变量。

import re # 提取面积中的数字,"89.5㎡" -> 89.5 df["area"] = df["area"].str.extract(r"(\d+\.?\d*)").astype(float) # 楼层列处理,"低楼层/共6层" -> current=1, total=6 def parse_floor(s): m = re.search(r"共(\d+)层", str(s)) c = re.search(r"(\d+)/", str(s)) if c and m: return int(c.group(1)), int(m.group(1)) return None, None df["current_floor"], df["total_floor"] = zip(*df["floor"].apply(parse_floor)) df["floor_ratio"] = df["current_floor"] / df["total_floor"] # 朝向只保留第一个方向,减少类别数 df["direction"] = df["direction"].str.split("/").str[0] df = pd.get_dummies(df, columns=["direction"], prefix="dir")

str.extract配合正则提取数字,是处理混合格式最顺手的方式,记得提取后要astype(float)转换,不然还是文本。floor_ratio的含义是“楼层的相对高度”,0.3 代表在总楼层偏下位置,0.8 代表在高区。这个特征对房价的影响其实是有的,高层采光好,但也要看有没有电梯。朝向拆成哑变量之前,先只保留第一个方向,能让列数少一点,避免“南北”“南 北”这种同一含义拆成两列。

2.3 对总价做归一化:为什么预测总价比预测单价更稳

作业里很多人直接拿总价做预测目标,但二手房数据里的总价受面积影响太大,同样 300 万,30 平米和 120 平米的房子完全不是一个量级。这里有一个常见分歧:预测总价,还是预测单价。我建议做单价预测,因为单价剔除了面积的影响,更能反映地段、楼层、装修这些特征的作用。预测完之后,展示时再乘上用户输入的面积,这样用户看到的总价也更直观。

# 计算单价,作为预测目标 df["unit_price"] = df["total_price"] / df["area"] # 对连续特征做标准化 from sklearn.preprocessing import StandardScaler feature_cols = ["area", "bedrooms", "living_rooms", "bathrooms", "floor_ratio", "building_age"] scaler = StandardScaler() df[feature_cols] = scaler.fit_transform(df[feature_cols])

单位价的单位是“万元/平米”还是“元/平米”,取决于原始总价单位,分析时要先看好数据集里的字段说明。用StandardScaler做标准化的一个实际好处是,后面用线性回归、岭回归这类模型时,收敛更快,系数也能直接比较大小,用来判断“哪个特征对房价影响更大”。如果数据集里的房子坐标、板块信息可用,还可以考虑加“距地铁站距离”这类衍生特征,这类资料往往要去高德或百度地图的 API 算,作业时间紧的话用小区均价替代也行。

重要提示:fit_transform只应该在训练集上调用,验证集和测试集要用训练集拟合好的scaler做transform,不要在验证集上重新算均值方差。这是很多作业被老师看出“数据泄漏”的点,后面我还会细说。

3. 房价预测模型选型与训练:从线性回归到集成模型的全过程

3.1 先用线性回归打基线:结果再差也要有一个参照

拿到干净数据后的第一件事,不是直接上随机森林、XGBoost,而是先用一个最朴素的线性回归跑一遍,得到一组“下限分数”。这样做的价值是,后面换任何模型都有了参照系——新模型比线性回归高多少分,才算真正有效果。很多作业翻车,就是先上复杂模型,结果调参半天也不知道自己调到什么程度算“好”。

from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score X = df[feature_cols + [c for c in df.columns if c.startswith("dir_")]] y = df["unit_price"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = LinearRegression() model.fit(X_train, y_train) y_pred = model.predict(X_test) print("RMSE:", mean_squared_error(y_test, y_pred, squared=False)) print("R2:", r2_score(y_test, y_pred))

这套流程是标准的分割、训练、评估三件套。random_state=42固定随机种子,保证每次运行结果一致,这是作业里必须养成的习惯,不然答辩的时候跑两次结果不一样,没法跟老师解释。R²在房价预测里一般能到 0.6~0.8 就算不错的线性结果,如果低于 0.4,说明数据里非线性关系强,或者特征太粗糙,这时候再去换集成模型才有意义。

3.2 换随机森林:处理非线性关系的主要手段

线性回归跑完,接下来上随机森林。随机森林对异常值和特征尺度不敏感,不用再担心标准化的问题,而且能给出特征重要性,方便你在答辩时跟老师说“哪些特征对房价影响最大”。随机森林的主要参数是n_estimators、max_depth、min_samples_split,这些参数不要盲目抄网上的值,要结合数据量来定。

from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor( n_estimators=200, max_depth=12, min_samples_split=5, min_samples_leaf=2, random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) y_pred_rf = rf.predict(X_test) print("RF RMSE:", mean_squared_error(y_test, y_pred_rf, squared=False)) print("RF R2:", r2_score(y_test, y_pred_rf)) print(pd.Series(rf.feature_importances_, index=X.columns).sort_values(ascending=False))

这里n_estimators=200和max_depth=12是我试过比较多组之后觉得比较稳的起点。数据量在几千条的时候,200 棵树比 100 棵的误差略低,再往上就边际递减了。max_depth如果设得太深比如 20 以上,很容易过拟合,训练集 R² 接近 1,但测试集掉得厉害。特征重要性输出之后,通常会发现面积、建筑年代、floor_ratio 排在最前,朝向的哑变量普遍靠后,这是数据本身的信号,不代表朝向没用,只能说在这个数据集里变化不够大。

3.3 再用 XGBoost 调高一点:网格搜索要控制时间

随机森林效果如果已经不错,可以再试 XGBoost。XGBoost 在房价这类表格式数据上成绩稳定,而且处理缺失值、非线性都比线性模型强。常见的调参顺序是:先定学习率learning_rate和树数量n_estimators,再调max_depth和min_child_weight,最后调subsample和colsample_bytree。作业时间有限,不要做全量网格搜索,用GridSearchCV也要控制参数组合数量,不然一个交叉验证可能跑半小时以上。

import xgboost as xgb from sklearn.model_selection import GridSearchCV xgb_model = xgb.XGBRegressor( learning_rate=0.05, n_estimators=300, max_depth=6, subsample=0.8, colsample_bytree=0.8, random_state=42 ) param_grid = { "max_depth": [4, 6, 8], "subsample": [0.7, 0.8, 0.9] } grid = GridSearchCV( xgb_model, param_grid, cv=5, scoring="r2", n_jobs=-1, verbose=1 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)

cv=5的意思是五折交叉验证,每一折都用训练集的一部分做验证,最终best_score_是五折的平均 R²,这比单次划分测试集更稳定,答辩时也是一个可说的亮点。n_jobs=-1会用满所有 CPU 核心,但如果你的电脑是 4 核老笔记本,跑网格搜索时记得把verbose=1打开,能看到进度,不然看起来像死机。还有一个实操细节:XGBoost 在数据量小的时候容易过于自信,测试集 R² 高但真实场景不一定好,所以在作业里最好同时保留随机森林和 XGBoost 两个模型,界面上做一个模型下拉切换,效果对比更直观。

参数调优是门玄学,没有一组参数适合所有数据集。比较稳的办法是固定一颗随机种子,用交叉验证评估,每次只动一个参数,记录分数变化,这样既能看到趋势,也能省时间。

4. 用 PyQt5 把预测模型包装成交互界面:从模型文件到桌面应用

4.1 界面布局:输入区和结果区怎么规划

预测模型跑通后,下一步就是用 PyQt5 做一个桌面界面。界面一般分成三个区域:左侧是房源信息输入区,包括面积、户型、朝向、楼层、建筑年代等;右侧是预测结果展示区,显示预测单价、总价和一个小区的对比;底部放一个“开始预测”按钮和状态栏。PyQt5 的布局用QFormLayout加下拉框QComboBox和数值输入框QSpinBox的组合,比手写坐标定位方便得多。

from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QFormLayout, QComboBox, QSpinBox, QDoubleSpinBox, QPushButton, QLabel, QVBoxLayout, QHBoxLayout ) class HousePriceWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("二手房价格分析与预测系统") self.setMinimumSize(720, 480) central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) form = QFormLayout() self.area_input = QDoubleSpinBox() self.area_input.setRange(20.0, 300.0) self.area_input.setSuffix(" ㎡") self.room_input = QSpinBox() self.room_input.setRange(1, 6) self.floor_input = QSpinBox() self.floor_input.setRange(1, 40) self.direction_input = QComboBox() self.direction_input.addItems(["东", "南", "西", "北"]) form.addRow("面积:", self.area_input) form.addRow("卧室数:", self.room_input) form.addRow("所在楼层:", self.floor_input) form.addRow("朝向:", self.direction_input) layout.addLayout(form)

这里每个输入控件的数据范围都做了约束,比如面积限制在 20 到 300 平米,这是根据数据集分布来的。QDoubleSpinBox支持小数,适合面积;QSpinBox只能整数,适合卧室数这类离散值。QComboBox做朝向选择,避免用户输入五花八门的文本,这是桌面应用里很讨巧的设计——从源头杜绝脏数据。

4.2 按钮事件与预测逻辑打通:加载模型、构造特征、输出结果

界面搭好之后,核心工作量在“点按钮之后干什么”。我习惯把模型保存成文件,界面启动时用joblib加载,而不是每次点按钮都重新训练。预测的逻辑链是:读取界面控件里的值 → 按训练时的顺序构造特征向量 → 喂给模型 → 把输出格式化成“单价 + 总价”展示。这里最容易出错的点,是特征顺序错乱。训练时的特征列顺序是固定的,如果界面这边少传了一列、多传了一列,预测结果就跑偏,而且这种错很难查。

import joblib # 训练完成后保存模型和特征列 # joblib.dump(model, "model_rf.pkl") # joblib.dump(list(X.columns), "feature_columns.pkl") class HousePriceWindow(QMainWindow): def __init__(self): # ... 上面的控件初始化代码 ... self.predict_btn = QPushButton("开始预测") self.result_label = QLabel("预测结果将显示在这里") layout.addWidget(self.predict_btn) layout.addWidget(self.result_label) self.predict_btn.clicked.connect(self.on_predict) def on_predict(self): self.model = joblib.load("model_rf.pkl") self.feature_cols = joblib.load("feature_columns.pkl") area = self.area_input.value() rooms = self.room_input.value() floor = self.floor_input.value() direction = self.direction_input.currentText() # direction 的哑变量必须与训练时一致 feat = {} for c in self.feature_cols: if c.startswith("dir_"): feat[c] = 1 if c == f"dir_{direction}" else 0 elif c == "area": feat[c] = area elif c == "floor_ratio": feat[c] = floor / (floor + 9) # 假设数据中位总楼层 else: feat[c] = 0 import pandas as pd sample = pd.DataFrame([feat])[self.feature_cols] pred = self.model.predict(sample)[0] total_price = pred * area self.result_label.setText( f"预测单价: {pred:.0f} 元/平米\n" f"预测总价: {total_price:.0f} 元" )

这段代码里有一个关键细节:哑变量是按feature_cols里的顺序生成的,而且只生成训练时出现过的方向列。如果训练数据里有“东南”这个朝向,界面里却没有这个选项,预测时就会漏列并报错。解决办法就是上面这种“从feature_cols反推构造字典”的思路,保证列名、列顺序永远跟着训练时的记录走。floor_ratio的计算这里用了floor / (floor + 9)来模拟相对楼层,实际作业时可以简化成从界面传总楼层和所在楼层一起算,更准确。

关于模型文件保存,有一套配套命令先跑:

# 训练完模型后执行保存 import joblib joblib.dump(rf, "model_rf.pkl") joblib.dump(list(X.columns), "feature_columns.pkl")

保存两个文件的原因是为了还原特征顺序,这也避免了把scaler和模型绑在一起时出现的状态错乱。如果数据里还有标准化步骤,最好用joblib把scaler也保存成一个文件,加载模型后先做标准化再喂数据,这个顺序别颠倒。

4.3 PyQt5 内嵌 matplotlib:用图表把预测结果讲清楚

只有一行数字的界面显得单薄,加分项是在界面里加一个图表,展示用户输入的房源与周边小区均价对比,或者展示该小区历史价格走势。PyQt5 内嵌 matplotlib 的常见做法是用FigureCanvasQTAgg把画布嵌到窗口里,替代传统的plt.show()弹窗模式。

import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure class ChartWidget(FigureCanvas): def __init__(self, parent=None): self.fig = Figure(figsize=(5, 3), dpi=100) super().__init__(self.fig) self.setParent(parent) self.ax = self.fig.add_subplot(111) def update_chart(self, price, avg_price): self.ax.clear() labels = ["预测单价", "小区均价"] values = [price, avg_price] bars = self.ax.bar(labels, values, color=["#4C72B0", "#DD8452"]) self.ax.set_ylabel("价格(元/平米)") for bar, v in zip(bars, values): self.ax.text(bar.get_x() + bar.get_width()/2, v, f"{v:.0f}", ha="center", va="bottom") self.ax.set_title("预测价格 vs 周边均价") self.draw()

这里FigureCanvasQTAgg是把 matplotlib 的图嵌入 PyQt5 的关键接口,update_chart每次先clear()再重新绘制,避免画布重影。要注意的是,dpi=100在 1080p 屏幕上显示还行,分辨率更高的屏幕上字会偏小,可以调到 120~150。图表数据里的avg_price,可以从数据集里按小区分组算好均值,存成一个 dict,界面加载时直接读取即可,不必实时查数据库。

5. 避坑:期末大作业最常见的 6 个翻车现场与排查方法

5.1 PyQt5 安装失败:报错信息对不上怎么办

期末季最热门的问题之一就是“PyQt5 安装不上”。常见现象是pip install pyqt5卡在下载阶段,或者装完之后from PyQt5.QtWidgets导入报错ModuleNotFoundError。原因有两个:一是 PyQt5 的 wheel 包体积大,默认源下载慢,二是 Python 版本和 PyQt5 版本不匹配。解决方法是换国内镜像源安装,同时注意 Python 版本,Python 3.9 以上装 5.15 系列基本没问题,Python 3.6 太老的话装新版会直接拒绝安装。

在命令行里执行下面的命令,装完验证一次导入,能省去后面大量的界面调试时间:

pip install pyqt5 -i https://pypi.tuna.tsinghua.edu.cn/simple python -c "from PyQt5.QtWidgets import QApplication; print('ok')"

如果提示ModuleNotFoundError: No module named 'PyQt5.sip',这是 PyQt5 和 PyQt5-sip 版本不对齐的老问题,单独再装一次pip install pyqt5-sip通常就解决了。

5.2 模型预测结果几乎全是同一个值

辛苦训练完之后,发现不管是输入什么户型,预测出来的价格都差不多,图表上是一条平线。这个现象的原因集中在特征上:要么是特征列顺序错乱,模型吃进去的数值和训练时完全不对应;要么是界面构造的feat字典缺失关键列,模型只能用默认值填充。排查方法是先打印出每次预测时的特征向量,检查面积、楼层这类高权重特征是否真的随界面输入在变化。

可以在预测代码里临时加一行:

print(pd.DataFrame([feat])[self.feature_cols].to_string())

然后切换几次界面的输入值,看对应列的数字有没有变化。如果发现floor_ratio恒为某个固定值,那问题多半出在楼层换算公式上;如果所有列都不变,那是feat字典的键名没跟feature_cols对齐。

5.3 PyQt5 界面点击按钮后无响应或卡死

点“开始预测”之后窗口卡住,转圈几秒甚至直接崩溃。这类问题几乎都是因为在主线程里跑了重型计算。on_predict里如果直接加载几百 MB 的模型文件,或者把网格搜索也放进去了,界面就会阻塞。解决方法是把模型加载放到窗口初始化阶段,只做一次,另外把预测运算放到QThread里,界面保持响应。期末作业如果数据量不大,模型文件一般只有几十 MB,放在__init__里加载是足够快的。

线程问题是 PyQt5 里最典型的翻车点之一。简单做法是写一个Worker类,用QThread的run方法执行预测并发射信号返回结果:

from PyQt5.QtCore import QThread, pyqtSignal class PredictWorker(QThread): finished = pyqtSignal(float, float) def __init__(self, model, sample_df): super().__init__() self.model = model self.sample_df = sample_df def run(self): pred = self.model.predict(self.sample_df)[0] self.finished.emit(pred, pred * self.sample_df["area"].values[0])

界面上实例化这个 worker,把finished信号连到一个槽函数更新self.result_label。信号槽机制是 PyQt5 里跨线程回传数据的事实标准,用好了整个界面都不会卡。注意线程对象要在界面类里持有引用,防止被垃圾回收掉导致信号发不出来。

5.4 数据集里出现重复或异常明显的离群值

二手房价数据里有大量重复的房子记录很常见,同一个小区同一套户型在不同时间被抓了多次;还有那种 2000 年的老破小卖 2000 万,或者郊区 500 平别墅单价 1 万这样的极端值。不去重、不处理离群值,模型会被这些点拉偏。处理分两步:第一步按房源 ID 或“小区+面积+户型”去重;第二步用箱线图法把单价在上下四分位之外 5 倍四分位距的点标出来,人工判断是删还是留。

# 去重 df = df.drop_duplicates(subset=["community", "area", "bedrooms", "living_rooms"]) # 单价的四分位距过滤 Q1 = df["unit_price"].quantile(0.25) Q3 = df["unit_price"].quantile(0.75) IQR = Q3 - Q1 df = df[(df["unit_price"] >= Q1 - 5 * IQR) & (df["unit_price"] <= Q3 + 5 * IQR)]

这里用5 * IQR而不是常见的1.5 * IQR,是因为房价数据本身方差大,用1.5会把大量正常房源误杀。这个倍数没有标准答案,可以先画出单价分布直方图,再按倍数调整看保留样本量是否合理。离群值处理会直接影响最终分数,值得多花时间。

5.5 界面打包成 exe 后缺少依赖

交作业时导师经常要求打包成可执行文件,双击就能跑。用pyinstaller -w main.py打完的 exe 经常在别人的电脑上报错,最常见的是缺 PyQt5 的插件目录,或者缺模型文件的路径。解决方法是把模型文件和代码放在同一个目录,打包时用--add-data把模型文件带进去,并且在代码里用sys._MEIPASS处理临时解压路径。

pyinstaller -w -F main.py --add-data "model_rf.pkl;." --add-data "feature_columns.pkl;."

-w是隐藏控制台窗口,-F是打包成单文件。--add-data后面的分号在 Windows 下是路径分隔符,Linux/macOS 下要换成冒号。代码中读取文件的路径需要写成:

import sys, os base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) model_path = os.path.join(base_path, "model_rf.pkl")

用_MEIPASS的原因是 PyInstaller 单文件打包后,资源文件会被解压到临时目录,直接读相对路径会找不到文件。这个坑比较隐蔽,属于打包阶段才能发现的类型,提前写对能省很多时间。

5.6 训练集和验证集的数据泄漏:你精心调出来的分数可能无效

这是最容易被老师问倒的一个点。如果先对整个数据集做了StandardScaler,再切分训练集和测试集,验证集的分布信息已经提前泄露给了训练过程。这个泄漏会让测试分数虚高,答辩时老师抽查代码很容易发现。正确顺序是:先train_test_split,再在训练集上fit,然后transform两个集。随机森林这类树模型不需要归一化,但如果你的流程里用了线性回归或者神经网络,这个顺序就必须严格。

X_train, X_test, y_train, y_test = train_test_split(...) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)

这组代码体现的规则是:所有“学习参数”的操作都只允许在训练集上做。缺失值填补、特征选择、降维同理,都要先把 fit 和 transform 分开。很多网上的教程没有强调这一点,作业里被老师提出来就是硬伤。

6. 把作业做深一个档次:交叉验证、特征重要性与答辩验证技巧

到了这个阶段,你的系统已经能跑通了:数据处理、模型训练、PyQt5 界面、打包成 exe 全套都有。但如果想让老师觉得“这个学生真的理解自己在做什么”,还有三件性价比很高的事可以做,最后一件对答辩最有用。

第一件事是把单次train_test_split评估换成cross_val_score。交叉验证用多轮数据切分取平均,能给出模型真实水平的上限和下限,规避单次划分运气好或运气坏的问题。把交叉验证结果打成一个表格,放到 README 或者答辩 PPT 里,比只报一个测试 R² 要有说服力得多。代码很简单:

from sklearn.model_selection import cross_val_score scores = cross_val_score(rf, X_train, y_train, cv=10, scoring="r2") print(f"10折交叉验证 R2: {scores.mean():.4f} ± {scores.std():.4f}")

±后面的标准差代表模型在不同数据子集上的稳定性,这个数字越小越好。如果标准差超过了 0.1,说明模型对数据划分太敏感,这时候就要考虑是不是数据量太少,或者模型过拟合了。

第二件事是在界面上加一个“特征重要性”的展示。把随机森林的feature_importances_画成横向条形图,面积、建筑年代、楼层位置排在前三名,这个图表既是模型的解释工具,也是答辩时的讲解素材。可以利用前面写的ChartWidget类,把条形图嵌到界面右侧,用户点击“模型分析”按钮就能切换到这张图。这个功能做起来半小时,但对技术深度的展示效果远超过预期。

第三件事,也是回报最高的,是做一个简单的模型对比表,把线性回归、随机森林、XGBoost 三个模型的 RMSE、R²、训练时间都列出来。不用做得多复杂,一个QTableWidget就能搞定,或者直接在报告里画一张表格。内容上要写清楚“为什么随机森林在这个数据集上比 XGBoost 好”,这个分析本身就体现了差异化的思考能力。常见原因是数据集量不大、特征不多时,XGBoost 的复杂度优势发挥不出来,而随机森林更容易收敛。

我个人的习惯是,把整个项目按“数据 → 特征 → 模型 → 界面 → 验证”分成五个目录,每个目录放一个README.md,写明这个环节做了什么、选了什么参数、踩了什么坑。这个习惯在答辩时帮了大忙——老师说“你这个楼层比例特征怎么想到的”,我直接打开特征工程的 README,里面写了两行我当时对比过不同楼层算法的结果,比现场组织语言要从容得多。

整个项目做下来,最大的感受是:期末大作业拼的不只是调包能力,更是工程化的组织能力。数据不乱、模型可复现、界面可交互、验证说得清,这四个维度做到位,分数不会差。希望这篇笔记能帮到你,在动手之前少走几个弯路。

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

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

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

立即咨询