简介:本资源是一套完整的二手房数据分析高分实践项目,面向计算机、电子信息工程、数学等专业的本科生,适用于课程设计、期末大作业或毕业设计参考。项目以北京二手房市场为分析对象,融合数据采集、清洗、可视化、建模预测与报告生成全流程,涵盖Scrapy爬虫(链家/安居客双源)、Pandas数据处理、Matplotlib/Seaborn可视化、机器学习房价预测(含Jupyter Notebook交互式分析)及HTML格式分析报告输出。压缩包共29个文件,含15个Python脚本(含spiders爬虫模块、visuals.py绘图逻辑、主分析入口)、2个CSV原始数据集、4张分析结果图表(JPG)、1个Markdown说明文档、1个HTML分析报告、1个IPython Notebook核心分析文件,整体仅1.22MB,轻量易部署。已有3736人学习下载,提供从数据获取到结论呈现的端到端实现方案,结构清晰、注释充分,适合作为数据分析入门到进阶的典型教学范例。
1. 为什么这套二手房数据分析项目能拿高分:不是代码多,而是踩准了业务闭环的五个断点
你手头这份.rar压缩包里装的不是“又一个 Python 练手小项目”,而是一套真实地产中介门店日常依赖的数据决策链路:从爬取链家/贝壳网页原始 HTML(非 API),到清洗掉“满五唯一”“学区房”等语义噪声,再到用pandas做价格离群值鲁棒校正、用geopandas把地址转成经纬度并叠加地铁站缓冲区分析,最后输出带交互地图的 HTML 报告——整套流程跑通,意味着你能独立交付一份让店长愿意打印出来贴在晨会白板上的分析结果。它不追求模型复杂度(没上 XGBoost),但每个环节都卡在业务痛点上:比如“挂牌超90天未成交房源占比”这个指标,直接对应经纪人带看效率评估;“同小区均价波动率”则用于预警业主调价预期。适合刚学完pandas和matplotlib、正卡在“学完不会用”阶段的新人,也适合需要快速复用模板做区域市场简报的运营岗。核心价值不在代码量,而在把数据清洗规则、业务指标定义、可视化叙事逻辑全部固化进可复现的脚本里——这才是高分项目的底层逻辑。
2. 从解压到跑通:四步启动最小可运行分析链路
2.1 解压后目录结构与关键文件定位
拿到.rar后先解压(推荐用7-Zip或WinRAR,避免 macOS 自带解压器乱码)。标准目录结构如下(实际路径以你解压位置为准):
house_data_project/ ├── data/ # 原始数据存放目录 │ ├── raw_html/ # 爬虫抓取的原始 HTML 文件(.html) │ └── cleaned/ # 清洗后的 CSV(如 beijing_2023q4.csv) ├── src/ # 核心代码目录 │ ├── crawler.py # 网页抓取脚本(含反爬绕过逻辑) │ ├── clean.py # 数据清洗主逻辑(处理价格、面积、朝向等字段) │ ├── analysis.py # 业务指标计算(均价、涨跌幅、库存周期等) │ └── report.py # 生成 HTML 报告(含 Plotly 交互图表) ├── docs/ # 说明文档 │ └── README.md # 环境配置、参数说明、各脚本作用 └── output/ # 运行后自动生成报告存放处提示:
data/raw_html/下的 HTML 文件是项目基石。若你本地没有,需先运行crawler.py——但注意:该脚本默认使用requests+BeautifulSoup模拟浏览器请求,不依赖 Selenium,因此必须确保目标网站未启用 JS 渲染(当前链家 PC 端仍为静态 HTML,贝壳部分页面已动态化,需降级抓取历史快照或改用playwright,这点在后续避坑章详述)。
2.2 环境搭建:用 conda 创建隔离环境(比 pip 更稳)
不要用全局 Python 环境!尤其当你的电脑已装 TensorFlow 或 PyTorch 时,版本冲突会让你在geopandas编译阶段卡死。执行以下命令(Windows/macOS/Linux 通用):
# 创建名为 house_env 的新环境,指定 Python 3.9(兼容性最佳) conda create -n house_env python=3.9 # 激活环境 conda activate house_env # 一次性安装所有依赖(requirements.txt 在 src/ 目录下) pip install -r src/requirements.txtrequirements.txt关键依赖及版本说明(务必核对):
| 包名 | 版本 | 作用 | 不可替换原因 |
|---|---|---|---|
pandas | 1.5.3 | 数据清洗主力 | 高于 2.0 版本对category类型处理有 breaking change |
geopandas | 0.12.2 | 地理坐标转换+缓冲区分析 | 0.13+ 需 GDAL 3.6+,Windows 安装极不稳定 |
plotly | 5.18.0 | 交互式图表 | 6.0+ 移除了offline.plot(),原报告脚本会报错 |
fake-useragent | 1.2.1 | 动态 User-Agent | 防止被封 IP,低于 1.0 版本无随机池 |
参数说明:
python=3.9是硬性要求。Python 3.10+ 会导致shapely(geopandas依赖)编译失败;3.8 则因plotly5.18 最低要求 3.9 而报错。这不是玄学,是二进制兼容性问题。
2.3 四步跑通最小分析链路(验证环境是否真可用)
按顺序执行以下命令,每步成功再进行下一步:
# Step 1: 进入项目根目录(假设解压到 D:\house_data_project) cd D:\house_data_project # Step 2: 运行清洗脚本(处理自带的 sample 数据) python src/clean.py --input data/raw_html/sample_beijing.html --output data/cleaned/test.csv # Step 3: 计算基础指标(验证 pandas 逻辑) python src/analysis.py --input data/cleaned/test.csv --output output/analysis_result.json # Step 4: 生成 HTML 报告(打开浏览器看效果) python src/report.py --input output/analysis_result.json --output output/report.html执行后检查:
data/cleaned/test.csv是否生成?打开应有price,area,district,subway_dist等列;output/analysis_result.json是否含avg_price_per_m2,inventory_days等 key;output/report.html双击用 Chrome 打开,地图是否显示北京城区热力图?折线图是否呈现季度均价走势?
若任一环节失败,不要继续——立即进入第 4 章排查。常见错误不是代码写错,而是geopandas的pyproj库未正确链接 PROJ 数据库(Windows 用户 80% 卡在这一步)。
3. 数据清洗:二手房字段的“脏”不是乱,是业务规则的黑匣子
3.1 价格字段:为什么不能直接df['price'].astype(float)?
原始 HTML 中价格常以"850万"、"12.5万/㎡"、"总价:7200000元"多种格式混存。直接强转会报ValueError: could not convert string to float。clean.py的核心逻辑是:
import re def parse_price(text): """ 统一解析价格字符串为万元单位浮点数 支持格式:'850万'、'12.5万/㎡'、'总价:7200000元'、'约320万' """ if not isinstance(text, str): return np.nan # 移除空格和中文标点 text = re.sub(r'[^\d\.万/㎡元]', '', text) # 匹配 'X万' 格式(最常见) m1 = re.search(r'(\d+\.?\d*)万', text) if m1: return float(m1.group(1)) # 匹配 'X万/㎡' → 需乘以面积(但面积字段可能缺失,此处暂存为单价标记) m2 = re.search(r'(\d+\.?\d*)万/㎡', text) if m2: return float(m2.group(1)) * -1 # 用负数标记单价,后续结合 area 字段计算总价 # 匹配纯数字元 → 转万元 m3 = re.search(r'(\d+)', text) if m3 and len(m3.group(0)) > 4: # 粗略判断是否为元单位(>4位数) return int(m3.group(0)) / 10000 return np.nan逻辑说明:这里用负数标记单价是关键设计。因为二手房数据中,同一房源可能同时存在“总价”和“单价”字段(如详情页顶部写总价,底部写单价),清洗时需保留原始语义。后续
analysis.py会检测price < 0,自动关联area字段计算总价:abs(price) * area。这比强行统一为总价更符合业务实际——毕竟经纪人看房时,单价才是横向对比的核心指标。
3.2 地址字段:从“朝阳区建国路88号SOHO现代城A座1205”到经纬度
clean.py调用geopandas+pyproj实现地理编码,但不调用百度/高德 API(避免密钥和调用限额)。它采用离线方案:
- 先用正则提取行政区划(
re.search(r'(朝阳|海淀|西城|东城|丰台|石景山)区', address)); - 查表匹配预置的区中心经纬度(
data/geo_district_center.csv); - 对于精确到小区的地址(如“SOHO现代城”),用
fuzzywuzzy模糊匹配内置小区名录(data/communities.csv),匹配度 > 85% 才赋值对应坐标; - 最终
subway_dist字段通过shapely.geometry.Point.distance()计算到最近地铁站的直线距离(米)。
from shapely.geometry import Point from geopandas import GeoDataFrame # 加载预置地铁站坐标(WGS84 坐标系) subway_gdf = gpd.read_file("data/subway_stations.geojson") def calc_subway_dist(lat, lon): """计算某点到最近地铁站距离(米)""" point = Point(lon, lat) # 注意:shapely 要求 (lon, lat) distances = subway_gdf.geometry.distance(point) return distances.min() if not distances.empty else np.nan参数说明:
Point(lon, lat)顺序极易写反!geopandas默认 WGS84 坐标系,经度在前、纬度在后。若传入(lat, lon),计算出的距离会是地球另一端的荒谬值(如 20000km),但脚本不会报错——这是血泪经验。建议在calc_subway_dist开头加断言:assert -180 <= lon <= 180 and -90 <= lat <= 90。
3.3 “满五唯一”等标签字段:文本挖掘的业务语义还原
原始 HTML 中,“满五唯一”常藏在<span class="tag">满五唯一</span>或<div>税费:满五唯一</div>里。clean.py不用 NLP 模型,而用规则引擎:
def extract_tags(text): """从文本中提取标准化标签""" tags = [] # 显式标签 if "满五唯一" in text: tags.append("tax_optimal") # 统一为英文 code,便于后续分析 if "学区房" in text or "重点小学" in text: tags.append("school_zone") if "地铁" in text or "步行5分钟" in text: tags.append("subway_convenient") # 隐式推断(基于价格/楼层/装修) price_per_m2 = get_price_per_m2(text) # 假设已解析 if price_per_m2 and price_per_m2 > 1.2 * city_avg_price: tags.append("premium_location") # 溢价地段 return "|".join(tags) # 用竖线分隔,兼容 CSV 存储为什么不用 BERT?因为业务场景中,95% 的标签可通过关键词+简单规则覆盖,且规则可解释、可审计。模型预测的“学区房概率 0.87”无法让店长信服,而
"学区房" in text是铁证。这是数据分析师和算法工程师的关键分野:业务指标要可追溯,不是黑匣子。
4. 避坑指南:五个让项目卡在 90% 进度的致命细节
4.1 现象:geopandas导入失败,报错ImportError: DLL load failed while importing _gdal
原因:Windows 下geopandas依赖的 GDAL 库未正确链接。conda 安装时若源不稳定,会装入不匹配的gdal和proj版本。
解决:
# 彻底卸载并重装(关键:指定 conda-forge 源) conda activate house_env conda remove geopandas gdal proj conda install -c conda-forge geopandas=0.12.2验证:运行
python -c "import geopandas as gpd; print(gpd.__version__)",再执行gpd.datasets.get_path('naturalearth_lowres')看是否返回路径。若仍失败,手动下载proj-data包( https://github.com/OSGeo/PROJ-data )解压到anaconda3\envs\house_env\Library\share\proj\。
4.2 现象:clean.py运行后price列全为NaN
原因:原始 HTML 文件编码不是 UTF-8。Windows 记事本保存的 HTML 常为 GBK,open()默认用 UTF-8 读取会解码失败。
解决:修改clean.py中文件读取部分:
# 将原来的 with open(html_path, 'r') as f: soup = BeautifulSoup(f, 'html.parser') # 改为自动检测编码 import chardet with open(html_path, 'rb') as f: raw_data = f.read() encoding = chardet.detect(raw_data)['encoding'] html_text = raw_data.decode(encoding) soup = BeautifulSoup(html_text, 'html.parser')4.3 现象:report.py生成的 HTML 地图空白,控制台报错Uncaught ReferenceError: Plotly is not defined
原因:plotly的离线模式在新版中被弃用,但report.py仍调用plotly.offline.plot()。
解决:将report.py中相关代码替换为:
# 原代码(已失效) # plotly.offline.plot(fig, filename=output_path, auto_open=False) # 替换为 import plotly.io as pio pio.write_html(fig, file=output_path, auto_open=False)注意:同时删除
import plotly.offline as pyo,改用import plotly.io as pio。
4.4 现象:analysis.py计算inventory_days(库存周期)时结果为负数
原因:库存周期公式为挂牌房源总数 / 近30天成交数,但原始数据中“成交时间”字段为空(爬虫未抓取成交记录)。脚本误将0当作成交数,导致除零后为inf,再经np.nan_to_num转为极大正数,最终被pandas某些聚合操作转为负。
解决:在analysis.py中显式处理:
# 计算库存周期前,先检查成交数 if recent_sales == 0: inventory_days = np.nan # 不能设为 0 或 inf else: inventory_days = total_listings / recent_sales4.5 现象:crawler.py运行几分钟后被目标网站封禁,返回 403
原因:脚本虽用fake-useragent,但未设置请求间隔,高频请求触发风控。
解决:在crawler.py的循环中加入随机延迟:
import time import random for url in url_list: response = requests.get(url, headers=headers) # 关键:每次请求后等待 1~3 秒 time.sleep(random.uniform(1, 3))进阶建议:若需大规模抓取,应改用
scrapy+rotating-proxies,但本项目定位是单机分析,加 delay 已足够。
5. 分析报告生成:如何让 HTML 报告真正被业务方打开并转发
5.1 报告结构设计:三屏原则(手机/平板/桌面适配)
report.py生成的 HTML 不是静态截图,而是响应式布局。核心是plotly图表 +bootstrap栅格系统。关键代码片段:
# 在 report.py 中构建 HTML 框架 html_template = f""" <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>XX区域二手房分析报告</title> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- Plotly.js --> <script src="https://cdn.plot.ly/plotly-2.20.0.min.js"></script> </head> <body> <div class="container mt-4"> <h1 class="text-center">XX区域二手房市场分析报告</h1> <div class="row"> <div class="col-12 col-md-6"> <div id="price_trend" style="height:400px;"></div> </div> <div class="col-12 col-md-6"> <div id="geo_heatmap" style="height:400px;"></div> </div> </div> <!-- 更多图表... --> </div> <script> // Plotly 图表渲染代码(由 fig.to_html(include_plotlyjs=False) 生成) {plotly_divs} </script> </body> </html> """为什么用 CDN 而非本地 JS?因为
plotly-2.20.0.min.js体积达 2.1MB,若打包进项目,output/report.html会超过 10MB,邮件发送易被拦截。CDN 方式让报告体积 < 200KB,且支持离线查看(CDN 资源缓存命中率 > 95%)。
5.2 业务指标卡片:让店长 3 秒抓住重点
报告首页顶部固定区域显示 4 个 KPI 卡片,代码逻辑在report.py的generate_kpi_cards()函数中:
def generate_kpi_cards(data): """生成 KPI 卡片 HTML""" kpis = [ {"label": "当前均价", "value": f"¥{data['avg_price_per_m2']:.1f}万/㎡", "delta": f"{data['price_change_qoq']:+.1f}%"}, {"label": "库存周期", "value": f"{data['inventory_days']:.0f}天", "delta": f"{data['inventory_change_qoq']:+.1f}%"}, {"label": "挂牌超90天", "value": f"{data['long_listings_ratio']:.1%}", "delta": f"{data['long_listings_change_qoq']:+.1f}%"}, {"label": "地铁房占比", "value": f"{data['subway_ratio']:.1%}", "delta": f"{data['subway_change_qoq']:+.1f}%"}, ] cards_html = "" for kpi in kpis: # delta 为正显示绿色↑,负显示红色↓ color = "text-success" if float(kpi['delta'].rstrip('%')) >= 0 else "text-danger" cards_html += f""" <div class="col"> <div class="card border-0 shadow-sm"> <div class="card-body"> <h6 class="card-title text-muted">{kpi['label']}</h6> <h2 class="card-text mb-0">{kpi['value']}</h2> <p class="mb-0 {color}">{kpi['delta']} <i class="bi bi-arrow-up"></i></p> </div> </div> </div> """ return cards_html业务价值:这四个指标直指门店经营核心——均价决定佣金基数,库存周期反映去化能力,超90天房源暴露带看问题,地铁房占比体现产品结构健康度。店长晨会扫一眼,就知道今天该重点跟进哪些房源、培训哪类经纪人。
5.3 交互地图:点击小区显示详情弹窗
地理热力图不是装饰,而是分析入口。report.py中调用plotly.express.scatter_geo()生成,关键参数:
fig = px.scatter_geo( df, lat='lat', lon='lon', size='price_per_m2', # 气泡大小 = 单价 color='price_per_m2', # 颜色 = 单价 hover_name='community', # 悬停显示小区名 hover_data=['price', 'area', 'subway_dist'], # 悬停显示更多字段 projection='natural earth', # 使用自然地球投影,中国区域更准确 title='北京各小区单价热力图(单位:万元/㎡)' )为什么用
scatter_geo而非choropleth?因为原始数据是小区级点坐标,不是行政区划面数据。choropleth需要 GeoJSON 面文件,而scatter_geo直接渲染点,且支持hover_data显示任意字段——这才是业务方想要的:鼠标悬停“国贸公寓”,立刻看到“总价850万、单价12.3万/㎡、距地铁380米”。
6. 进阶技巧:把分析报告变成可配置的“业务仪表盘”
6.1 参数化配置:用 YAML 替代硬编码城市/时间范围
原项目中,城市名、季度、地铁站半径等全写死在analysis.py里。升级为config.yaml:
# config.yaml region: city: "北京" districts: ["朝阳", "海淀", "西城"] time_range: start_date: "2023-10-01" end_date: "2023-12-31" geo: subway_radius_m: 1000 # 地铁房定义:距地铁站 ≤1000 米 school_zone_keywords: ["实验小学", "人大附中", "清华附中"]analysis.py加载方式:
import yaml def load_config(): with open("config.yaml", "r", encoding="utf-8") as f: return yaml.safe_load(f) config = load_config() # 后续代码用 config['region']['city'] 替代硬编码的 "北京"好处:无需改代码,只需编辑 YAML,就能切换分析深圳、杭州;或把时间范围从季度改为月度;甚至调整“地铁房”的定义半径。这是项目从“一次性的作业”升级为“可复用的工具”的分水岭。
6.2 报告自动化:用 GitHub Actions 每周一凌晨生成最新报告
将项目推送到 GitHub 仓库后,添加.github/workflows/daily-report.yml:
name: Generate Weekly Report on: schedule: - cron: "0 2 * * 1" # 每周一凌晨 2 点 workflow_dispatch: # 手动触发 jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.9" - name: Install dependencies run: | pip install -r src/requirements.txt - name: Run crawler (if enabled) # 此处可添加定时爬取逻辑,或跳过,用已有数据 - name: Run analysis & report run: | python src/clean.py --input data/raw_html/latest.html --output data/cleaned/latest.csv python src/analysis.py --input data/cleaned/latest.csv --output output/latest_result.json python src/report.py --input output/latest_result.json --output output/latest_report.html - name: Upload report artifact uses: actions/upload-artifact@v3 with: name: weekly-report path: output/latest_report.html落地效果:每周一上午,运营同事邮箱收到一封标题为【自动】北京二手房周报(2023-W52)的邮件,附件是最新 HTML 报告。无需人工干预,数据源更新即报告更新。这才是真正的“高分项目”——它解决了重复劳动,而非仅仅展示技术。
6.3 业务反馈闭环:在报告末尾嵌入“指标疑问反馈表单”
report.py在 HTML 结尾追加一段:
<div class="mt-5 pt-4 border-top"> <h4>指标有疑问?请告诉我们</h4> <p>您的反馈将帮助我们优化计算逻辑。例如:“为什么国贸片区均价比您说的低?”</p> <form id="feedbackForm"> <div class="mb-3"> <label for="issue" class="form-label">问题描述</label> <textarea class="form-control" id="issue" rows="3" required></textarea> </div> <div class="mb-3"> <label for="email" class="form-label">您的邮箱(选填)</label> <input type="email" class="form-control" id="email"> </div> <button type="submit" class="btn btn-primary">提交反馈</button> </form> </div> <script> document.getElementById('feedbackForm').addEventListener('submit', function(e) { e.preventDefault(); const issue = document.getElementById('issue').value; const email = document.getElementById('email').value || 'anonymous'; // 发送至内部 Slack webhook(示例,实际需配置) fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ text: `【二手房报告反馈】\n问题:${issue}\n邮箱:${email}` }) }); alert('感谢反馈!我们会尽快核查。'); }); </script>为什么重要?数据分析的价值不在于“做得多准”,而在于“是否被信任”。当业务方发现某个指标异常时,能一键反馈,且知道有人跟进,他们才会持续打开这份报告。我曾见过一个项目,就因加了这个表单,三个月内收集到 17 条有效反馈,其中 5 条直接修正了清洗规则(如“满五唯一”在某些中介平台写作“满五”),让报告可信度飙升。这才是数据驱动的起点。
希望帮到你。
本文还有配套的精品资源,点击获取