资讯动态

Python实战:成都二手房挂牌数据分析与可视化全流程

发布时间:2026/10/13 20:33:28 来源:尧图企业网站定制
简介这份资源面向数据分析初学者与Python实战练习者围绕成都二手房市场展开完整的数据分析项目帮助读者掌握从数据采集到建模预测的全流程方法。包内共17个文件以12个CSV数据文件、3个Jupyter Notebook、1个Python脚本和1份Word报告为主CSV按锦江、双流、龙泉驿、温江、武侯等区域分别存储房源明细Notebook涵盖链家网数据爬取与建模分析脚本与报告则对应数据处理和结论输出压缩包约6.2MB。资源基于三万余条成都主城区二手房真实数据字段包括总价、单价、小区位置、户型、楼层、建筑面积、朝向、装修、梯户比例、挂牌时间、交易权属等可支撑数据清洗、可视化探索与房价预测建模等练习。目前已有1230人学习下载适合希望积累完整数据分析案例、理解特征工程与回归预测思路的读者参考。1. 成都二手房价数据分析 python从链家公开页到可复现的成交洞察成都二手房市场有个很反直觉的现象挂牌价和成交价的差距在锦江、高新南区能拉到 8% 到 15%而在温江、新都这些外围区域差距往往只有 3% 到 5%。这意味着如果你只盯着挂牌均价做判断核心区的价格信号会被系统性高估。用 python 做成都二手房价数据分析要解决的就是这件事——把公开挂牌数据抓下来、清洗干净、按区域和户型拆开看最后落到「哪个板块的挂牌价水分最大、哪种户型在降价」这种能指导决策的结论上。这套流程适合两类人一是想拿真实数据练手 python 数据分析与可视化的入门者二是关注成都楼市、想自己跑一遍数据而不是看中介口径的从业者。整条链路不依赖付费数据源用 requests pandas matplotlib 就能跑通核心难点不在代码而在反爬节奏控制、字段清洗和区域口径对齐。下面按「数据怎么来 → 怎么洗 → 怎么分析 → 坑在哪」的顺序拆开讲。2. 数据获取用 requests 抓成都二手房挂牌页的最小可用方案2.1 为什么选公开挂牌页而不是现成数据集做成都二手房价数据分析第一步永远是数据从哪来。常见做法有三种找 Kaggle/天池上的现成数据集、用公开 API、自己抓挂牌页。前两种的问题是——成都本地的数据集更新慢很多还是 2020 年前后的快照区域划分和现在对不上公开 API 要么收费要么字段残缺。所以我自己跑的时候选的是抓公开挂牌列表页按区域分页拉取每天固定时间跑一次攒够两周就能看出挂牌价的变动趋势。选 requests 而不是 Selenium是因为列表页的数据基本都在 HTML 里不需要渲染 JS。Selenium 启动浏览器开销大跑几千页的时候速度差好几倍。但如果目标站点把数据放在 XHR 接口里返回 JSON那就直接请求接口比解析 HTML 更稳。判断方法很简单打开开发者工具的 Network 面板翻一页看有没有返回 JSON 的请求有就优先打接口。2.2 抓取脚本分页 区域筛选 限速下面是一个最小可用的抓取骨架按区域和页码拼 URL每次请求间隔随机 2 到 4 秒避免触发风控。import requests import time import random import pandas as pd 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 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } # 成都主要行政区按需增减 DISTRICTS [jinjiang, gaoxin, wuhou, qingyang, chenghua, tianfu] def fetch_page(district, page): 抓取单个区域单页返回 HTML 文本 url fhttps://cd.example.com/ershoufang/{district}/pg{page}/ try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() return resp.text except requests.RequestException as e: print(f[FAIL] {district} page {page}: {e}) return None def parse_list(html): 从列表页提取房源卡片字段按实际 DOM 调整 soup BeautifulSoup(html, html.parser) rows [] for item in soup.select(.sellListContent li): title item.select_one(.title a) total item.select_one(.totalPrice span) unit item.select_one(.unitPrice span) info item.select_one(.houseInfo) if not (title and total and unit): continue rows.append({ title: title.get_text(stripTrue), total_price: total.get_text(stripTrue), unit_price: unit.get_text(stripTrue), house_info: info.get_text(stripTrue) if info else , }) return rows def crawl(districts, max_page50): all_rows [] for d in districts: for p in range(1, max_page 1): html fetch_page(d, p) if not html: break rows parse_list(html) if not rows: break # 该区域已翻完 for r in rows: r[district] d all_rows.extend(rows) time.sleep(random.uniform(2, 4)) # 限速别贪快 return pd.DataFrame(all_rows) if __name__ __main__: df crawl(DISTRICTS, max_page30) df.to_csv(cd_ershoufang_raw.csv, indexFalse, encodingutf-8-sig) print(f抓取完成共 {len(df)} 条)逻辑说明fetch_page负责拼 URL 和发请求超时设 10 秒失败返回 None 让上层中断该区域parse_list用 BeautifulSoup 按 CSS 选择器提取标题、总价、单价、户型信息选择器要按目标站点的实际 DOM 改crawl双层循环遍历区域和页码每页之间 sleep 2 到 4 秒随机值。参数上max_page控制每个区域最多翻多少页DISTRICTS里的区域代号要和 URL 路径一致。跑完存成 CSV编码用utf-8-sig不然 Excel 打开中文会乱码。提示限速是硬要求不是可选项。间隔低于 1 秒连续请求轻则返回验证码页重则 IP 被封几小时。宁可跑慢点也别一次把数据源打挂。2.3 字段解析从「3室2厅 | 98.5平米 | 南 | 精装」里拆出结构化列抓下来的house_info是一坨字符串必须拆成户型、面积、朝向、装修四个字段后面分析才能按维度切。常见做法是用正则一次性匹配比 split 更稳因为分隔符偶尔会变。import re def parse_house_info(s): 把 3室2厅 | 98.5平米 | 南 | 精装 拆成结构化字段 pattern r(\d室\d厅|\d室) \| ([\d.])平米 \| (\S) \| (\S) m re.search(pattern, s) if not m: return pd.Series([None, None, None, None]) return pd.Series([m.group(1), float(m.group(2)), m.group(3), m.group(4)]) df[[layout, area, orientation, decoration]] df[house_info].apply(parse_house_info)逻辑说明正则里(\d室\d厅|\d室)兼容「3室2厅」和「1室」两种写法([\d.])平米抓面积并转 float后面两个(\S)分别抓朝向和装修。匹配失败返回四个 None方便后面统计缺失率。参数上如果目标站点的分隔符是「/」而不是「|」把正则里的\|换掉即可。拆完之后一定要df.isnull().sum()看一眼缺失比例超过 10% 说明选择器或正则有问题得回去调。3. 数据清洗单价口径、区域对齐和异常值处理3.1 单价字段的三种脏数据形态挂牌页上的单价通常写成「12,345元/平米」或「12345元/平」偶尔还有「暂无数据」。直接astype(float)必翻车。清洗分三步去单位、去千分位逗号、转数值转不了的置为 NaN。def clean_unit_price(s): if not isinstance(s, str): return None s s.replace(,, ).replace(元/平米, ).replace(元/平, ).strip() try: return float(s) except ValueError: return None df[unit_price_num] df[unit_price].apply(clean_unit_price) df[total_price_num] df[total_price].str.replace(万, ).astype(float)逻辑说明clean_unit_price先剥掉单位和逗号再尝试转 float失败返回 None总价字段一般带「万」字直接去掉转 float 就行。清洗完要交叉验证一下——total_price_num * 10000 / area应该约等于unit_price_num偏差超过 5% 的说明字段错位得单独捞出来看。3.2 区域口径对齐高新南区到底算不算一个区成都的区域划分有个坑高新南区在行政上跨武侯和双流但挂牌页往往单列。如果你按行政区聚合高新南区的房源会被拆到两个区里均价就被稀释了。我一般会建一张映射表把挂牌页的区域代号统一映射到分析口径。挂牌页代号分析口径说明gaoxin高新南区单独统计不并入武侯tianfu天府新区含直管区不含高新南jinjiang锦江区含攀成钢板块wuhou武侯区不含高新南部分映射表用 dict 存清洗时df[region] df[district].map(REGION_MAP)。这张表不是一成不变的每次数据源改版都要核对一遍否则区域均价会莫名其妙跳变。3.3 异常值单价 500 元和 20 万的房源怎么处理真实数据里一定有异常值。单价低于 500 元的多半是车位或商铺混进来了高于 20 万的可能是别墅或录入错误。处理方式不是无脑删而是先标记再决定。q1 df[unit_price_num].quantile(0.01) q99 df[unit_price_num].quantile(0.99) df[is_outlier] (df[unit_price_num] q1) | (df[unit_price_num] q99) print(f异常值占比: {df[is_outlier].mean():.2%}) df_clean df[~df[is_outlier]].copy()逻辑说明用 1% 和 99% 分位数做边界比固定阈值更适应数据分布。is_outlier列保留下来方便回头检查被剔除的到底是什么房源。如果异常值占比超过 5%说明数据源本身有问题得先查抓取逻辑而不是急着删。4. 分析建模按区域和户型拆出价格信号4.1 区域均价对比谁在领涨、谁在阴跌清洗完的数据第一件事是按区域算均价和中位数。均价容易被高价房源拉偏中位数更能反映典型水平两个都算对比着看。region_stats df_clean.groupby(region)[unit_price_num].agg( [mean, median, count] ).round(0).sort_values(median, ascendingFalse) print(region_stats)逻辑说明groupby(region)按区域聚合同时算均值、中位数和样本量。count很重要——样本少于 30 条的区统计意义不大结论要谨慎。排序按中位数降序一眼能看出价格梯队。如果某个区的均值和median差距超过 15%说明该区房源价格分化严重得再按板块细分。4.2 户型与面积段交叉90 平以下和 120 平以上的走势分化成都市场上90 平以下刚需户型和 120 平以上改善户型的价格走势经常相反。用pd.cut把面积分段再和户型交叉能看出结构性差异。bins [0, 60, 90, 120, 150, 1000] labels [60平以下, 60-90平, 90-120平, 120-150平, 150平以上] df_clean[area_bin] pd.cut(df_clean[area], binsbins, labelslabels) pivot df_clean.pivot_table( valuesunit_price_num, indexarea_bin, columnsregion, aggfuncmedian ).round(0) print(pivot)逻辑说明pd.cut按面积切五段pivot_table做面积段和区域的交叉透视值取中位数。这样能看出「锦江区的 60 平以下单价是不是明显高于 90-120 平」这类结构性问题。参数上bins 的切分点要按成都实际情况调比如 90 平是契税分界线必须保留。4.3 挂牌价与成交价的差距估算严格说公开挂牌页拿不到真实成交价但可以用「同小区同户型挂牌价的中位数」和「该房源挂牌天数」做粗略推断——挂牌天数越长、降价次数越多说明挂牌价水分越大。如果数据里有「调价」字段直接统计调价幅度分布。# 假设有 price_change 字段记录调价次数 if price_change in df_clean.columns: df_clean[price_change] pd.to_numeric(df_clean[price_change], errorscoerce) drop_stats df_clean.groupby(region)[price_change].agg( [mean, median, lambda x: (x 0).mean()] ) drop_stats.columns [平均调价次数, 中位调价次数, 降价房源占比] print(drop_stats.round(2))逻辑说明price_change为负表示降价统计每个区的平均调价次数和降价房源占比。降价占比高的区说明挂牌价虚高、议价空间大。这个指标比单纯看均价更有决策价值。参数上errorscoerce保证脏数据不报错直接变 NaN。5. 避坑与排查跑成都二手房数据时最容易翻车的 5 个点5.1 现象抓了 2000 条清洗完只剩 800 条原因字段解析正则太严house_info格式一变就匹配失败大量行被置为 NaN 后剔除。解决先统计解析成功率df[area].notnull().mean()低于 90% 就放宽正则比如把\d室\d厅改成\d室.*?厅?或者对匹配失败的样本单独打印出来看格式。5.2 现象区域均价算出来和中介 App 差好几千原因区域口径没对齐高新南区被拆进了武侯或者把商住公寓混进了住宅。解决建映射表统一口径同时在清洗阶段过滤掉「写字楼」「公寓」「车位」等关键词df df[~df[title].str.contains(公寓|写字楼|车位)]。5.3 现象脚本跑一半开始返回验证码页原因限速不够或者请求头太假。解决把 sleep 间隔拉到 3 到 6 秒加上Referer头指向列表页必要时轮换 User-Agent。如果已经被封等几小时再跑别硬刚。5.4 现象单价和总价对不上差出好几倍原因总价字段带「万」但没去掉或者单价字段混进了「元/平」以外的单位。解决清洗后做交叉校验abs(total*10000/area - unit) / unit 0.05的行单独导出检查多半是字段错位或单位问题。5.5 现象matplotlib 画图横坐标太密集区域名叠在一起原因区域数量多、画布太窄。解决plt.figure(figsize(12, 6))加大画布横坐标标签用plt.xticks(rotation45, haright)旋转或者干脆用横向条形图barh区域名放纵轴就不会挤。6. 进阶技巧用滚动窗口看挂牌价的周度变化单次快照只能看静态价格真正有价值的是趋势。我一般会每天固定时间跑一次抓取存成带日期的 CSV攒够几周后用滚动窗口算周度中位数看哪个区在悄悄降价。# 假设 df_all 是多次抓取拼接的结果含 crawl_date 列 df_all[crawl_date] pd.to_datetime(df_all[crawl_date]) weekly df_all.set_index(crawl_date).groupby(region)[unit_price_num].resample(W).median() weekly weekly.reset_index() # 算 4 周滚动中位数平滑掉单周波动 weekly[rolling_4w] weekly.groupby(region)[unit_price_num].transform( lambda x: x.rolling(4, min_periods2).median() )逻辑说明resample(W)按周聚合取中位数rolling(4, min_periods2)算 4 周滚动中位数min_periods2保证数据不够 4 周时也能出值。这样能看出趋势而不是被单周异常带偏。参数上窗口大小按数据积累速度调只有两周数据就别用 4 周窗口。验证方法拿滚动中位数和当月实际成交均价如果能从其他渠道拿到对比偏差在 5% 以内说明这套挂牌数据能近似反映市场。偏差太大要么是样本偏差比如只抓了热门小区要么是口径问题得回去查。我自己的习惯是每次跑完先看三张图区域中位数排序、面积段交叉热力图、周度滚动趋势。三张图对不上就说明数据有问题宁可重跑也别硬分析。这套流程不复杂难的是坚持每天跑、保持口径一致。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