☰
基于PyQt5的二手房价预测系统:数据清洗到模型界面化实战
2026/10/3 3:00:33 网站建设 项目流程

简介:一份基于Python和PyQt5实现的二手房价分析与预测系统期末大作业,面向学习Python数据分析、机器学习与GUI开发的高校学生,可作为课程设计、毕业设计或入门实战项目。系统完整覆盖数据读取、缺失值处理、特征分析、模型训练与结果可视化全流程,内置线性回归等经典算法,通过Matplotlib绘制房价分布、趋势及预测对比图,PyQt5搭建可交互界面,支持加载数据集、切换模型与查看分析结果。包体共17个文件,含6个py源码文件、6个png图表、1个ui界面文件、csv数据集、md说明文档及pyc缓存,压缩包仅132KB,结构清晰便于直接运行与二次开发。源码附详细注释,涵盖Pandas数据处理、Scikit-Learn建模、Matplotlib绘图及PyQt5布局等关键知识点,数据集中含真实二手房价特征字段,可据此调整参数、扩展算法。目前已有343人学习下载,能帮助快速贯通数据分析与机器学习在真实场景中的应用。

1. 期末大作业里的二手房价预测,难点到底在哪

期末大作业只要沾上“房价预测”,十个人里有八个把重心压在模型上,真正拿高分的人却往往赢在数据和界面。基于 Python 和 PyQt5 实现的二手房价分析与预测系统,核心不是某个高级算法,而是把 pandas 清洗、sklearn 建模、PyQt5 桌面界面串成一条能现场演示的完整链路。它解决的是课程设计最典型的三个诉求:能跑通、能演示、能讲清楚原理。适合拿源码做模板改造的学生,也适合想快速复现一套 PyQt5 数据分析案例的初学者。下文按架构、数据、模型、界面、打包的顺序,把能直接抄作业的代码和踩过的坑一次讲完。

2. 拆解项目架构:为什么选 PyQt5、目录怎么组织、界面怎么画

2.1 为什么是 PyQt5 而不是 Flask 网页版

做期末大作业,最忌讳的是花时间在“环境跑不起来”这种和内容无关的事情上。PyQt5 的优势恰恰在于它是个纯本地桌面框架,不依赖浏览器、不依赖后端服务,启动一个 main.py 就能看到窗口,适合答辩现场不联网的环境。

另一个现实原因:后端逻辑是 pandas 和 sklearn,PyQt5 和它们同在 Python 生态里,数据可以从 DataFrame 直接塞进表格控件,不用像 Web 方案那样写 JSON 接口。用 FastAPI 或 Flask 也不是不行,但你要多处理一整套请求、路由、渲染的复杂度,还可能面对浏览器跨域之类的幺蛾子。课程设计的时间本来就紧,把复杂度留在自己熟悉的 Python 这一层,比把问题引到前端更划算。

Flask 方案适合你后续想把它改造成线上产品;如果目标只是“期末大作业”这四个字,PyQt5 是性价比最高的选择。它在 PyCharm 里调试也顺手,设置好 Python 解释器后直接运行,不需要额外启动任何服务。

提示:Python 环境建议用 3.9 以上的版本,PyQt5 用 pip install PyQt5 安装最省事。如果遇到慢的情况,换国内镜像源即可,不要自己去下安装包乱试。

2.2 源码目录怎么拆,每个文件干什么

下载下来的 zip 解压后,内部结构通常不是一个大文件堆在一起,而是把界面、数据处理、模型分成独立模块。这种拆分不是为了好看,而是为了答辩时能单独讲每个文件的作用。我建议用下面这种目录组织,适合房价分析和预测系统的通病——数据、界面、模型互相纠缠。

# 推荐目录结构,解压后按这个思路整理 second_hand_house/ ├── data/ # 数据集目录 │ └── house_data.csv # 二手房价原始数据 ├── ui/ # 界面相关代码 │ ├── main_window.py # 主窗口,负责布局与事件 │ └── charts.py # 封装 matplotlib 图表绘制 ├── core/ # 业务逻辑 │ ├── data_clean.py # 数据清洗函数 │ ├── feature.py # 特征工程函数 │ └── model.py # 模型训练与预测封装 ├── models/ # 训练好的模型文件 │ └── house_model.pkl └── main.py # 程序入口,只做启动一件事

