简介:这份资源是面向计算机相关专业学生与项目实战学习者的二手房数据分析预测系统完整项目包,可直接用于课程设计、期末大作业或简历项目练习。项目围绕二手房数据展开,涵盖数据采集与清洗、特征工程、可视化分析到价格预测建模的完整流程,代码经过严格调试,下载即用,能帮助读者快速理解数据分析项目的落地思路。压缩包共156个文件,约40.03MB,包含18个py源码文件、18个csv数据集、15个html页面、11个js脚本,以及65张png图表、8个txt说明与pptx汇报文档等,覆盖源码、数据、前端展示与报告材料。目前已有831人学习下载。读者可据此获得一套可直接运行的完整项目方案,参考数据清洗与建模代码、可视化页面结构及文档报告写法,适合作为课程设计模板或数据分析入门实战练习。
1. 从一份二手房数据到能上线的预测系统:这套 Python 项目流程到底解决什么问题
二手房数据分析预测系统,说白了就是把「爬下来的房源表」变成「能对一套没见过的房子报价」的完整链路。很多人手里有 CSV,也会pd.read_csv,但一到「预测系统」四个字就卡住:模型训完怎么存、新数据怎么进、前端怎么调、报告怎么出。这套 Python 项目流程实战要解决的,正是从原始房源表到可交互预测页面的全流程落地,而不是停在 notebook 里跑个LinearRegression就完事。它适合两类人:一类是刚学完 Python 基础、想找一个能写进简历的完整项目练手的新手;另一类是做数据相关岗位、需要快速搭一套房价预估原型验证业务假设的从业者。核心链路只有五步——数据采集与清洗、特征工程、模型训练与调优、预测服务封装、可视化与报告输出,每一步都有能直接抄的代码和参数。下面按真实落地顺序拆开讲,中间会重点说清楚哪些参数必须调、哪些坑我踩过。
2. 数据采集与清洗:把脏房源表变成能喂给模型的干净数据
2.1 二手房数据从哪来、字段怎么定
做预测系统,第一步不是写模型,是确定你要预测什么。二手房的核心预测目标通常是「总价」或「单价」,我一般选总价,因为挂牌信息里总价最完整,单价可以反推。采集渠道常见做法是公开房源平台的列表页加详情页,用requests配合BeautifulSoup或直接解析页面里的 JSON 接口。这里不展开具体站点,重点说字段设计。一张能用的二手房原始表至少要有这些列:小区名称、区域、户型、建筑面积、所在楼层、总楼层、朝向、装修情况、建成年代、挂牌总价、挂牌时间。字段少了模型学不到东西,字段多了清洗成本爆炸。
采集阶段最容易翻车的是反爬和字段缺失。我的血泪经验是:不要一上来就全量抓,先抓 200 条跑通清洗和建模,确认字段可用再放量。采集代码里必须加超时和重试,否则一个请求卡住整个脚本就挂在那。
import requests import time import random HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } def fetch_page(url, retry=3): for i in range(retry): try: resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code == 200: return resp.text except requests.RequestException as e: print(f"第{i+1}次请求失败: {e}") time.sleep(random.uniform(1, 3)) # 随机间隔,降低被封风险 return None这段代码的关键参数是timeout=10和retry=3。超时太短会误杀正常请求,太长会拖慢整体采集;重试三次是经验值,超过三次基本说明目标站点已经限制你,继续重试没意义。random.uniform(1, 3)是请求间隔,固定间隔比随机间隔更容易被识别,这个细节新手经常忽略。
2.2 清洗的四个必做动作和参数
拿到原始数据后,清洗比采集更耗时间。我一般按四步走:去重、缺失值处理、异常值处理、类型转换。去重按「小区名称 + 户型 + 建筑面积」组合判断,因为同一套房可能被多次挂牌。缺失值方面,朝向和装修缺失可以用众数填充,建成年代缺失如果比例超过 30% 建议直接删列,硬填会引入噪声。异常值重点看建筑面积和总价,建筑面积小于 10 平米或大于 1000 平米的直接剔除,总价用分位数法处理。
import pandas as pd import numpy as np df = pd.read_csv("ershoufang_raw.csv") # 去重 df = df.drop_duplicates(subset=["小区名称", "户型", "建筑面积"]) # 建筑面积异常值剔除 df = df[(df["建筑面积"] >= 10) & (df["建筑面积"] <= 1000)] # 总价按 1% 和 99% 分位截断 low, high = df["总价"].quantile([0.01, 0.99]) df = df[(df["总价"] >= low) & (df["总价"] <= high)] # 缺失值:朝向、装修用众数填充 df["朝向"] = df["朝向"].fillna(df["朝向"].mode()[0]) df["装修"] = df["装修"].fillna(df["装修"].mode()[0]) # 建成年代缺失超 30% 则删列 if df["建成年代"].isnull().mean() > 0.3: df = df.drop(columns=["建成年代"]) else: df["建成年代"] = df["建成年代"].fillna(df["建成年代"].median()) df.to_csv("ershoufang_clean.csv", index=False)分位数截断用 1% 和 99% 而不是 5% 和 95%,是因为房价数据长尾明显,截太狠会丢掉真实的高价房源。众数填充适合类别型字段,数值型字段用中位数更稳,因为中位数不受极端值影响。清洗完一定要df.info()看一眼每列的非空数量和类型,这一步能提前发现后面建模时的类型报错。
3. 特征工程:把小区名、户型、朝向变成模型能读的数字
3.1 类别特征编码:独热编码和目标编码怎么选
模型不认识「朝南」「精装」这种文字,必须转成数字。低基数类别特征,比如朝向只有几个取值,直接用独热编码pd.get_dummies。高基数特征,比如小区名称可能有几百个,独热编码会让特征维度爆炸,这时候用目标编码,也就是用该小区对应的房价均值来替换小区名。目标编码必须做交叉验证式的计算,否则会数据泄漏,这是新手最容易踩的坑。
# 低基数:独热编码 df = pd.get_dummies(df, columns=["朝向", "装修"], drop_first=True) # 高基数:目标编码(带平滑) def target_encode(df, col, target, min_samples=10, smoothing=10): agg = df.groupby(col)[target].agg(["mean", "count"]) prior = df[target].mean() # 平滑:样本少的小区向全局均值靠拢 smooth = (agg["mean"] * agg["count"] + prior * smoothing) / (agg["count"] + smoothing) return df[col].map(smooth) df["小区_编码"] = target_encode(df, "小区名称", "总价") df = df.drop(columns=["小区名称"])平滑参数smoothing=10的作用是防止只有一两套房的小区编码值过于极端。min_samples这里没直接用,但思路是样本太少的小区应该被归到「其他」。目标编码一定要在训练集上算,然后映射到测试集,不能全量算完再切分,否则测试集信息泄漏进训练,模型评估会虚高。
3.2 数值特征衍生:面积、房龄、楼层比的组合
原始数值特征往往不够用,需要衍生。二手房场景里我必做的三个衍生特征:房龄(当前年份减建成年代)、楼层比(所在楼层除以总楼层)、厅室数量(从户型字符串里拆)。房龄比建成年代更直接,因为房价和房龄的关系更线性。楼层比能反映采光和视野,低楼层和高楼层价格差异明显。
df["房龄"] = 2024 - df["建成年代"] df["楼层比"] = df["所在楼层"] / df["总楼层"] # 从"3室2厅"拆出室和厅 df["室数"] = df["户型"].str.extract(r"(\d+)室").astype(float) df["厅数"] = df["户型"].str.extract(r"(\d+)厅").astype(float) df = df.drop(columns=["户型", "建成年代", "所在楼层", "总楼层"])str.extract里的正则(\d+)室表示提取「数字+室」里的数字部分。如果户型格式不统一,比如有「3室2厅」也有「三室两厅」,正则就失效了,清洗阶段必须先把中文数字转成阿拉伯数字。衍生完特征记得把原始列删掉,避免多重共线性,尤其是房龄和建成年代这种高度相关的列同时保留会让线性模型系数不稳定。
4. 模型训练与调优:从线性回归到梯度提升的选型与参数
4.1 基线模型先跑通,再谈调优
很多人一上来就上 XGBoost,结果数据没处理好,调参调到怀疑人生。我的习惯是先跑一个线性回归当基线,看 R² 和 MAE 大概在什么水平,再换树模型对比。线性回归的好处是可解释性强,能看出哪些特征对房价影响大。如果线性回归 R² 已经到 0.8 以上,说明特征工程做得不错,换树模型提升空间有限。
from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, r2_score X = df.drop(columns=["总价"]) y = df["总价"] 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) pred = lr.predict(X_test) print("MAE:", mean_absolute_error(y_test, pred)) print("R2:", r2_score(y_test, pred))test_size=0.2是常规切分比例,数据量小于 1000 条时建议用 0.3,保证测试集有足够样本。random_state=42固定随机种子,保证每次切分结果一致,方便对比不同模型。MAE 的单位是万元,比 R² 更直观,业务方更容易理解「平均预测误差多少万」。
4.2 梯度提升树的关键参数怎么设
基线跑完换梯度提升树,常用的是sklearn的GradientBoostingRegressor或XGBoost。核心参数就四个:n_estimators、learning_rate、max_depth、subsample。我的经验配置是n_estimators=300、learning_rate=0.05、max_depth=4、subsample=0.8。学习率低一点、树多一点,泛化更好但训练慢;深度控制在 3 到 5 之间,太深容易过拟合。
from sklearn.ensemble import GradientBoostingRegressor gbr = GradientBoostingRegressor( n_estimators=300, learning_rate=0.05, max_depth=4, subsample=0.8, random_state=42 ) gbr.fit(X_train, y_train) pred_gbr = gbr.predict(X_test) print("GBR MAE:", mean_absolute_error(y_test, pred_gbr)) print("GBR R2:", r2_score(y_test, pred_gbr))subsample=0.8表示每棵树只用 80% 的样本训练,这是随机梯度提升的做法,能显著降低过拟合。如果 MAE 比线性回归还差,先别调参,回去检查特征里有没有泄漏目标信息的列,比如「单价」这种由总价算出来的字段必须删掉。调参用GridSearchCV时,参数网格不要一次铺太大,先粗调n_estimators和max_depth,再细调learning_rate。
5. 避坑与排查:二手房预测系统落地时最容易翻车的五件事
5.1 数据泄漏:单价字段没删导致 R² 虚高到 0.99
现象是模型评估 R² 接近 1,MAE 小得离谱,但一上真实新数据预测就完全不准。原因是特征里混进了「单价」或者「总价/面积」这类由目标直接算出来的列。解决方法是建模前逐列检查,凡是和目标有确定性计算关系的字段一律删掉,X = df.drop(columns=["总价", "单价"])。
5.2 目标编码泄漏:全量算完再切分训练测试集
现象是训练集和测试集表现都很好,但交叉验证分数波动极大。原因是目标编码用了全量数据的均值,测试集信息提前进了训练。解决方法是先切分训练测试集,目标编码只在训练集上计算映射关系,再应用到测试集,或者用 K 折目标编码。
5.3 类别特征没对齐:训练时有 200 个小区,预测时来了新小区
现象是线上预测时报错「特征维度不匹配」或新小区编码为 NaN。原因是独热编码或目标编码的类别集合在训练和预测时不一致。解决方法是把训练时的编码映射表用joblib存下来,预测时用同一张表做映射,遇到没见过的类别统一归到「其他」编码。
5.4 异常值没处理干净:一套 5000 万的房源把模型带偏
现象是模型对普通房源预测偏高。原因是训练数据里混进了极端高价房源,线性模型对异常值特别敏感。解决方法是在清洗阶段用分位数截断,或者改用对异常值不敏感的树模型,同时检查高价房源是否真实。
5.5 模型文件没版本管理:换了特征忘了重新训练
现象是代码更新了特征工程逻辑,但线上用的还是旧模型文件,预测结果和预期对不上。解决方法是用joblib.dump保存模型时带上时间戳和特征列表,加载模型时校验特征数量和顺序,不一致直接报错而不是静默预测。
6. 预测服务封装与报告输出:让模型真正能被调用
6.1 用 joblib 保存模型和编码映射表
模型训完不能只留在内存里,必须持久化。除了模型本身,特征列顺序、目标编码映射表、独热编码的列名都要一起存,否则预测时对不上。我一般用一个字典打包所有产物,统一joblib.dump。
import joblib artifacts = { "model": gbr, "feature_columns": list(X_train.columns), "target_encode_map": df.groupby("小区名称")["总价"].mean().to_dict(), "train_time": "2024-01-01" } joblib.dump(artifacts, "house_price_model.pkl")feature_columns存下来是为了预测时按同样顺序排布特征,target_encode_map存下来是为了新数据里的小区能映射到编码值。加载时先校验feature_columns和当前输入列是否一致,不一致就抛异常,别让模型硬跑。
6.2 用 Flask 包一个最小预测接口
预测系统最终要能被前端或其他服务调用,最轻量的做法是 Flask 起一个 POST 接口,接收 JSON 格式的房源信息,返回预测总价。
from flask import Flask, request, jsonify import joblib import pandas as pd app = Flask(__name__) artifacts = joblib.load("house_price_model.pkl") @app.route("/predict", methods=["POST"]) def predict(): data = request.get_json() row = pd.DataFrame([data]) # 按训练时的特征列对齐,缺失列填 0 row = row.reindex(columns=artifacts["feature_columns"], fill_value=0) pred = artifacts["model"].predict(row)[0] return jsonify({"predicted_price": round(float(pred), 2)}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)reindex这一步是接口稳定的关键,前端传过来的字段可能少几个,用fill_value=0补齐,保证特征维度一致。返回时round(float(pred), 2)把 numpy 类型转成 Python 原生 float,否则jsonify会报序列化错误,这个坑我踩过不止一次。
6.3 报告输出:把评估指标和特征重要性写进文档
项目交付通常要一份报告,内容包括数据概况、清洗前后对比、模型评估指标、特征重要性排序。特征重要性直接从树模型取feature_importances_,和列名配对排序输出。
import pandas as pd importance = pd.DataFrame({ "feature": X_train.columns, "importance": gbr.feature_importances_ }).sort_values("importance", ascending=False) importance.to_csv("feature_importance.csv", index=False) print(importance.head(10))报告里重点写清楚 MAE 是多少万、R² 是多少、哪些特征影响最大。如果「建筑面积」和「小区_编码」排在前两位,说明模型学到的规律和业务直觉一致,这份报告才有说服力。特征重要性还能反过来指导数据采集,重要性低的字段下次可以不抓,节省采集成本。
最后说一个我自己的习惯:每次做完一个预测系统,我都会留一份「预测偏差最大的 20 条样本」清单,逐条看是数据问题还是模型问题。这个动作比调参更能提升系统可用性,因为真实业务里最怕的不是平均误差大,而是某些类型的房源系统性预测错。希望帮到你。
本文还有配套的精品资源,点击获取