简介这份资源是面向高校学生与Python初学者的二手房数据分析完整项目包适用于毕业设计、期末大作业与课程设计等场景可帮助读者快速完成从数据清洗到可视化呈现的全流程实践。包内共157个文件涵盖18个py源码、18个csv数据集、15个html页面、11个js脚本及65张png图表另附docx文档说明与pptx汇报资料压缩包约48.05MB目录结构清晰便于按模块查阅。项目代码注释详尽新手也能看懂部署简单下载后即可运行。数据集包含原始与清洗后的多版本CSV文件覆盖编码转换与数据预处理环节配合可视化页面可直观展示房价分布与影响因素。已有127人学习下载适合需要高分项目参考、希望掌握数据分析实战流程的读者。1. 从一份二手房数据到能讲清楚的结论这套 Python 分析项目到底解决什么问题二手房市场有个很反直觉的现象同一小区、同样面积挂牌价能差出 20% 以上而中介给出的理由往往是楼层不一样装修不一样。真去翻数据会发现楼层和装修对价格的影响远没有想象中大真正拉开差距的是房龄、学区、地铁距离和挂牌时间。这套基于 Python 的二手房数据分析项目要解决的就是把这种说不清变成算得清——用爬虫拿到真实挂牌数据用 pandas 做清洗和特征工程用可视化把价格分布、区域差异、影响因素摆到台面上最后配一份能直接讲给答辩老师或面试官听的文档和 PPT。适合三类人一是做课程设计或毕业设计的学生需要一个结构完整、能跑通、有文档有 PPT 的项目二是想转数据分析的从业者需要一个真实场景练手而不是拿鸢尾花数据集反复折腾三是做房产相关业务的人想快速搭一套自己的价格分析流程。整套东西的核心不是代码多复杂而是流程完整、结论能落地、文档能讲清楚每一步为什么这么做。2. 数据从哪来、怎么存二手房数据采集与落库的完整链路2.1 采集目标与字段设计做二手房分析第一步不是写爬虫而是想清楚要哪些字段。字段设计错了后面清洗和分析全是返工。我一般会先列一张目标表把分析需要什么和页面能拿到什么对齐。字段名类型来源用途titlestr列表页辅助判断房源类型total_pricefloat列表页总价分析unit_pricefloat列表页单价分析核心指标areafloat列表页计算单价、面积分布layoutstr列表页户型分析如3室1厅floorstr详情页楼层影响分析build_yearint详情页房龄计算districtstr列表页区域对比communitystr列表页小区聚合follow_countint列表页热度指标visit_countint详情页带看热度publish_daysint详情页挂牌时长这张表里unit_price是最关键的因为总价受面积影响太大直接比总价没有意义。publish_days是很多人会漏掉的字段但它对判断房东急不急卖非常有用后面分析里会专门讲。2.2 采集脚本与反爬处理采集部分用 requests BeautifulSoup 就够了不需要上 Scrapy因为数据量不大单机跑几个小时能拿到几千条。关键是控制频率和做好异常处理。import requests from bs4 import BeautifulSoup import pandas as pd import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch_page(url, retry3): 带重试的页面抓取失败返回 None for i in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: return resp.text # 状态码异常时退避重试 time.sleep(2 ** i) except requests.RequestException: time.sleep(2 ** i) return None def parse_list(html): 解析列表页返回房源字典列表 soup BeautifulSoup(html, html.parser) items [] for node in soup.select(.sellListContent li): try: title node.select_one(.title a).get_text(stripTrue) total node.select_one(.totalPrice span).get_text(stripTrue) unit node.select_one(.unitPrice span).get_text(stripTrue) items.append({ title: title, total_price: float(total), # 单价文本带元/平米需要去掉单位 unit_price: float(unit.replace(元/平米, ).replace(,, )), }) except (AttributeError, ValueError): # 单条解析失败不影响整体跳过 continue return items def crawl(pages50): all_data [] for p in range(1, pages 1): url fhttps://example.com/sale/pg{p}/ html fetch_page(url) if not html: print(fpage {p} failed, skip) continue all_data.extend(parse_list(html)) # 随机间隔避免请求过于规律 time.sleep(random.uniform(1.5, 3.5)) return pd.DataFrame(all_data) if __name__ __main__: df crawl(pages50) df.to_csv(raw_ershoufang.csv, indexFalse, encodingutf-8-sig) print(f共采集 {len(df)} 条)这段代码有三个关键点。第一fetch_page里的重试用了指数退避第 i 次失败等 2 的 i 次方秒避免连续快速重试被识别。第二parse_list里每条房源单独 try某一条字段缺失不会让整页数据丢掉这是血泪经验——早期版本一页里有一条异常整页白抓。第三time.sleep用随机值而不是固定值固定间隔的请求模式太规律容易被判定为脚本。参数上pages根据目标城市房源量调整一般 50 到 200 页能覆盖一个城市的主要挂牌。timeout设 10 秒太短容易误判超时太长卡住整个流程。encodingutf-8-sig是为了 Excel 打开 CSV 不乱码这个细节在交付文档时很重要。2.3 落库与增量更新采集完直接存 CSV 能用但做多次采集就会乱。常见做法是存 SQLite轻量、不用装数据库服务、Python 原生支持。import sqlite3 def save_to_db(df, db_pathershoufang.db): conn sqlite3.connect(db_path) # 建表时给 unit_price 和 district 建索引后面查询快很多 conn.execute( CREATE TABLE IF NOT EXISTS houses ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, total_price REAL, unit_price REAL, area REAL, layout TEXT, floor TEXT, build_year INTEGER, district TEXT, community TEXT, follow_count INTEGER, visit_count INTEGER, publish_days INTEGER, crawl_date TEXT ) ) conn.execute(CREATE INDEX IF NOT EXISTS idx_unit ON houses(unit_price)) conn.execute(CREATE INDEX IF NOT EXISTS idx_district ON houses(district)) df.to_sql(houses, conn, if_existsappend, indexFalse) conn.commit() conn.close()用append而不是replace是为了保留历史采集记录后面可以做价格随时间变化的分析。crawl_date字段在采集时补上用datetime.now().strftime(%Y-%m-%d)这样同一套房源不同时间点的挂牌价都能追溯。索引建在unit_price和district上因为后面分析里这两个字段的查询最频繁。3. 清洗与特征工程把脏数据变成能算的字段3.1 缺失值与异常值处理真实采集的数据一定脏。常见问题有三类面积缺失、单价异常比如 1 元/平米或 100 万/平米、楼层字段格式不统一低楼层和1/6层混在一起。import numpy as np import pandas as pd def clean(df): df df.copy() # 面积缺失用同户型中位数填充比全局均值合理 df[area] df.groupby(layout)[area].transform( lambda x: x.fillna(x.median()) ) # 单价用 1% 和 99% 分位数截断去掉极端异常 low, high df[unit_price].quantile([0.01, 0.99]) df df[(df[unit_price] low) (df[unit_price] high)] # 总价和单价对不上时以单价为准重算总价 df[total_price] df[unit_price] * df[area] / 10000 # 房龄 当前年份 - 建成年份 df[house_age] 2024 - df[build_year] df.loc[df[house_age] 0, house_age] np.nan return dfgroupby(layout)填充面积是有讲究的同样3室1厅面积中位数比全局均值更接近真实值。分位数截断用 1% 和 99% 而不是 3σ因为房价分布是右偏的用标准差会把正常的高价房也砍掉。总价重算这一步很多人不做但采集时总价和单价经常因为页面更新不同步而对不上以单价为准更可靠。3.2 从文本字段里挖特征layout、floor、title这些文本字段里藏着大量信息直接丢掉太浪费。常见做法是拆成结构化特征。import re def extract_features(df): df df.copy() # 户型拆成室和厅 df[rooms] df[layout].str.extract(r(\d)室).astype(float) df[halls] df[layout].str.extract(r(\d)厅).astype(float) # 楼层拆成楼层位置和总层数 df[floor_level] df[floor].str.extract(r(低|中|高)楼层) df[total_floors] df[floor].str.extract(r共(\d)层).astype(float) # 标题里出现地铁的标记为近地铁 df[near_subway] df[title].str.contains(地铁).astype(int) # 标题里出现精装简装的标记装修等级 df[decoration] df[title].str.extract(r(精装|简装|毛坯)) return dfrooms和halls用正则提取比字符串切割稳。near_subway用标题关键词判断是权宜之计准确率大概七成但比没有强如果详情页能拿到地铁距离优先用距离字段。decoration提取出来是分类变量后面做分析时要转成哑变量。3.3 特征相关性初筛特征做完不是全塞进模型先看相关性把冗余的去掉。import seaborn as sns import matplotlib.pyplot as plt def plot_corr(df, cols): corr df[cols].corr() plt.figure(figsize(10, 8)) sns.heatmap(corr, annotTrue, fmt.2f, cmapcoolwarm, center0) plt.title(特征相关性矩阵) plt.tight_layout() plt.savefig(corr.png, dpi150) return corr cols [unit_price, area, house_age, rooms, total_floors, near_subway] corr plot_corr(df, cols) print(corr[unit_price].sort_values(ascendingFalse))看相关性有两个目的一是确认哪些特征和单价强相关二是发现特征之间的共线性。比如area和rooms相关性通常很高做线性回归时同时放进去会导致系数不稳定这时候保留area丢掉rooms更合理。热力图存成 PNG直接放进文档和 PPT 里比截图清晰。4. 分析建模与可视化从描述统计到价格预测4.1 描述性分析先看清数据长什么样建模之前先做描述统计这一步能发现很多后面会踩的坑。def describe_by_district(df): 按区域统计单价分布 result df.groupby(district)[unit_price].agg([ (count, size), (mean, mean), (median, median), (std, std), (q25, lambda x: x.quantile(0.25)), (q75, lambda x: x.quantile(0.75)), ]).round(0) # 按中位数排序均值容易被极端值拉偏 return result.sort_values(median, ascendingFalse) stats describe_by_district(df) print(stats.head(10))用中位数排序而不是均值是因为每个区域都有几套天价房源均值会被拉高中位数更能反映典型房源的价格水平。q25和q75给出价格区间比单一数字信息量大。这个表直接放进文档就是区域价格对比那一节的核心内容。4.2 可视化三张图讲清价格分布可视化不用多三张图能讲清楚就行单价分布直方图、区域单价箱线图、面积与单价散点图。import matplotlib.pyplot as plt import matplotlib # 中文字体设置不设会显示方块 matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] False fig, axes plt.subplots(1, 3, figsize(18, 5)) # 图1单价分布 axes[0].hist(df[unit_price], bins50, color#4C72B0, edgecolorwhite) axes[0].set_xlabel(单价元/平米) axes[0].set_ylabel(房源数量) axes[0].set_title(单价分布直方图) # 图2区域箱线图只取房源量前8的区域 top_districts df[district].value_counts().head(8).index data [df[df[district] d][unit_price].values for d in top_districts] axes[1].boxplot(data, labelstop_districts, vertFalse) axes[1].set_xlabel(单价元/平米) axes[1].set_title(各区域单价分布) # 图3面积与单价散点 axes[2].scatter(df[area], df[unit_price], alpha0.3, s10) axes[2].set_xlabel(面积平米) axes[2].set_ylabel(单价元/平米) axes[2].set_title(面积与单价关系) plt.tight_layout() plt.savefig(overview.png, dpi150)中文字体那两行是必须的不设的话图上全是方块这是新手最常翻车的地方。箱线图只取房源量前 8 的区域区域太多图会挤成一团。散点图alpha0.3让重叠的点颜色加深能看出密度分布。这三张图放进 PPT一页就能把数据概况讲完。4.3 价格预测随机森林比线性回归更稳做价格预测线性回归可解释性强但对非线性关系拟合差随机森林精度高但不好解释。我的建议是两个都做用随机森林出预测结果用线性回归的系数讲影响因素。from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, r2_score from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline # 特征列 num_cols [area, house_age, rooms, total_floors, near_subway] cat_cols [district, floor_level, decoration] # 分类变量独热编码数值变量直接过 preprocessor ColumnTransformer([ (num, passthrough, num_cols), (cat, OneHotEncoder(handle_unknownignore), cat_cols), ]) X df[num_cols cat_cols] y df[unit_price] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 随机森林 rf Pipeline([ (prep, preprocessor), (model, RandomForestRegressor(n_estimators200, max_depth12, random_state42)), ]) rf.fit(X_train, y_train) pred_rf rf.predict(X_test) print(fRF MAE: {mean_absolute_error(y_test, pred_rf):.0f} R2: {r2_score(y_test, pred_rf):.3f}) # 线性回归做对照 lr Pipeline([ (prep, preprocessor), (model, LinearRegression()), ]) lr.fit(X_train, y_train) pred_lr lr.predict(X_test) print(fLR MAE: {mean_absolute_error(y_test, pred_lr):.0f} R2: {r2_score(y_test, pred_lr):.3f})ColumnTransformer把数值列和分类列分开处理这是 sklearn 的标准做法比手动编码干净。handle_unknownignore保证测试集出现训练集没见过的区域时不会报错。随机森林的max_depth12是防止过拟合树太深会把训练集的噪声也学进去。n_estimators200是精度和速度的平衡点再往上提升有限但训练时间线性增长。评估指标用 MAE 和 R2。MAE 直接告诉你预测单价平均差多少元比 RMSE 好解释。R2 看模型解释了多大比例的方差二手房数据一般能到 0.7 到 0.85低于 0.6 说明特征不够或者数据质量问题。4.4 特征重要性哪些因素真正影响房价随机森林能输出特征重要性这是做分析报告最有价值的部分。import numpy as np # 从 pipeline 里取出编码后的特征名 ohe rf.named_steps[prep].named_transformers_[cat] cat_names ohe.get_feature_names_out(cat_cols) feature_names num_cols list(cat_names) importances rf.named_steps[model].feature_importances_ idx np.argsort(importances)[::-1][:15] for i in idx: print(f{feature_names[i]:30s} {importances[i]:.4f})输出结果里area和district的某些区域通常排最前house_age和near_subway紧随其后。这个排序直接回答了什么因素最影响房价这个问题比任何定性描述都有说服力。把前 15 个特征画成条形图放进 PPT就是影响因素分析那一页。5. 避坑与排查这套流程里最容易翻车的五个地方5.1 采集被封请求频率和请求头没配对现象跑了几十页之后开始返回 403 或者验证码页面数据突然断掉。原因请求间隔太短或者 User-Agent 一直是同一个。有些站点还会检查 Referer 和 Cookie。解决把time.sleep调到 2 到 5 秒随机准备多个 User-Agent 轮换必要时加上 Referer 头。如果还是被封降低采集量分多次跑每次间隔几小时。不要试图用多线程加速单机多线程只会更快触发封禁。5.2 单价算错总价单位是万不是元现象算出来的单价是几百万一平米明显不对。原因页面上的总价是320万直接当 320 元用了。单价页面给的是元/平米但总价是万。解决统一单位。总价乘以 10000 转成元再除以面积得到元/平米。或者反过来单价除以 10000 得到万/平米。关键是全流程统一不要一部分用万一部分用元。我在清洗函数里专门加了一行df[total_price] df[unit_price] * df[area] / 10000来强制对齐。5.3 中文乱码matplotlib 和 CSV 两个地方都要设现象图表上中文全是方块或者 CSV 用 Excel 打开是乱码。原因matplotlib 默认字体不支持中文CSV 默认编码是 utf-8 但 Excel 认的是 utf-8-sig。解决matplotlib 加rcParams[font.sans-serif] [SimHei]CSV 保存时用encodingutf-8-sig。这两个地方都要设只设一个另一个还是会出问题。Linux 环境下 SimHei 可能没有换成WenQuanYi Micro Hei或者把字体文件放到项目目录里手动指定路径。5.4 模型过拟合训练集 R2 很高但测试集很差现象训练集 R2 到 0.98测试集只有 0.5。原因决策树深度太深或者特征里有泄漏。比如把total_price也放进特征了而total_price和unit_price是直接换算关系。解决先检查特征里有没有和标签直接相关的字段total_price、unit_price只能留一个。然后限制树深度max_depth从 8 开始试逐步加到验证集 R2 不再提升为止。再加min_samples_leaf5防止叶子节点样本太少。5.5 区域字段不统一同一个区有好几种写法现象groupby 之后发现朝阳和朝阳区分成两组统计结果对不上。原因不同页面或者不同采集批次区域字段的写法不一致。解决清洗时做一次标准化用映射表把常见变体统一。比如df[district] df[district].str.replace(区$, )去掉末尾的区或者建一个字典手动映射。这个步骤放在清洗函数最前面后面所有分析都依赖它。6. 文档与 PPT 怎么写出高分感把分析过程变成可讲的故事文档和 PPT 是这套项目里最容易被低估的部分。代码跑通只是及格能把为什么这么做讲清楚才是高分。我一般按问题—数据—方法—结论—局限五段式组织文档每段对应 PPT 的两到三页。文档部分重点写三块。第一块是数据字典把每个字段的来源、类型、处理方式列成表答辩时老师最爱问字段定义。第二块是方法选择理由比如为什么用中位数不用均值、为什么用随机森林不用线性回归每个选择都要有对比。第三块是结论和局限结论要具体到数字比如房龄每增加 5 年单价中位数下降约 8%局限要诚实比如标题关键词判断近地铁准确率约七成未使用真实距离数据。PPT 部分控制在 15 到 20 页。封面一页背景和问题一页数据来源和字段一页清洗流程一页可视化三页分布、区域、散点模型对比一页特征重要性一页结论一页局限和展望一页最后致谢一页。每页只放一张图或一张表文字不超过 5 行。图表用之前存好的 PNG分辨率 150 dpi 足够投影。代码组织上建议按crawl.py、clean.py、analyze.py、model.py四个文件拆分每个文件有if __name__ __main__入口能单独跑。再加一个requirements.txt列出依赖版本一个README.md写清楚运行顺序。这样别人拿到你的项目照着 README 三步就能跑起来这在答辩和面试里是加分项。最后一个技巧把分析结论做成一个可交互的查询脚本输入区域和面积输出预测单价和同区域对比。这个脚本不用多复杂二十行代码就够但演示效果很好能让老师或面试官直观感受到你的项目能用。我自己做这类项目最大的教训是别一上来就追求模型精度先把数据链路跑通、把文档写清楚。模型 R2 从 0.75 提到 0.80 要花三天但文档从能看改到能讲只要三小时后者对分数的贡献反而更大。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?