这样拆的好处是,答辩时你说“main.py 只负责启动,数据清洗在 core/data_clean.py,界面在 ui/main_window.py”,结构上就比一个几百行的单文件体面得多。数据文件单独放 data 目录,是为了避免和代码混在一起,后面打包和替换数据集都方便。

实际项目里,我更推荐把每个模块都写成函数而不是脚本。例如 data_clean.py 里暴露 clean_data(df) 函数,model.py 里暴露 train_model(X, y) 和 load_model(path)。这样你在 ui 层调用时,只需要 import 进来,不必关心内部实现。

2.3 Qt Designer 画模板和纯代码手写界面

做 PyQt5 界面有两条路:用 Qt Designer 拖控件生成 .ui 文件,再转成 .py;或者直接手写代码。期末大作业我一般建议主窗口用手写代码,画布和控件少的表单也用代码,原因很实际:手写代码在 merge 和改动时更可控,答辩老师问“这个按钮怎么加上去的”,你直接说 addWidget 比说“我用 Designer 拖的”更经得起追问。

但对于控件数量超过十个的复杂窗口,Qt Designer 省时间。常见做法是用 Designer 画好布局,再用 pyuic5 命令把 .ui 转成 .py,之后在代码里继承这个生成的类。两种方式可以混用,核心窗口手写,复杂表单用 Designer。

下面给一个最小可运行的主窗口骨架,包含了标题、按钮和布局,这是整个系统的地基:

import sys from PyQt5.QtWidgets import ( QApplication, QMainWindow, QVBoxLayout, QPushButton, QLabel, QWidget ) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("二手房价分析与预测系统") self.resize(1100, 700) # 宽 1100,高 700,适合展示表格和图表 # 中心控件:QMainWindow 不能直接挂布局,必须套一层 QWidget central_widget = QWidget() self.setCentralWidget(central_widget) # 垂直布局:从上到下依次放控件 layout = QVBoxLayout(central_widget) self.title_label = QLabel("区域均价统计") layout.addWidget(self.title_label) self.load_button = QPushButton("加载数据集") layout.addWidget(self.load_button) # 这里预留的信号连接,通常在另一个方法里写 # self.load_button.clicked.connect(self.on_load_data) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())

这段代码的逻辑很简单:QMainWindow 是主窗口基类,setCentralWidget 设置中心控件,QVBoxLayout 管理垂直排列。setWindowTitle 和 resize 决定了演示时的第一印象,标题要写全称,尺寸建议 1100x700 以上,太小了表格和图表挤在一起不好看。QApplication(sys.argv) 是每个 PyQt5 程序的必经入口,sys.exit(app.exec_()) 保证窗口关闭后进程正常退出,这两行不要省略。

从布局到业务还有最后一层:按钮点击后要触发加载数据、画图、预测这些动作。我习惯把所有槽函数写在 MainWindow 类里,用 clicked.connect 绑定。这样做的好处是,界面层只负责调用 core 层的函数,不直接操作 pandas 或 sklearn,职责清楚,答辩也容易讲。

3. 处理二手房价数据集:缺失值、异常值与特征工程

3.1 先认清数据集里有哪些字段

二手房价数据集的字段一般包含小区名称、区域、户型、面积、朝向、楼层、建造年份、总价、单价、关注度、发布时间等。拿到数据第一件事不是急着建模,而是跑一遍 df.info() 和 df.head(),看清每列类型和缺失情况,别被“房价分析”四个字带偏。

常见字段和用途对照如下:

字段名类型用途
area数值面积,核心特征
total_price数值总价,预测目标
unit_price数值单价,可作衍生特征
build_year数值建造年份,用于计算房龄
orientation文本朝向,需转数值
floor文本楼层,可分组
district文本区域,可用于分组对比

