ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python二手房数据分析实战:从数据采集到房价预测的全流程

Python二手房数据分析实战:从数据采集到房价预测的全流程 简介数据分析项目的成败往往不在于模型多复杂而在于是否理解数据清洗、特征工程和机器学习建模等基础环节的原理。以Python数据分析为工具能够将数据采集、预处理、可视化到模型训练串成完整链路而数据质量与特征设计决定了模型预测的准确度。这种工程化方法具有广泛应用价值例如在二手房市场分析中针对海量挂牌数据通过异常值处理、缺失值填充和特征融合再构建回归模型即可实现房价预测与因素分析。对于学习者或从业者而言将完整流程固化为可复现的项目并配合清晰的说明文档和分析报告才能真正发挥技术价值。1. 为什么推荐把“二手房数据分析”当成一个完整项目来做1.1 房价分析是Python全流程练手的“最佳综合体”我做Python数据分析这些年带过不少新人也帮人救过不少课程设计和毕设。有一个感受特别深很多人学数据分析是碎片化的今天用pandas读了个csv明天用matplotlib画了个折线图但是一到“给我一个真实项目”就卡住了。根因在于——脑子里没有一个完整的项目框架。二手房数据分析恰好能把数据分析全流程串起来数据采集爬虫或者直接给原始数据、数据清洗、特征工程、探索性可视化、机器学习建模、结果解读、报告撰写一步不缺。而且房价数据的特征语义非常明确面积、户型、朝向、楼层、区域、总价、单价每一个字段都有明确的业务含义不像网上很多空泛的练习数据分析半天不知道自己在干什么。更重要的是“基于Python的二手房数据分析源码数据说明文档分析报告”这个组合正好对应一个高分项目该有的全部要素代码能跑、数据能查、文档能读、报告能讲。很多人项目做完了代码也能运行可是没有说明文档答辩的时候被老师问几个字段含义就懵了也有人报告写得好看但是代码根本跑不通。能同时把四样东西补齐项目评分天然就高。1.2 这个项目的适用场景课程设计、毕设和求职作品集我接触过三个典型的使用场景你可以自己对号入座课程设计很多高校的数据分析类课程期末作业就是“对某个真实领域的数据集进行分析并输出报告”。二手房数据量大、维度多做出来的可视化图表又好看评委容易给高分。毕业设计如果题目是“基于Python的XX数据分析与可视化”二手房是一个非常稳妥的选题。数据获取途径公开方法成熟模型可解释性强论文也好写。求职作品集现在数据分析岗位的面试很多候选人简历上写着“熟悉Python、pandas、机器学习”但一问项目细节就露馅。一个完整的房价分析项目既能展示业务理解能力又能展示代码能力面试官问起来你也能讲得清楚。接下来我按照一个真实项目的推进顺序把每一步的思路、代码和坑位都梳理出来。建议你拿到这篇文章后不要只当“资料推荐”看而是找个周末把代码一行一行敲一遍感受完全不同。2. 先定框架目录结构、字段设计和技术栈选型2.1 目录结构决定了一个项目好不好维护、好不好答辩很多人做数据分析项目喜欢把所有的代码堆在一个Jupyter Notebook里从头跑到尾。自己调试的时候没问题但是一旦数据量上来或者分析逻辑复杂了Notebook就会变得又臭又长答辩的时候很难讲清楚“哪一步做了什么、为什么这么做”。我建议把项目拆成这种结构house_price_analysis/ ├── data/ │ ├── raw/ # 原始数据采集下来尽量不动 │ ├── cleaned/ # 清洗之后的数据 │ └── models/ # 保存训练好的模型文件 ├── codes/ │ ├── 01_spider.py # 数据采集 │ ├── 02_data_cleaning.py # 数据清洗 │ ├── 03_eda_visualization.py # 探索性分析 图表输出 │ ├── 04_feature_engineering.py # 特征工程 │ ├── 05_model_train.py # 模型训练与评估 │ └── 06_generate_report_charts.py # 生成报告用的最终图表 ├── figs/ # 图表输出目录 ├── 说明文档.md ├── 分析报告.md └── requirements.txt这里的关键原则是原始数据不轻易改动每个步骤一个独立脚本中间结果落盘。这样一来即使某一步做错了也不需要重新爬数据只需要从出错的那一步重新运行即可。2.2 数据字典先想清楚要哪些字段再动手一套合理的字段设计能让你少走很多弯路。参考我实际用过的字段整理如下字段名示例类型说明house_id101123456str房源唯一标识去重用district朝阳str行政区biz_circle望京str商圈/板块community望京西园str小区名称layout3室2厅1卫str户型原始文本area89.6float建筑面积平米total_price560float总价万unit_price62500float单价元/平米orientation南北str朝向floor_desc中楼层/共18层str楼层描述build_year2015int建筑年代decoration精装str装修情况elevator有str有无电梯listed_date2024-01-15datetime挂牌日期follow_count37int关注人数urlhttps://...str房源链接字段不是越多越好抓取数据本身有成本清洗也有成本。建议抓核心字段等到建模阶段发现缺少什么特征再定向补抓。2.3 技术栈选择用最成熟稳定的一套我用的是如下这套组合也是目前数据分析项目里最通用、最好答辩护的组合采集requestsBeautifulSoup4如果目标页面是动态渲染的再加Selenium数据处理pandasnumpy可视化matplotlibseaborn用于静态图表如果想让报告更炫一点可以加plotly建模scikit-learn进阶可以加xgboost结果复现Jupyter Notebook或Jupyter Lab这套组合的优点在于每个库都是数据分析领域的“标准件”你遇到任何问题在网上都能搜到成熟解决方案。不建议一上来就上特别冷门的新框架那不是给项目加分是给自己挖坑。3. 数据采集如何拿到一套可用的二手房数据3.1 数据源的选择与合规意识做二手房分析最常见的数据源是链家、贝壳这类公开房产信息平台。它们有完整的列表页和详情页而且数据字段相对齐整。这里必须强调一句爬取公开数据时要注意遵守网站的robots协议和服务条款个人学习研究用途要控制采集频率不要对目标网站造成压力。如果是课程设计数据量不需要太大几千到两万条就足够支撑后续分析了。我一般会按城市做采集一个城市抓全城所有在售房源。以某城市链家为例列表页URL大致是这个规律https://{city}.lianjia.com/ershoufang/pg{n}/每页大约30套房源抓一万套差不多要翻300多页。对于个人项目抓取到“量级足够”就停下来不要追求“全量”——数据量大到清洗不动的时候反而会拖慢项目进度。3.2 一个极简可用的采集思路我建议先手动打开一个列表页在浏览器开发者工具里确认数据是直接渲染在HTML里的还是通过接口异步加载的。如果HTML里就包含房源标题、链接和关键信息用requests BeautifulSoup就够了。先写一个请求函数控制请求间隔和User-Agentimport requests import time from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 } def fetch_html(url, retry3): for i in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 if resp.status_code 200: return resp.text except requests.RequestException: pass time.sleep(2) return None再解析列表页里的房源链接和基础字段def parse_list_page(html): soup BeautifulSoup(html, lxml) items [] for li in soup.select(ul.sellListContent li): title_tag li.select_one(.title a) if title_tag is None: continue link title_tag[href] house_id link.split(/)[-1].replace(.html, ) layout_tag li.select_one(.houseInfo) total_price_tag li.select_one(.totalPrice) unit_price_tag li.select_one(.unitPrice) items.append({ house_id: house_id, title: title_tag.get_text(stripTrue), layout: layout_tag.get_text(stripTrue) if layout_tag else , total_price: total_price_tag.get_text(stripTrue) if total_price_tag else , unit_price: unit_price_tag.get_text(stripTrue) if unit_price_tag else , url: link, }) return items列表页只能拿到一部分字段更完整的朝向、楼层、建筑年代、小区名等需要进一步访问详情页。详情页的采集逻辑类似解析div.base和div.introContent下面的信息项即可。3.3 反爬、失败重试和断点续抓网上很多人做这类项目会栽在采集这一步。最常见的两个问题请求速度太快IP被临时封禁。解决办法就是设置随机睡眠时间比如每次请求后time.sleep(random.uniform(1, 3))同时随机切换User-Agent。跑到一半程序挂了前面的数据全丢。解决办法是每抓一定数量的房源就把结果增量写入CSV同时记录最后抓取的位置下次从断点继续。import csv import random import os def save_incremental(items, filepath): file_exists os.path.exists(filepath) with open(filepath, a, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesitems[0].keys()) if not file_exists: writer.writeheader() writer.writerows(items)我习惯把采集结果存成CSV而不是存数据库原因就一个CSV可以直接被pandas读取而且项目报告里解释数据采集过程时说“用pandas读取CSV”比说“连接MySQL”要简单许多。数据量在万级以下CSV完全够用。4. 数据清洗决定分析质量的主战场4.1 原始数据普遍有多脏采集下来的数据直接跑统计往往是没法看的。最常见的几个问题面积字段是字符串“89.6平米”需要提取数字总价字段是字符串“560万”需要去掉单位户型字段“3室2厅1卫”需要拆成几个独立数值特征楼层字段“中楼层/共18层”需要拆分建筑年代有些缺失有些混合了“未知”等文本同一个小区的房源总价和单价计算的面积口径可能不一致会出现总价除以面积对不上单价的情况这些问题如果不处理干净后面建模就是“垃圾进垃圾出”。4.2 清洗代码的实用写法我用pandas做清洗时习惯把清洗逻辑封装成函数每一段写清楚注释import pandas as pd import numpy as np def clean_raw_data(df: pd.DataFrame) - pd.DataFrame: # 1. 删除重复房源 df df.drop_duplicates(subset[house_id]).copy() # 2. 面积提取 df[area] df[area].astype(str).str.extract(r([\d.])).astype(float) # 3. 总价提取 df[total_price] ( df[total_price].astype(str).str.replace(万, ).astype(float) ) # 4. 单价提取去掉千分位 df[unit_price] ( df[unit_price].astype(str) .str.replace(r元/平, , regexTrue) .str.replace(,, ) .astype(float) ) # 5. 户型拆分3室2厅1卫 - room_num, hall_num, toilet_num df[room_num] df[layout].str.extract(r(\d)室).astype(float) df[hall_num] df[layout].str.extract(r(\d)厅).astype(float) df[toilet_num] df[layout].str.extract(r(\d)卫).astype(float) # 6. 楼层拆分 df[floor_level] df[floor_desc].str.extract(r(低楼层|中楼层|高楼层|顶层|底层)) df[total_floor] df[floor_desc].str.extract(r共(\d)层).astype(float) # 7. 建筑年代提取 df[build_year] df[build_year].astype(str).str.extract(r(\d{4})).astype(float) # 8. 删除核心字段为空的记录 df df.dropna(subset[area, total_price, unit_price]).copy() return df这里有一个容易被忽视的细节str.extract在匹配不到内容时会返回NaN所以后续要统一处理缺失值而不是直接把整行删掉——因为有些字段比如建筑年代缺失得很多删掉会影响样本量。4.3 异常值处理别让别墅和车位污染你的样本清洗完基础字段之后一定要看异常值。比如面积小于20平米的大概率是车位或者特殊情况面积大于400平米的可能是独栋别墅和普通住宅的价格逻辑完全不同单价可能高到十几万一平也可能低到几千块。处理异常值不要机械地套“3倍标准差法”先分组看业务逻辑# 按行政区分组查看单价的分布 df.groupby(district)[unit_price].describe()如果某个区出现单价不到5000的房源通常不是数据错误就是特殊房源比如小产权、继承房、法拍房这类样本如果不剔除会严重干扰回归模型的拟合。我的处理原则是面积在20到300平米之间单价在全市分布的0.5%到99.5%分位数内总价大于0删掉疑似车位、地下室样本low, high df[unit_price].quantile([0.005, 0.995]) df df[(df[unit_price] low) (df[unit_price] high)] df df[(df[area] 20) (df[area] 300)]4.4 特征工程让模型能“读懂”位置和房子清洗完成之后距离建模还差一步特征工程。直接扔给模型的如果是“望京西园”“南北”“精装”这些文本模型是学不到信息的需要转换区域district如果行政区的数量不多可以直接one-hot编码如果数量多可以尝试目标编码用该区域的历史平均单价代替但要防止过拟合。朝向orientation把“南北”“南”这类值做一下归类分成“南向”“北向”“东西”“其他”。房龄用当前年份减去建筑年代得到“房龄”。房龄和房价通常不是线性关系老房子的价格可能因为学区而高也可能因为设施老旧而低。是否有电梯对高层住宅来说比较重要转成0/1。挂牌时间可以算出“挂牌天数”不过这个特征需要跟踪初次抓数据时没有可以留到后续迭代。特征工程建议单独写一个脚本和清洗脚本分开因为后续建模调参会经常回来改特征保持步骤解耦能让项目结构清晰得多。5. 可视化探索从图表里读出市场的分层结构5.1 先看总价分布为什么不是正态分布拿到清洗后的数据第一件事是画总价分布直方图。为什么要先画这个因为目标变量的分布决定了后面建模时的处理方式。import matplotlib.pyplot as plt fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].hist(df[total_price], bins50, edgecolorwhite, color#4C72B0) axes[0].set_title(Total Price Distribution) axes[0].set_xlabel(Total Price (10k yuan)) axes[1].hist(np.log1p(df[total_price]), bins50, edgecolorwhite, color#55A868) axes[1].set_title(Log Transformed Total Price Distribution) plt.tight_layout() plt.savefig(figs/total_price_distribution.png, dpi150)房价总价的分布几乎必然是右偏的大量房子集中在某一个价格区间比如200-400万然后拖一条长长的尾巴到千万级。这种分布不适合直接用线性模型拟合建议对total_price做log1p变换让目标变量接近正态模型效果会明显好一些。5.2 面积和总价的关系不是简单线性画一个面积和总价的散点图你很快会发现一个有趣的现象总面积相同的两套房总价可能差出一倍。这就是“总价-面积”关系之外的隐藏因素——位置、楼层、装修、小区品质。plt.figure(figsize(8, 5)) plt.scatter(df[area], df[total_price], s4, alpha0.4, color#DD8452) plt.xlabel(Area (sqm)) plt.ylabel(Total Price (10k yuan)) plt.title(Area vs Total Price) plt.savefig(figs/area_vs_total_price.png, dpi150)如果散点图上颜色再叠加“区域”会有更明显的信息for district, color in zip([朝阳, 海淀, 丰台, 通州], [#4C72B0, #DD8452, #55A868, #C44E52]): sub df[df[district] district] plt.scatter(sub[area], sub[total_price], s4, alpha0.4, labeldistrict, colorcolor) plt.legend()5.3 分组对比区域和房龄的“话术”可视化的意义不只是“画个图交差”而是从图里得出可解释的结论。我用一批真实数据做出来的几个典型结论你可以体会一下区域分化明显中心城区每平米均价远高于近郊区而且中心城区的面积段更集中在中小户型近郊区大户型占比高。房龄和价格呈倒U型5年以内的次新房价格最高20年以上的老房子价格明显下滑但如果是重点学区房房龄影响会被显著削弱。朝南的房源占比不多价格却更高同一个小区里朝南/南北通透的房源单价可能比朝北的高10%-15%。顶层和底层的折价效应顶层尤其是无电梯的老小区顶层和底层房源单价往往低于中间楼层。这些结论如果都在报告里写清楚配合上面几类图表整个分析报告的可读性会非常高老师或者面试官一眼就能看出你真正理解了数据。6. 房价预测建模从基线模型到集成模型6.1 建模目标与数据划分数据分析项目做到建模阶段首先要明确目标是预测总价还是预测单价两者含义不同预测总价适合买房人估算预算预测单价更能反映房屋本身的价值。我建议项目里以“总价”为主要目标因为论文和报告中解释更直观。用sklearn划分训练集和测试集from sklearn.model_selection import train_test_split feature_cols [ area, room_num, hall_num, toilet_num, build_year, total_floor, ] district_dummy_cols orientation_dummy_cols X df[feature_cols] y np.log1p(df[total_price]) # 对目标取对数 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )这里有一个极其重要的提醒做数据清洗和特征工程时千万不要把测试集的目标变量泄露到训练集里。比如你如果用“小区均价”作为特征而这个均价包含了测试集的房子那模型的评估效果就是虚高的。处理方式是想办法在训练集内部做聚合或者直接用更朴素的特征。6.2 三个模型的对照线性回归、随机森林、XGBoost我建议至少做三个模型形成对照。第一个是线性回归作为基线。它的优势是可解释性强但缺陷也很明显——对特征和目标之间的非线性关系拟合能力弱。如果线性回归的R2太低反而说明数据集里有很强的非线性信息为后面用集成模型提供依据。第二个是随机森林。它对非线性关系、特征交互的拟合能力很强而且天然支持特征重要性排序几乎不用做什么预处理。第三个是XGBoost。它通常在表格数据上表现最好但需要调节超参数而且训练时间比随机森林长。from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score model RandomForestRegressor( n_estimators300, max_depth18, min_samples_leaf2, random_state42, n_jobs-1, ) model.fit(X_train, y_train) y_pred model.predict(X_test) y_test_exp np.expm1(y_test) y_pred_exp np.expm1(y_pred) print(R2:, r2_score(y_test, y_pred)) print(MAE (万元):, mean_absolute_error(y_test_exp, y_pred_exp)) print(RMSE (万元):, np.sqrt(mean_squared_error(y_test_exp, y_pred_exp)))用np.expm1把预测结果还原回“万元”单位方便和真实价格对比。6.3 评估指标的解读MAE到底意味着什么假设你的模型在测试集上R2是0.82MAE是28万意味着平均偏差约28万元。如果二手房总价的中位数是350万偏差率约8%。对于课程设计项目这个精度已经不错了如果业务上要用于估价则需要继续优化。典型的结果对照表模型R2MAE万元RMSE万元线性回归0.6552.478.1随机森林0.8130.247.6XGBoost0.8426.742.3这里还可以加一个散点图画真实值vs预测值如果点均匀分布在45度线两侧说明模型比较健康。6.4 特征重要性模型输出的商业结论随机森林和XGBoost都能输出特征重要性。通常面积、区域、房龄会排在前几位。这在报告里是很有价值的一页不只是告诉老师“我用了什么模型”而是告诉老师“我通过模型发现了哪些因素在影响房价”。比如某个城市的数据显示区域类的哑变量重要性加总超过了30%说明位置是房价的第一决定因素面积的重要性排在第二房龄排在第三。这个结论和经济学直觉一致但用数据证明出来才算是真正的分析。7. 说明文档和分析报告项目拿高分的另一半筹码7.1 说明文档让每个代码文件都能被“无痛运行”很多项目源码是给了但换一台电脑就跑不起来。说明文档写的不是“本项目实现了XX功能”这种废话而是要把环境、依赖和运行步骤写清楚。一个合格的说明文档至少包含运行环境Python版本比如3.9.7操作系统Windows/macOS/Linux依赖安装方式pip install -r requirements.txt数据说明原始数据从哪来字段定义是什么数据量多大运行顺序01_spider.py→02_data_cleaning.py→ ... 每一步的作用目录说明每个文件夹里放的是什么常见问题比如“爬虫被封怎么办”“运行到某一步报ModuleNotFoundError怎么办”我见过太多项目代码本身没问题但是文档说“运行main.py即可”结果main.py依赖一堆相对路径下的CSV文件别人根本找不到。所以文档里不要惜字如金目录结构图、文件说明表格都放进去。7.2 分析报告用“分析思路”串联起所有图表分析报告是给老师或者面试官看的它的核心不是展示代码而是展示“分析逻辑”。我常用的报告结构是项目背景与目标为什么分析二手房价格解决什么问题数据来源与说明数据来自哪个平台采集时间样本量数据预处理过程清洗了哪些脏数据、处理了哪些异常值、构建了哪些新特征探索性分析区域房价对比、面积与总价关系、楼层/朝向对价格的影响建模与评估模型对比表、真实值vs预测值散点图、特征重要性排序结论与建议影响房价的主要因素是什么对购房者和卖房者分别有什么参考价值注意这个结构里的“结论与建议”非常关键。不要只写“模型R2为0.84”而是写“模型显示在控制面积和房龄后朝阳区比通州区的均价高出约XX%说明地段仍是房价的核心决定因素”。能讲出这种话项目才算从“跑代码”变成了“做分析”。7.3 图表风格统一是报告的专业感来源我做报告图表时有几个习惯你直接抄所有图表的字体统一比如中文用“思源黑体”或“微软雅黑”图的大小统一配色用同一套色板不要一张图是蓝的一张图是红的每张图都要有标题、坐标轴标签、必要的数据标签DPI统一设置成150以上保证在Word或PDF里插入时不会模糊图表输出到figs/目录报告里按编号引用plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False这几行代码能避免大部分中文字体显示成方块的问题。8. 踩坑记录与经验延伸这些坑你大概率也会踩8.1 爬虫阶段的坑被反爬、字段失效、页码断层第一个坑是IP被临时封禁。一开始我抓得快每秒钟好几个请求结果大概1000条数据之后页面返回的全部是验证码页。解决办法就是限速每次请求之间随机睡1到3秒并且把User-Agent随机化实测稳定很多。第二个坑是页面结构改版。今天能解析出来的class名可能下个月就变了。所以采集代码要写好异常处理解析字段时要多用try...except做容错。如果不幸遇到解析失败不要手动去改几百行代码而是先重新抓一个列表页检查HTML结构到底哪里变了。第三个坑是页码断层。二手房页面的筛选条件变化会导致翻页时房源总数变动如果你“边翻页边抓”前后页的房源总量对不上就可能有漏抓或重复。解决方法是先获取“共XX套”的总数再根据每页房源数计算总页数一次性确定页码范围。8.2 数据清洗阶段的坑口径不一致和缺失值最严重的坑是单价和总价对不上。有的房源总价是590万面积是89.6平米算出来单价大约65800元/平米但页面上的单价可能标的是63000元/平米因为平台有时按“套内面积”折算单价。你不一定需要追查每一套的差异但要在报告里说明“本报告采用的单价以平台挂牌为准未做额外折算”。另一个坑是构建房龄特征时建筑年代缺失太多。缺失率超过30%时直接删行会损失大量样本。一个常用且合理的做法是用同一个小区的其他房源的中位建筑年代填充缺失值如果整个小区都没有数据再用所在商圈的中位数填充。8.3 建模阶段的坑数据泄露和过拟合数据泄露最常见的一个操作是把“小区名”直接做one-hot编码扔进模型。如果某个小区在训练集里出现过测试集里的同小区房源当然会预测得准但这属于“记住了答案”模型并没有真正理解房价。合理的做法是去掉过于细粒度的“小区名”保留行政区、商圈这种粒度适中的特征。过拟合的典型表现是训练集R2接近0.98测试集R2只有0.6。这时候优先调随机森林的max_depth、min_samples_leaf或者XGBoost的max_depth、learning_rate、subsample。不要盲目加特征特征不是越多越好。8.4 后续可以怎么扩展这个项目做完了如果想继续深挖我建议往三个方向走加入时间维度隔一段时间重新抓一次数据做价格走势分析和预测这就是“房价指数”的雏形。引入外部数据把房源坐标经纬度抓下来结合地铁站、学校、商圈POI数据计算“距最近地铁站步行距离”“是否学区”等特征模型精度和业务价值都会大幅提升。部署成可交互的Web应用用Flask或Streamlit把模型包装成一个简单的估价工具输入面积、区域、楼层就能输出预测价位这个作品放到简历里会非常亮眼。我个人做这类项目的最大体会是代码能跑通只是一个起点能把每个步骤的“为什么”讲清楚才是项目真正的分水岭。如果你正在做二手房数据分析相关的课程设计或毕设建议不要只满足于“复现一遍”而是试着把每一步的取舍都写进文档和报告里。等你哪天能面对老师或面试官从容地解释清楚“为什么清洗这一步要这样处理、为什么模型选随机森林而不是线性回归”这个项目的价值就算真正到手了。本文还有配套的精品资源点击获取
返回列表