简介:这份资源面向数据分析初学者与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;q=0.9", } # 成都主要行政区,按需增减 DISTRICTS = ["jinjiang", "gaoxin", "wuhou", "qingyang", "chenghua", "tianfu"] def fetch_page(district, page): """抓取单个区域单页,返回 HTML 文本""" url = f"https://cd.example.com/ershoufang/{district}/pg{page}/" try: resp = requests.get(url, headers=HEADERS, timeout=10) 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(strip=True), "total_price": total.get_text(strip=True), "unit_price": unit.get_text(strip=True), "house_info": info.get_text(strip=True) if info else "", }) return rows def crawl(districts, max_page=50): 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_page=30) df.to_csv("cd_ershoufang_raw.csv", index=False, encoding="utf-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", ascending=False) 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"], bins=bins, labels=labels) pivot = df_clean.pivot_table( values="unit_price_num", index="area_bin", columns="region", aggfunc="median" ).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"], errors="coerce") 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为负表示降价,统计每个区的平均调价次数和降价房源占比。降价占比高的区,说明挂牌价虚高、议价空间大。这个指标比单纯看均价更有决策价值。参数上,errors="coerce"保证脏数据不报错,直接变 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(rotation=45, ha="right")旋转,或者干脆用横向条形图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_periods=2).median() )逻辑说明:resample("W")按周聚合取中位数,rolling(4, min_periods=2)算 4 周滚动中位数,min_periods=2保证数据不够 4 周时也能出值。这样能看出趋势而不是被单周异常带偏。参数上,窗口大小按数据积累速度调,只有两周数据就别用 4 周窗口。
验证方法:拿滚动中位数和当月实际成交均价(如果能从其他渠道拿到)对比,偏差在 5% 以内说明这套挂牌数据能近似反映市场。偏差太大,要么是样本偏差(比如只抓了热门小区),要么是口径问题,得回去查。
我自己的习惯是每次跑完先看三张图:区域中位数排序、面积段交叉热力图、周度滚动趋势。三张图对不上就说明数据有问题,宁可重跑也别硬分析。这套流程不复杂,难的是坚持每天跑、保持口径一致。希望帮到你。
本文还有配套的精品资源,点击获取