如果数据里没有 unit_price,可以用 total_price / area 自己算出来。房价分析里面积和总价是最基本的两个字段,缺了任何一个模型都无从谈起。size 特别小的数据集比如只有几百行,也能做完整的课程设计演示,但训练出来的模型预测精度有限,答辩时不要把它说成“精确预测”。

3.2 缺失值与异常值清洗

房价数据的缺失和异常是家常便饭。面积出现 5 平米或者 500 平米的记录,不一定是真实房源,很可能是录入错误;总价低到离谱的,可能是车位或者地下室混进来了。处理逻辑要写进一个可复用的函数,后续每次启动都走同一套清洗流程。

import pandas as pd def clean_house_data(df: pd.DataFrame) -> pd.DataFrame: # 删除完全没有分析价值的列,比如无意义的自增 id df = df.drop(columns=["id", "url"], errors="ignore") # 面积缺失用中位数填充;中位数比均值抗离群值 if "area" in df.columns: df["area"] = df["area"].fillna(df["area"].median()) # 楼层缺失不适合填数字,单独标记为“未知” if "floor" in df.columns: df["floor"] = df["floor"].fillna("未知") # 计算单价,剔除单价异常区间的记录 # 单价单位是元/平米,这里按 3000 到 120000 过滤 df["unit_price"] = df["total_price"] / df["area"] df = df[(df["unit_price"] > 3000) & (df["unit_price"] < 120000)] # 面积异常区间,小于 20 平米或大于 300 平米的记录剔除 df = df[(df["area"] >= 20) & (df["area"] <= 300)] # 重置索引,避免删除后出现空洞 df = df.reset_index(drop=True) return df

这里的几个参数要注意:fillna(df["area"].median()) 用中位数而不是均值,因为房价数据离群值多,均值容易被极端值拉偏;unit_price 的上下界 3000 到 120000 不是拍脑袋,而是参考主流二手房源价格带,如果你的数据集来自豪宅或特定城市,需要先打印 describe() 再调区间。reset_index(drop=True) 是为了清洗后再训练时不带原始行号,否则 train_test_split 会出现几千个空索引位,日志看着很难受。

清洗完记得打印 df.shape 和 df.isnull().sum(),确认删了多少行、还缺哪些值。这一步是后面所有工作的基石,清洗逻辑讲不清楚,答辩时数据环节就会被打穿。

3.3 特征筛选与相关性分析

二手房价预测的特征不需要太多,常见有效的是面积、房龄、朝向、户型、所在楼层段。特征太多反而稀释模型,课程设计的数据集本来就不大,五个以内的数值特征已经足够。朝向是文本,得先映射成数值。

# 朝向映射:优先处理主流朝向,其他归为 0 orientation_map = {"南": 1, "南北": 2, "东": 3, "西": 4, "东南": 5, "西南": 6} df["orientation"] = df["orientation"].map(orientation_map) df["orientation"] = df["orientation"].fillna(0) # 未命中的朝向统一为 0 # 房龄:用当前年份减去建造年份,避免直接用年份导致模型学到无关的时间趋势 df["house_age"] = 2024 - df["build_year"] # 户型拆成卧室数和客厅数,卧室数更影响价格 df["bedrooms"] = df["bedrooms"].astype(int) # 相关系数矩阵,打印后可以判断哪些特征和总价相关 numeric_cols = ["area", "house_age", "orientation", "bedrooms", "total_price"] print(df[numeric_cols].corr())

朝向映射成数字的规则不是唯一的,关键是保持训练和预测时用同一张映射表。如果你的数据里朝向更多样,把映射表补全即可,顺序不影响模型,只要全部朝向都映射到数字,不要留下 NaN。house_age 这一列很有价值,直接用 build_year 会导致模型学到“年份越大越贵”的假象,因为新房和旧房的价格差异本质上体现在房龄上。

相关系数矩阵建议打印到控制台看,而不只是画图。数值上如果 area 和 total_price 的相关系数在 0.5 以上,说明面积是主导特征;house_age 与总价负相关也正常,老房子通常总价更低。这一步也能提前发现两个特征高度相关的情况,比如面积和总价相关 0.9 会导致多重共线性,线性回归解释起来会有问题。

