简介:基于 Python 的二手房数据分析完整项目,面向数据分析与爬虫方向的初学者、课程设计和毕业设计学生,提供从数据采集、清洗到可视化展示的一站式参考方案。压缩包共 157 个文件、约 48.05MB,其中包含 18 个 Python 源码、18 个 CSV 数据文件、15 个 HTML 页面及配套 JS 脚本,另有 Word 文档、PPT 答辩资料、65 张 PNG 截图和少量字体配置文件,结构清晰,便于按模块阅读。代码注释详细,个人手打 98 分项目,系统功能完善、界面美观、操作简单,下载后简单配置即可运行,适合期末大作业、毕业设计直接参考或二次开发。项目数据集覆盖原始数据、清洗后数据等多个版本,可通过折线图、柱状图等形式展示二手房价格分布与区域热度;经过严格调试可稳定运行,同时附有文档说明和 PPT,帮助快速理解设计思路,目前已有 126 人学习下载。
1. 二手房数据分析这个“高分项目”,到底在分析什么
打开任意一个源码分享平台,这类标注“完整源码+文档说明+PPT资料”的二手房数据分析项目下载量都常年靠前。但大多数人的实际体验是:代码能跑通,数据也有,却讲不清整个项目到底在论证什么。二手房价分析不是算法竞赛,它的本质是一条完整的数据流水线——从房源网页把半结构化的挂牌信息抓下来,清洗成可计算的表格,再通过可视化和回归模型回答“哪些因素在影响房价”这个问题。这个项目适合Python基础语法已过关、正在找数据分析方向课程设计或求职作品的人,也适合想看看爬虫、Pandas和统计模型在真实工程里怎么被串起来的开发者。读透它,你得到的不是一个脚本集,而是一套可以迁移到任何结构化数据分析任务的完整方法。
2. 数据采集:先定字段再写爬虫,二手房源数据怎么落到本地
2.1 为什么源码里最先出现的不是爬虫,而是建表脚本
拿到这类项目源码,很多人第一个动作是找爬虫主文件,翻完目录却发现排在最前面的是一个 schema.sql 或 fields.py。这个顺序是有道理的:二手房源的原始信息是典型的半结构化数据,一个房源页面里“总价”可能带货币单位,“户型”写的是“3室2厅1卫”,“朝向”偶尔整条缺失。如果不在采集之前把字段约定清楚,后面每做一次分析都要回头补数据,这比爬虫本身更耗时间。
我一般会把房源字段分成三类。标识字段:城市、区县、板块、小区名,用于后续分组对比;价格字段:总价、单价,是分析的核心因变量;结构字段:面积、户型、楼层、朝向、楼龄、装修状况,是候选自变量。另外一定要加一个 crawled_at 采集时间字段,因为房价对时间敏感,不同批次的数据混在一起时,没有时间戳就无法解释价格波动。
建表的 SQLite 脚本通常长这样:
CREATE TABLE house_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, city TEXT NOT NULL, district TEXT, block TEXT, community TEXT, layout TEXT, area_raw TEXT, floor TEXT, orientation TEXT, built_year TEXT, total_price REAL, unit_price REAL, crawled_at TEXT DEFAULT (datetime('now', 'localtime')) );注意 total_price 用 REAL 存储浮点价格,unit_price 留到清洗阶段再计算,而 area_raw、floor、built_year 这些先统一存成 TEXT。原因很简单:原始字段里“89.5平”“共6层”“2008年建”混着各种单位符号,文本存储不会在采集阶段丢信息,解析延后到清洗环节是最稳妥的做法。字段设计回答的是“这个项目能分析什么”,而不是“今天能爬什么”。
2.2 python爬虫的关键代码:requests 加 BeautifulSoup 抓取列表页
采集模块是这个项目里最依赖目标网站结构的部分。基础的 python 爬虫方案固定是 requests 拿 HTML,BeautifulSoup 做解析。一个最小可用的抓取函数如下:
import requests from bs4 import BeautifulSoup import random import time def fetch_listing(city, page): url = f"https://example.entry.house/{city}/pg{page}/" 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": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", } resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text html = fetch_listing("chaoyang", 1) soup = BeautifulSoup(html, "html.parser") card_nodes = soup.select(".house-lst .info") print("本页房源卡片数:", len(card_nodes))headers 里的 User-Agent 是必须的第一层伪装,很多服务端只凭这一个字段就会拒绝请求;timeout=10 避免单页卡死拖垮整个采集任务;resp.apparent_encoding 根据页面字节内容推断编码,解决中文乱码。select 里的 CSS 选择器一定要对照目标站点的实际 HTML 结构调整,这是整个爬虫里唯一必须人工改的部分,其他逻辑换个城市名就能复用。
拿到卡片节点后,再从每个节点里提取标题、链接、总价等字段,逐条写入 DataFrame。这个循环代码在真实项目里往往有几十行,但核心逻辑就是定位节点、取文本、存字段这三步。解析逻辑建议单独写一个函数 parse_card(node),后续网站改版时只改这个函数,不必动采集主流程。
2.3 落库选择:CSV、SQLite 还是 Excel
采集后的数据要落盘,三类方式的取舍直接决定后面清洗的效率。
| 落库方式 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|
| CSV | 一次性分析、快速验证 | 通用性强,Excel 可打开 | 无类型约束,重复写入易出错 |
| SQLite | 多批次增量采集 | 字段类型明确,支持 SQL 查询 | 需要一点 SQL 基础 |
| Excel | 直接交付给业务方 | 直观、筛选方便 | 写入慢,数据量过万行就吃力 |
我一般的做法是采集阶段先写 CSV,理由是断点续采简单,追加一行就行;等全量采完,再用 pandas 一次性导入 SQLite,后续清洗全部用 SQL 或 pandas 操作。导入一行代码即可:
import sqlite3 import pandas as pd df = pd.read_csv("data/raw/house_list.csv") conn = sqlite3.connect("data/house.db") df.to_sql("house_info", conn, if_exists="append", index=False)参数说明:if_exists="append" 表示不清空已有表,适合分批次追加;index=False 避免把 pandas 自带索引写成多余列。到这里,原始数据已经在一个可查询的结构里了,下一步的清洗才有据可依。
2.4 反爬与采集纪律:限速、重试和边界
二手房网站的列表页通常有基础的反爬策略,常见做法是请求频率限制和 User-Agent 检测。对个人学习项目来说,做到三步就够:每次请求之间随机延时 2 到 5 秒;遇到 403 或 5xx 状态码时指数退避重试;把请求头补全,尤其是 Accept-Language。这里不讨论任何绕过人机验证的方案,那既不合规,对一个数据分析项目也没有必要——几千条样本完全够用。
for page in range(1, 51): try: text = fetch_listing("chaoyang", page) # 解析并写入文件的逻辑 except requests.RequestException as exc: print(f"page {page} failed: {exc}") time.sleep(10) continue time.sleep(random.uniform(2, 5))提示:采集前先看目标网站的 robots.txt 和版权声明,只拿公开列表页的展示信息,不碰需要登录或验证码的数据。
3. 数据清洗与特征工程:把“89平米”“3室2厅”变成可算的字段
3.1 缺失值处理:先看业务含义,再决定丢弃还是填充
爬下来的数据大概率不干净。第一件要做的事是统计缺失和重复,而不是立刻写填补逻辑。常见的统计代码是这样:
import pandas as pd df = pd.read_sql("SELECT * FROM house_info", conn) print(df.isnull().sum()) print(df.duplicated(subset=["community", "total_price", "area_raw"]).sum()) df = df.drop_duplicates(subset=["community", "total_price", "area_raw"]) df = df.dropna(subset=["total_price"]) df["orientation"] = df["orientation"].fillna("未知")df.isnull().sum() 按列统计空值数量,能快速看出哪些字段不可靠;drop_duplicates 用小区名加总价加面积三个字段联合去重,比只用房源标题更稳;dropna 删掉 total_price 为空的记录,因为总价是分析核心,缺失值无法合理填充;orientation 的缺失率如果很高,说明该平台很多房源本来就不填朝向,缺失本身代表一种状态,填“未知”比删行更符合业务事实。
判断标准就一条:缺失值有没有业务含义。有含义就单独分类,没有含义才考虑填充或删除。朝向、装修这类字段属于前者,总价、面积属于后者。
3.2 文本字段的正则解析:从“3室2厅”里拆出结构化数字
户型、面积、楼层这类字段在网页里全是字符串,不能直接进模型。用正则提取是标准方案,Pandas 的 str.extract 配合命名分组可以直接生成新列:
df["面积"] = df["area_raw"].str.extract(r"(\d+\.?\d*)平").astype(float) df["室数"] = df["layout"].str.extract(r"(\d+)室").astype(float) df["厅数"] = df["layout"].str.extract(r"(\d+)厅").astype(float) df["所在楼层"] = df["floor"].str.extract(r"(\d+)层").astype(float) df["总楼层"] = df["floor"].str.extract(r"共(\d+)层").astype(float) df["楼龄"] = 2025 - df["built_year"].astype(int)正则解释:(\d+\.?\d*)匹配“89”或“89.5”这类带可选小数的数字;(\d+)室定位到“3室2厅”里的“3”;“共(\d+)层”提取总楼层,因为“低楼层/共6层”这类文本里的数字不止一个。注意 astype(float) 而不是 astype(int),因为任何一行解析失败都会产生 NaN,NaN 是浮点类型,转 int 会直接报错。所有提取后的新列先保持 float,后续建模时再用 fillna 处理残余缺失。
解析完一定要检查解析失败比例。如果 layout 里有“3室0厅”“4室2厅2卫”等变体,某些样本的厅数可能提取不到,这个比例超过 5% 就需要回去看原始文本。
3.3 特征构造:单价、距地铁距离、楼层系数
原始字段清洗完,还需要构造几个对房价解释力很强的特征。单价是第一个必须算的:
df["单价"] = df["total_price"] * 10000 / df["面积"]这里的 total_price 单位是万元,面积单位是平方米,乘以 10000 把总价换成元,再除以面积得到每平米的单价。单价在不同区域之间直接对比才有意义,总价受面积影响太大,不能作为区域差异的比较基准。
距地铁站距离从房源描述里很难直接拿到,常见做法是用小区名反查地图 API,或者用页面里已有的“距地铁站XX米”文本。后者更简单:
df["距地铁站_m"] = df["traffic_text"].str.extract(r"距地铁站(\d+)米").astype(float) df["距地铁站_km"] = df["距地铁站_m"] / 1000提取后用公里做单位,系数量级对回归模型更友好。楼层系数也是一个高解释力的特征,把所在楼层除以总楼层得到一个 0 到 1 的比值,用来表达“相对楼层位置”,比绝对楼层信息更通用,因为 5 层楼里的 4 层和 30 层楼里的 4 层含义完全不同。
3.4 数据体检:describe 和 value_counts 的快速排查法
清洗完不是直接建模,先做一次全表体检。以下三行代码是体检标配:
print(df.describe().T) print(df["district"].value_counts().head(10)) print(df[df["单价"] < 5000].head())describe 输出所有数值列的 count、mean、std、min、四分位数、max,重点看 min 和 max 是否合理,比如面积出现 1 平米或单价出现 0 元都是明显的脏数据;value_counts 看区县的样本分布,如果一个区只有十几条,后面分组对比要谨慎;筛出单价低于 5000 的行逐条检查,这种往往是车位、商业公寓或者数据录入错误。
提示:对脏数据先查一条确认原因再批量处理,不要因为一个离群值就把整列替换掉。
4. 从描述到推断:探索性分析与房价影响因素的量化结论
4.1 可视化先行:Matplotlib 和 Seaborn 的组合用法
建模之前必须先做可视化,这一步能暴露数据里 80% 的问题。一个标准的探索性分析会同时看单变量分布和分组差异:
import matplotlib.pyplot as plt import seaborn as sns fig, axes = plt.subplots(1, 2, figsize=(14, 5)) sns.histplot(df["单价"], bins=50, kde=True, ax=axes[0]) axes[0].set_title("单价分布") sns.boxplot(data=df, x="district", y="单价", ax=axes[1]) axes[1].set_title("各区单价对比") axes[1].tick_params(axis="x", rotation=45) plt.tight_layout() plt.savefig("output/eda_price.png", dpi=150)bins=50 让直方图保留足够细节,kde=True 叠加密度曲线方便看整体形态;boxplot 的中位数和箱体可以直接对比区域间价格差距。保存图片时 dpi 设到 150,后续放进 PPT 不会模糊。做这一步的意义是先用眼睛确认数据形态,再决定用什么模型。
4.2 区域差异与单价分布怎么读
单价分布通常呈右偏,少量豪宅把均值拉高,中位数才是代表性水平。箱线图的读法有固定顺序:先看箱体位置,中位数高的区域整体价格高;再看箱体宽度,宽度大说明区域内房价分化严重;最后看离群点,离群点往往对应豪宅或异常数据。如果某个区的箱体上方出现一大片散点,查询这些样本的小区名,确认是真实豪宅还是采集错误。
4.3 用 statsmodels 做多元线性回归,核心参数怎么看
探索性分析给出方向后,用多元线性回归量化每个因素的影响。statsmodels 是更合适的库,因为它的输出自带 P 值和置信区间,适合做统计推断:
import statsmodels.api as sm features = ["面积", "室数", "楼龄", "距地铁站_km", "总楼层"] X = sm.add_constant(df[features]) model = sm.OLS(df["总价"], X).fit() print(model.summary())sm.add_constant 给自变量矩阵加一列全 1,对应回归方程的截距项;OLS 是普通最小二乘估计;fit 完成拟合后,summary 输出系数、标准误、t 值和 P 值。结果里最需要读懂的参数如下:
| 参数 | 含义 | 解读方法 |
|---|---|---|
| const | 截距 | 所有自变量为 0 时的基础价格,本身很少有意义 |
| coef(系数) | 每增加 1 个单位因变量的变化 | 面积的 coef 表示每多 1 平米总价增加多少 |
| P>t | 系数显著性 | 小于 0.05 认为该特征显著,大于 0.05 考虑剔除 |
| R-squared | 模型解释力 | 越接近 1 拟合越好,但多变量时要看调整后 R² |
楼龄的系数通常为负,代表折旧效应;距地铁站_km 的系数也通常为负且显著,说明离地铁越远价格越低。这些系数符号如果和直觉相反,优先检查是否存在多重共线性,而不是急着改模型。
4.4 多重共线性与特征选择的两个坑
面积和室数天然高度相关,大户型面积必然大,放进同一个模型会导致系数不稳定。判断标准是方差膨胀因子 VIF:
from statsmodels.stats.outliers_influence import variance_inflation_factor X = sm.add_constant(df[features]) for i, name in enumerate(features): vif = variance_inflation_factor(X.values, i) print(f"{name}: VIF = {vif:.2f}")VIF 大于 10 说明该特征与其他特征存在严重共线性,一般做法是保留面积、删除室数,因为面积是更连续的度量。另一个坑是 R² 很高但系数全部不显著,这往往出现在样本量小、特征多的场景,处理方法是删减特征,保留业务上最核心的三到五个变量。回归模型在这里的目的不是追求最高精度,而是得到可解释的结论,这个定位决定了特征选择和模型评估的方式都和机器学习竞赛完全不同。
5. 源码、文档与 PPT:把分析项目交付成“高分作品”
5.1 源码目录怎么组织才算工程化
这类项目在答辩时被问得最多的问题就是“哪部分是你写的”。如果所有代码堆在两个文件里,这个问题很难回答。推荐按执行顺序组织目录,用编号让任何人拿到源码都能按序复现:
house_price_analysis/ ├── README.md ├── requirements.txt ├── data/ │ ├── raw/ # 采集原始 CSV │ └── clean/ # 清洗后数据 ├── scripts/ │ ├── 01_crawl.py │ ├── 02_clean.py │ ├── 03_eda.py │ └── 04_model.py ├── docs/ │ └── 分析报告.md └── slides/ └── 答辩资料.pptxscripts 里的编号就是执行顺序,04_model.py 直接读取 03_eda.py 生成的中间结果,每一步的输出都有明确去向。requirements.txt 必须锁定关键版本,Pandas、statsmodels、requests 缺一不可。
5.2 文档说明里必须写清楚的三件事
文档说明不是代码注释的合集,而是让别人不看代码也能复现流程。三件事必须写:环境依赖版本,包括 Python 大版本和依赖库版本;复现步骤,从安装依赖到运行哪个脚本再到产物位置;核心结论,用两三段话说明分析发现了什么。评分者通常先看文档再看代码,文档写得清楚等于给项目加分。
5.3 PPT 资料的叙事线:问题、数据、结论,不是贴代码
PPT 是给评审看的故事线,不是代码仓库的投影。建议按四段走:研究问题与数据来源、清洗与特征工程的难点、可视化发现、回归结论。每页只放一个核心图表或一个核心数字,比如“楼龄每增加 1 年,总价平均下降 X 万元”这种可验证的结论,比满屏代码有说服力得多。演示时被问到数据来源就讲采集流程,被问到结论就指向回归系数表,所有素材都在源码和文档里,现场翻得出来,项目才立得住。
本文还有配套的精品资源,点击获取