简介:面向计算机相关专业学生与数据分析初学者的Python二手房数据分析项目资料,整套资源针对课程大作业、毕业设计及实战练习场景设计,覆盖数据采集、清洗、可视化与建模分析全流程。包内共有114个文件,压缩包约29.31MB,主要包含Python脚本(py)实现核心分析逻辑,多份csv格式的原始与清洗后数据集,png可视化结果图,html/js构成的交互展示页面,以及txt/md说明文档和pptx讲解PPT,便于对照代码、文档和演示文稿系统学习。项目已通过导师审核并获98分高分评价,源码经本地调试可运行,配套解析文档对每个步骤的目的、实现方法与常见问题解决方案做了详细说明,讲解PPT则以图文并茂方式梳理数据分析整体思路,帮助学习者从数据读取、特征处理到可视化输出逐步掌握完整项目流程。目前已有42人学习使用,适合具备一定Python基础的学生作为期末大作业或毕业设计的可靠参考,也可用于提升真实数据场景下的实战能力。
1. 二手房数据分析项目:从数据到答辩的一次完整闭环
课程设计或毕业设计里,Python 数据分析永远是最稳妥的选题,但能拿到高分的并不多。多数人卡在同一个地方:代码能跑、图能出,答辩时却说不清每一步为什么这么做。二手房分析恰好是数据语义清晰、又带真实业务味道的场景——总价、单价、面积、户型、朝向、区域,每一列都对应普通人买房的一个决策点。这篇文章按一条可复现的流程走:数据获取与清洗、探索性可视化、特征工程与回归建模,最后落到 PPT 讲解如何把这四步串成让老师挑不出毛病的叙事线。适合正在做课程设计、毕业设计,或想用一份完整 python 数据分析项目练手的人,pandas、seaborn、scikit-learn 三个库足够。
2. 数据准备:房源字段设计与 pandas 清洗的完整代码
2.1 数据来源两条路线:爬虫采集与本地数据集
常见做法是爬取链家或贝壳的公开挂牌页,用 requests 加 BeautifulSoup 就能拿到。但写爬虫在答辩里只是"获取数据的手段",分数权重很低,真正拉开差距的是拿到数据之后做了什么。如果你的目标是把项目跑完整,直接用现成的二手房 CSV 数据集更省时间,网上很多免费 python 源码大全里都有类似的北京、上海房源数据。我一般建议数据集大于 3000 条、字段不少于 8 列就够用,重点是必须有总价、单价、面积、区域、户型、朝向这几项。
下面是一份典型的字段口径表,做爬虫解析时直接按这个对齐,能省掉大量后续返工:
| 字段名 | 原始格式示例 | 清洗后类型 | 说明 |
|---|---|---|---|
| region | 朝阳区 | str | 挂牌所在行政区,做分组对比的主键 |
| layout | 3室1厅 | str | 户型,建模时拆出室和厅 |
| size | 89.5平米 | float | 建筑面积,单位统一为平米 |
| floor | 低楼层/共18层 | str | 楼层区间加总层数,可拆出两层信息 |
| toward | 南北 | str | 朝向,低频值合并为"其他" |
| decoration | 精装 | str | 装修程度,存在缺失 |
| total_price | 620万 | float | 挂牌总价,单位统一为万元 |
| unit_price | 69273元/平 | float | 挂牌单价,建模的核心目标 |
写爬虫时最典型的坑是"低楼层/共18层"这类复合字段,直接用正则(\d+)层把总层数提出来,低/中/高楼层单独映射成 0/1/2。单价字段里单位混着"元/平"和"万元/平",一旦混进来,后面建模的 RMSE 会大得离谱。清洗前先打印df.dtypes确认每一列有没有被 pandas 解析成预期类型,这是最容易被跳过的调试步骤——字符串型数值列参与计算时,报错往往出在建模阶段,回头查数据要浪费一倍时间。
2.2 用 pandas 完成清洗的最小完整代码
import pandas as pd import numpy as np import re df = pd.read_csv("ershoufang.csv", encoding="utf-8-sig") print(df.shape, df.dtypes) # 先看形状和类型,确认列有没有解析错 # 1. 按关键列去重,同一套房源可能被抓了两遍 df = df.drop_duplicates(subset=["region", "size", "layout", "total_price"]) # 2. 去掉价格和面积为空的行,装修缺失保留 df = df.dropna(subset=["total_price", "unit_price", "size"]) # 3. 把"620万"变成数值,这里总价单位统一为万 def parse_price(x): if isinstance(x, str): num = re.search(r"([\d.]+)万", x) return float(num.group(1)) if num else np.nan return x df["total_price"] = df["total_price"].map(parse_price) # 4. 面积字段提取数字,并过滤掉异常区间 df["size"] = df["size"].str.replace("平米", "").astype(float) df = df[(df["size"] >= 30) & (df["size"] <= 300)] # 5. 单价单位统一,防止"元/平"和"万元/平"混用 def parse_unit(x): x = str(x) if "万" in x: return float(re.search(r"([\d.]+)", x).group(1)) * 10000 return float(re.search(r"([\d.]+)", x).group(1)) df["unit_price"] = df["unit_price"].map(parse_unit) # 6. 楼层字段拆分出楼层区间和总层数 floor_map = {"低": 0, "中": 1, "高": 2} df["floor_level"] = df["floor"].str.extract(r"([低中高])")[0].map(floor_map) df["total_floors"] = df["floor"].str.extract(r"共(\d+)层")[0].astype(float) # 7. 户型字段拆出室和厅两个数值 df["rooms"] = df["layout"].str.extract(r"(\d+)室")[0].astype(float) df["halls"] = df["layout"].str.extract(r"(\d+)厅")[0].astype(float) df = df.dropna(subset=["rooms", "floor_level"]) print(df.isnull().sum()) # 清洗完再核对一次缺失值 df.to_csv("ershoufang_clean.csv", index=False, encoding="utf-8-sig")这段代码的每一步都有对应的业务理由。drop_duplicates(subset=...)用四列联合判断重复,单用一列会误删面积相同但楼栋不同的房源。dropna(subset=...)只删关键列缺失的行,保留装修信息缺失但价格面积齐全的记录,后面建模时还能用。面积过滤在 30 到 300 平之间,是为了把车位、地下室和别墅这类极端值挡在门外,避免回归模型被个别样本带偏。楼层拆成floor_level和total_floors两个特征是有意的:floor_level是类别语义,total_floors是连续语义,模型可以自己决定用哪一个,而不是替它做死。文件导出用utf-8-sig而不是utf-8,这样在 Excel 里打开不会乱码,答辩现场展示数据时能少一个尴尬点。
2.3 清洗结果的验收标准和三个高频坑
清洗做没做干净,别用"看着还行"判断。我会打印每个数值列的describe(),重点看 min、max、mean 三个值是否符合常识:单价 min 低于 5000 或 max 高于 20 万,几乎可以断定单位没统一或混进了商铺数据。total_floors出现 0 或 99,说明正则没匹配上,需要回头检查原始字符串格式。另一个高频坑是 CSV 编码问题,爬虫存文件时用utf-8-sig,否则中文列名在 Excel 里打开是乱码。第三个坑是房源里混入车位和商铺,判断依据通常是面积小于 20 平或者单价异常低,这类记录在清洗阶段就该剔除,而不是留给模型去"学习"。
3. 探索性数据分析:让图表自己讲出房价规律
3.1 单价分布的可视化与偏态处理
数据清洗完先别急着建模,第一步永远是看单变量的分布形态。用 seaborn 画单价直方图,绝大多数城市的二手房单价都会呈现明显的右偏——低单价区间一堆房,高单价尾巴拖得很长。log 变换之后近似正态,这个观察在答辩里是加分项,说明你理解为什么后面建模要用 log1p 做目标变换。
import matplotlib.pyplot as plt import seaborn as sns plt.rcParams["font.sans-serif"] = ["SimHei"] # 解决中文乱码 plt.rcParams["axes.unicode_minus"] = False fig, axes = plt.subplots(1, 2, figsize=(12, 4)) sns.histplot(df["unit_price"], bins=50, kde=True, ax=axes[0]) axes[0].set_title("单价原始分布") df["unit_price_log"] = np.log1p(df["unit_price"]) sns.histplot(df["unit_price_log"], bins=50, kde=True, ax=axes[1]) axes[1].set_title("单价 log 变换后分布") plt.tight_layout() plt.savefig("price_dist.png", dpi=150)中文字体配置这段不能省,Windows 下 SimHei 通常可用,Mac 或 Linux 上要换成["PingFang SC"]或["WenQuanYi Zen Hei"],否则图上全是方块。np.log1p用1 + x避免价格为 0 时取对数得负无穷。subplots 左右对比的画法很适合答辩:放一张"变换前后对比",比放十张直方图有说服力。dpi=150是给 PPT 用的最低标准,低于这个值投影出来边缘发虚。
3.2 区域与户型的分组对比:用箱线图代替文字
fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 各区域单价箱线图,按中位数排序 region_order = (df.groupby("region")["unit_price"] .median().sort_values(ascending=False).index) sns.boxplot(data=df, x="region", y="unit_price", order=region_order, ax=axes[0]) axes[0].set_xticklabels(axes[0].get_xticklabels(), rotation=45) # 户型室数对比 sns.boxplot(data=df, x="rooms", y="unit_price", ax=axes[1]) plt.savefig("region_box.png", dpi=150)boxplot 的 order 参数按中位数排序,让图从左到右自然形成"贵到便宜"的视觉层级,比默认按字母排更容易让老师一眼看出结论。如果区域超过 15 个,我会先取中位数 top10 再画,避免横轴标签挤成一团。朝向字段不要直接画八个类,把东西、西北这些低频值合并成"其他",否则箱线图里会出现大量空箱。箱线图里的点和须要能讲清楚:盒子是中位数和四分位距,须以外的是离群房源,这部分可以自然引出"哪些因素让个别房源贵出这么多"的建模动机。
3.3 相关性矩阵:找到价格的真实驱动因子
corr_cols = ["unit_price", "total_price", "size", "rooms", "halls", "total_floors", "floor_level"] fig, ax = plt.subplots(figsize=(8, 6)) sns.heatmap(df[corr_cols].corr(), annot=True, fmt=".2f", cmap="RdYlBu_r", ax=ax) plt.savefig("corr_heatmap.png", dpi=150)注意结论的表述顺序。和单价相关性最高的往往是 size,但方向是负的——面积越大单价越低,这是总价约束下的一种结构性现象,而不是"房子越大越不值钱"。这个点拿出来讲,能明显区别于只会复述"相关性高/低"的作业。区域、朝向是类别变量,进不了这个相关矩阵,正确做法是回到箱线图,用组间中位数差去表述。相关性矩阵只放数值型字段,混入 one-hot 编码后的区域列会把图撑得没法看。fmt=".2f"让热力图里只显示两位小数,图面干净,投影出来也清晰。
4. 特征工程与回归建模:把结论变成可解释的数字
4.1 类别特征编码与数据划分
建模目标选 unit_price 而不是 total_price,理由很直接:总价 = 单价 × 面积,如果预测总价,模型只要学会乘面积就够了,单价才真正体现房屋各属性的权重。特征工程做四步:region 按 top10 之外合并为"其他"再做 one-hot;toward 低频合并后同样 one-hot;rooms 和 halls 保留数值;size 和 total_floors 标准化。
from sklearn.model_selection import train_test_split from sklearn.preprocessing import OneHotEncoder, StandardScaler # 低频区域合并,避免 one-hot 后维度爆炸 top_regions = df["region"].value_counts().head(10).index df["region2"] = df["region"].where(df["region"].isin(top_regions), "其他") num_cols = ["size", "total_floors", "floor_level", "rooms", "halls"] cat_cols = ["region2", "toward", "decoration"] # 数值列标准化 scaler = StandardScaler() X_num = pd.DataFrame(scaler.fit_transform(df[num_cols]), columns=num_cols, index=df.index) # 类别列 one-hot,drop_first 去掉各自的第一列 X_cat = pd.get_dummies(df[cat_cols], drop_first=True) X = pd.concat([X_num, X_cat], axis=1) y = df["unit_price_log"] # 用 log 后的单价做目标 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) print(X_train.shape, X_test.shape)drop_first=True去掉每个类别字段的第一列,避免哑变量共线性,这在线性回归的系数解读里尤其重要。区域 one-hot 之后字段数等于 top10 加"其他"再减一,也就是 10 列左右,维度完全可控。标准化只 fit 在训练集上,测试集用同一个 scaler 做 transform,这是防止数据泄露的基本要求——如果对全量数据 fit,测试集的信息就提前进了模型。随机种子固定成 42 是惯例,答辩被问到时可以说"固定随机种子是为了实验可复现",这个回答比"随便设的"体面得多。
4.2 线性回归与随机森林的对比训练
from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score models = { "LinearRegression": LinearRegression(), "RandomForest": RandomForestRegressor( n_estimators=300, max_depth=12, random_state=42, n_jobs=-1 ), } results = {} for name, model in models.items(): model.fit(X_train, y_train) pred_log = model.predict(X_test) # 还原到原始单价口径,RMSE 才具有"元/平"的物理意义 pred = np.expm1(pred_log) y_true = np.expm1(y_test) results[name] = { "MAE": mean_absolute_error(y_true, pred), "RMSE": np.sqrt(mean_squared_error(y_true, pred)), "R2": r2_score(y_true, pred), } print(name, results[name]) # 特征重要性只在树模型上有意义 rf = models["RandomForest"] importance = pd.Series(rf.feature_importances_, index=X.columns).sort_values(ascending=False) print(importance.head(10))评估顺序有个关键细节:模型在 log 空间训练,预测出来也是 log 值,必须np.expm1还原后再算 RMSE,单位才是"元/平"。直接在 log 空间算 RMSE,数值会小到零点几,看起来很好,但没法向老师解释物理意义。随机森林的n_estimators=300、max_depth=12是对几千条数据足够收敛又不至于过拟合的组合,n_jobs=-1让所有 CPU 核参与训练,数据量大时速度差异明显。线性回归不需要调参,如果它的 R² 异常高,先回头查是不是把 total_price 或 size 的倍数关系漏进特征里了,这是过拟合最常见的伪装。
4.3 模型效果解读的参数与表述
| 模型 | MAE(元/平) | RMSE(元/平) | R² |
|---|---|---|---|
| 线性回归 | 约 6200 | 约 9500 | 约 0.55 |
| 随机森林 | 约 5100 | 约 7800 | 约 0.68 |
表格里的数值是给你对照量级的参考,不同城市差异很大,别拿它当标准答案。结果的表述分三层:第一层说 MAE 的物理意义,"预测单价平均偏差 5000 元左右,在单价 5 万的市场里约 10% 的误差";第二层说随机森林比线性回归好,说明特征之间存在非线性关系和交互作用;第三层看特征重要性 top3,通常是 size、区域、floor_level,这个结论要和第 3 章的可视化对得上,形成闭环。特征重要性的排序比具体数值更值得讲,因为它是相对的。如果答辩老师追问"为什么 size 最重要",回看相关性矩阵里 size 与 unit_price 的负相关,用"总价约束下的面积折扣"来解释,逻辑就完整了。
5. PPT讲解:把代码过程改写成数据叙事
PPT 是拿分的关键环节。项目做得再完整,讲不清楚等于白做。固定做法是 PPT 只放 8 到 10 页,按"问题—数据—发现—模型—结论"五段走。第 1 页直接抛出问题:"挂牌价到底由什么决定?面积和区域谁的影响更大?" 第 2 页放字段口径表和清洗前后的行数对比,清洗前后行数变化必须截图而不是现场跑。第 3 到 5 页放第 3 章的直方图、箱线图和相关性热力图,每张图配一句话结论,字要少。第 6 页放模型对比表和特征重要性条形图,第 7 页收束到"单价由什么决定"的最终答案,呼应开头。
图表排版有个硬性经验:PPT 里文字不要小于 18 号,图不要小于半页。答辩现场老师坐最后一排,图缩在一角、字小到看不清是最常见的失分点。每页只讲一个结论,一页塞三张图等于三张都没讲。配色别用默认的蓝底白字,白底深色字投影效果最稳定。
答辩被问得最多的是三个问题,提前准备答案。第一,"数据从哪来的,覆盖什么时间段?"——如实回答来源,说明采集或下载时间,再补一句"这是挂牌价而非成交价,反映的是供给方定价逻辑"。第二,"为什么预测单价而不是总价?"——用总价等于单价乘面积一句话回答,强调单价才能体现房屋属性权重。第三,"模型误差能接受吗?"——不要辩解,承认误差存在,说明 MAE 在真实市场中的量级,再补一句"如果加入地铁距离、楼龄等特征,误差还会下降",这是给后续工作留口子。
还有一个容易被忽略的技巧:答辩前把关键数字记在本子上,比如"从 4200 行清洗到 3856 行""随机森林 max_depth=12""log 变换后的 RMSE 还原为 7800 元/平"。老师问细节时你能说出具体数字而不用翻代码,整个项目的可信度立刻不一样。同时把控制台输出和 error 信息截图准备两张备用,现场演示代码万一报错,直接切截图讲,别在台上调 bug,也不要在演示时现跑完整的训练流程,把时间留给结论和图表解释。
本文还有配套的精品资源,点击获取