4. 用 sklearn 训练价格预测模型并保存成可调用接口

4.1 期末作业场景下哪种模型性价比最高

二手房价预测在课程设计里最常见的是线性回归和随机森林。线性回归优点是解释性强,系数能直接讲“面积每增加一平米,价格大约增加多少”,答辩时特别加分;缺点是它对特征和价格的非线性关系拟合有限,遇到明显非线性数据会欠拟合。

随机森林的预测精度通常比线性回归高,尤其数据里有朝向、楼层这类非线性特征时。它的问题是不好解释,特征重要性也只能给个大概方向。我的建议是:两个模型都训练,用测试集对比 R²,哪个高就把它作为默认模型,另外在界面上留一个模型切换下拉框。

不要一上来就上 XGBoost、LightGBM。课程设计的数据集一般只有几千条,几十棵树已经足够,树模型反而要调参且打包后体积更大。xgboost 安装还会给你带来额外的环境问题,这属于典型的“给自己找麻烦”。sklearn 自带的 RandomForestRegressor 对大家来说更熟,答辩老师也更认可。

4.2 训练、评估、保存模型

训练过程要固定 random_state,保证每次跑出来的结果一样。你不希望演示前换一个随机种子,预测准确率就变了,解释不清。下面这段代码把训练、评估、保存串在一起,训练完自动输出两个指标。

import joblib from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import r2_score, mean_absolute_error # 特征列顺序必须固定,后续预测接口按同一顺序输入 feature_cols = ["area", "house_age", "orientation", "bedrooms"] X = df[feature_cols] y = df["total_price"] # test_size=0.2 表示留 20% 数据做测试;random_state=42 固定划分 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = RandomForestRegressor( n_estimators=200, # 树的数量,200 在这个数据规模下够用 max_depth=10, # 限制单棵树深度,防止过拟合 random_state=42 # 固定随机种子,保证结果可复现 ) model.fit(X_train, y_train) y_pred = model.predict(X_test) print("R2:", round(r2_score(y_test, y_pred), 4)) print("MAE:", round(mean_absolute_error(y_test, y_pred), 2)) # 保存模型,PyQt5 界面启动时直接加载,避免每回都重新训练 joblib.dump(model, "models/house_model.pkl")

train_test_split 的 test_size 和 random_state 是这里最值得讲的两个参数:test_size 越大,测试集越能代表整体但训练数据越少,0.2 是常见折中;random_state 的作用是让数据划分可复现,答辩时连续跑三次结果一致,这一点给老师的印象分很高。n_estimators 和 max_depth 是随机森林的两个核心超参,200 棵树和深度 10 在几千条数据的场景下已经够用,树太多只会拖慢训练和打包体积。

R² 和 MAE 分别看拟合优度和误差绝对值。R² 在 0.5 到 0.8 之间是这类数据集的正常水平,不必追求 0.9 以上——房价本身受很多未采集因素影响,硬把树加深只会换成过拟合。这里的评估指标要由数据规模决定,而不是拍脑袋追高分。

4.3 封装预测接口,供 PyQt5 界面调用

模型训练好后,界面不能直接操作 joblib 加载的裸模型。原因有两个:特征输入顺序容易记错;模型文件的路径在界面层是硬编码。封装一个 HousePredictor 类,把“加载模型”和“预测”两件事包起来,界面只管调 predict 方法。

import joblib class HousePredictor: def __init__(self, model_path="models/house_model.pkl"): # 加载模型文件,路径缺失时抛出明确错误 self.model = joblib.load(model_path) # 特征顺序和训练时保持一致 self.feature_cols = ["area", "house_age", "orientation", "bedrooms"] def predict(self, area: float, house_age: float, orientation: int, bedrooms: int) -> float: # 输入顺序必须和 feature_cols 一致 features = [[area, house_age, orientation, bedrooms]] result = self.model.predict(features)[0] # 保留两位小数返回,避免界面上显示一长串浮点 return round(float(result), 2)

这个类的接口设计有讲究:predict 接收四个普通参数而不是一个列表,调用方不用关心特征顺序;返回值做了 round,避免界面上显示 243.5684231 这种毫无美感的数字。model_path 用相对路径,配合打包时把 models 目录一起带上,就能在 exe 环境里正常工作。

如果演示时需要切换线性回归和随机森林,可以在init里加一个参数,或者提供 set_model(path) 方法。这属于进阶功能,放在最后一章细说。界面代码里调用就两行:两行代码调用十行封装,这种取舍在项目里永远值得。

5. PyQt5 界面集成与打包避坑:4 个高频翻车现场

5.1 matplotlib 嵌入后界面卡死

现象:在 PyQt5 窗口里用 FigureCanvasQTAgg 嵌入 matplotlib,点按钮画图时窗口直接卡住,拖动窗口也卡顿。

原因:matplotlib 默认后端是 TkAgg,在 Qt 环境里会冲突,或者你在绘图函数里用了 plt.show() 阻塞了事件循环。PyQt5 的事件循环和 matplotlib 的后端是两个线程在抢控制权。

解决:先明确声明使用 Qt 后端,代码开头加上import matplotlib; matplotlib.use("Qt5Agg"),然后所有图表都画在 FigureCanvasQTAgg 上,不调用 plt.show()。如果图特别多,把绘图逻辑放到单独线程里,或者至少用 canvas.draw_idle() 替代 draw()。我一般是这样封装:

import matplotlib matplotlib.use("Qt5Agg") # 必须在导入 pyplot 之前设置 from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from matplotlib.figure import Figure class PriceChart(FigureCanvasQTAgg): def __init__(self, parent=None, width=8, height=4, dpi=100): self.fig = Figure(figsize=(width, height), dpi=dpi) super().__init__(self.fig) self.setParent(parent) self.ax = self.fig.add_subplot(111) def plot_area_price(self, data): # 每次绘图前清空坐标轴,避免重复叠加 self.ax.clear() self.ax.scatter(data["area"], data["total_price"], s=8) self.ax.set_xlabel("面积") self.ax.set_ylabel("总价") self.fig.tight_layout() self.draw_idle() # 异步刷新,不阻塞界面

图例里最关键的两行是 matplotlib.use("Qt5Agg") 和 draw_idle()。前者告诉 matplotlib 用 Qt 后端,后者告诉画布“有更新了请尽快刷新”但不强制马上绘制,界面不会等绘图阻塞。清空坐标轴是为了防止多按几次按钮后图上叠着一堆点。

5.2 pip 环境冲突:装别的库把 PyQt5 搞坏了

现象:本来界面能跑,为了装 labelme 或其他工具,pip 自动升级或降级了 PyQt5 的版本,重新跑程序报一堆找不到模块的错误。

原因:pip 在装依赖的时候会做版本解析,如果新库要求的 PyQt5 版本和已有版本冲突,pip 可能直接替换掉。labelme 这类工具对 PyQt5 的版本要求很挑剔,是典型的冲突源。

解决:项目用虚拟环境隔离,不要往全局 Python 里乱装。创建环境后,先把核心依赖一次装齐:pip install PyQt5 pandas matplotlib scikit-learn joblib,之后非必要不再执行可能改动 PyQt5 的 pip 操作。实验室机器或者答辩机器上没有虚拟环境时,用 pip freeze > requirements.txt 导出一份依赖清单,换机器之后pip install -r requirements.txt一次复原。

注意:如果已经把 PyQt5 装坏了,先pip uninstall PyQt5 PyQt5-sip再重装,而不是继续 pip install PyQt5 往上叠。sip 版本和 PyQt5 版本不匹配是常见的“装完还是坏”的原因,两个包要一并对齐。我在这上面翻过车,后来凡是课程设计项目,一律先建虚拟环境再动 pip。

5.3 PyInstaller 打包后图标、数据集和模型文件丢失

现象:源码里运行一切正常,打包成 exe 后双击能打开,但一加载数据集就报文件找不到,或者模型加载失败。

原因:PyInstaller 打包时只把 .py 文件收集进去,data、models、ui 目录里的 pkl 和 csv 不会自动包含。运行时的工作目录也不是 exe 所在目录,用 sys.path[0] 拿路径在打包后会指向临时解压目录。

解决:先明确告诉 PyInstaller 要收集哪些非代码文件,其次在代码里用可执行文件目录来拼路径。一个可靠做法是:

import sys, os def resource_path(relative): # 打包后用 _MEIPASS 指向临时解压目录,源码运行用项目目录 base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) return os.path.join(base, relative)

然后把所有 data/models 引用都改成 resource_path("models/house_model.pkl")。打包命令加 --add-data 参数:

pyinstaller -F -w main.py \ --add-data "models;models" \ --add-data "data;data" \ --name house_price_gui

参数说明:-F 打成一个单文件,-w 不显示黑色控制台窗口,--add-data 把 models 和 data 目录原样带进包。注意 Windows 下 add-data 的源路径和目标路径用分号分隔,Linux 用冒号。打包后先在当前目录双击测一遍,再把 exe 移到桌面测一遍,路径问题一般移动位置就暴露了。

5.4 界面中文乱码或模型预测结果对不上

现象:PyQt5 界面上中文显示成方块,或者模型预测的结果和训练时对同一输入的结果不一致。

原因:中文乱码多半是系统字体缺失或 CSV 编码不是 utf-8;预测结果不一致,通常是你把朝向、房龄的输入顺序或映射方式搞错了,模型按训练时的特征顺序读数据,预测时却换了一个顺序。

解决:CSV 读取时显式指定 encoding="utf-8" 或 encoding="gbk",读取后立刻打印前五行确认中文没有乱。界面字体统一设置:

from PyQt5.QtGui import QFont app.setFont(QFont("Microsoft YaHei", 10))

预测对不上时,先在训练脚本里打印 df[feature_cols].head() 和模型预测的样本值,再和界面输入的极限值对比。最常见的就是朝向映射表不一致,训练时南北映射为 2,界面下拉框里南北却传成 3。解决方案是把 feature 的映射表统一放成一个公共常量文件,train 和 UI 都引用同一个字典,不要在两侧各写一份。

6. 把验证和调参做进界面:交叉验证与模型体检

6.1 用交叉验证给出更可信的 R²

单次 train_test_split 的结果受随机划分影响很大,演示时最好在训练结束后顺便跑一次 5 折交叉验证,把均值和标准差显示在界面上。老师问“你的模型稳定吗”,你直接用数字回答,比口头解释更有说服力。

from sklearn.model_selection import cross_val_score cv_scores = cross_val_score(model, X, y, cv=5, scoring="r2") print("CV R2:", cv_scores.mean().round(4), "+/-", cv_scores.std().round(4))

6.2 把模型参数做成界面上的下拉框

进阶一点的做法,是在界面上加一个“模型设置”区域,用 QComboBox 切换随机森林和线性回归,用 QSpinBox 调 n_estimators。切换后重新训练并更新指标标签,这样演示时就能现场展示“参数对结果的影响”,比静态截图生动得多。调参范围不用给太大,我一般限定在 50 到 500 之间,否则点一次训练等十几秒,现场气氛反而尴尬。

我现在的习惯是:不管多简单的课程设计,拿到项目先跑通数据清洗和基础统计,再谈界面和模型。因为数据一旦有脏值,后面所有结论都可能推翻;界面做得再好看,也只是把错误放大展示。这套基于 Python 和 PyQt5 的二手房价分析与预测系统,真正值钱的部分是把一条完整的数据分析链路串起来——从清洗到训练,再到用户填几个数字就能看到预测结果。

拿到源码和数据集以后,先不要急着跑 main.py,对照这篇的目录结构逐行读一遍 core 层代码。读懂了再动手改模型或界面,你的答辩状态会完全不一样。希望帮到你。

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

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

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

立即咨询