简介:一份以深圳为例的安居客租房数据分析与可视化实验报告,面向Python爬虫、数据分析与可视化学习者,系统展示从数据获取到建模的全流程。实验爬取了安居客10000多条租房信息,分别用单线程、多线程与Scrapy框架实现,并利用Power Query完成数据清洗,再用Excel和Tableau完成房租均价、各行政区及小区分布的可视化对比。建模阶段采用多元线性回归,以面积、租房方式、房屋类型、地铁距离等为自变量,配合KNN原理删除异常值、Lasso回归筛选变量,最终给出面积每增1平方米租金增55元、距地铁每增100米租金减6元等可解释结论。资源为1个PDF文件,大小912KB,共2545人浏览学习,适合想通过完整案例掌握爬虫、数据清洗、可视化和回归建模的初学者。
1. 从10000条安居客数据看深圳租金的定价逻辑
每年6月毕业季,深圳的租房需求集中爆发。同一个小区里,朝南和朝北的差价能有多少?离地铁站500米和1500米的租金差距,是否值得每天多走十分钟?这些问题散落在上万条挂牌房源里,靠肉眼很难看出规律。这篇实验报告用 Python 爬取安居客深圳站 10000 多条租房信息,经 Power Query 清洗后,用 Excel 和 Tableau 做可视化,最后建立多元线性回归模型,量化面积、租房方式、楼层、地铁距离等因素对租金的影响。比较有参考价值的地方在于,建模时没有直接套回归代码,而是先用 KNN 原理检测并删除异常值,再用 Lasso 回归压缩特征,把变量从 330 个降到 111 个。对正在做租房行情、二手房分析或城市数据项目的从业者来说,这条从采集到建模的完整链路有不少可复用的细节。
2. 爬虫选型与 Power Query 清洗
2.1 单线程、多线程与 Scrapy 的取舍
数据源是安居客的租房列表页和详情页,可获取的字段包括标题、租房方式、有无电梯、地铁、租金、付款方式、户型、面积、朝向、楼层、装修、类型、小区。抓取时分别用单线程爬虫、多线程爬虫和 Scrapy 框架三种方案实现,实验报告对三者做了对比。
| 爬虫方式 | 优点 | 缺点 |
|---|---|---|
| 单线程爬虫 | 易上手、易理解 | 爬取速度慢、CPU 利用率不高 |
| 多线程爬虫 | 效率高、速度快 | IO 密集型操作下,线程加锁解锁消耗资源 |
| Scrapy 框架 | 灵活高效、开发速度快 | 上手偏难、中间件设计较复杂 |
对于租房这类页面结构相对规整、数据量在万级的场景,多线程已经够用。我一般用 requests 配合 ThreadPoolExecutor,并发控制在 8 个线程左右,既保证速度又不容易触发反爬。
import requests from concurrent.futures import ThreadPoolExecutor from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } def fetch_page(page_no): url = f"https://sz.zu.anjuke.com/fangyuan/p{page_no}/" try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() except requests.RequestException as e: print(f"page {page_no} failed: {e}") return [] soup = BeautifulSoup(resp.text, "html.parser") items = [] for li in soup.select(".list-item"): items.append({ "title": li.select_one(".house-title a").text.strip(), "price": int(li.select_one(".price strong").text), "area": li.select_one(".details-item").text.strip(), }) return items if __name__ == "__main__": with ThreadPoolExecutor(max_workers=8) as executor: results = list(executor.map(fetch_page, range(1, 501)))fetch_page 接收页码参数,请求列表页后用 BeautifulSoup 解析,从.list-item节点里提取标题、租金和面积。executor.map 会把 500 个页面按顺序分发到 8 个线程执行,返回结果顺序与传入顺序一致,方便后续直接拼接。timeout=10是必须的,否则某个响应卡住会拖慢整个线程池;max_workers设到 8 比较稳妥,调到 16 以上容易触发滑块验证。如果抓取规模再上一个量级,才需要考虑 Scrapy 的并发调优和下载中间件设计。
2.2 Power Query:字段拆分与类型统一
爬下来的数据不能直接进入分析。报告用 Power Query 做了七步清洗,核心动作包括删除重复项和不完整记录、去除面积单位并转数值、楼层拆分为低中高、小区细分为行政区商圈小区、布吉替换为龙岗、地铁字段拆分为地铁线地铁站距离、距离统一转成米。这几步在 Excel 的 Power Query 编辑器里依次操作即可,也可以用 M 语言直接记录处理逻辑。
// Power Query M 语言部分处理逻辑 let 源 = Csv.Document(File.Contents("D:/zu_data.csv"), [Delimiter=",", Encoding=65001]), 删除重复 = Table.Distinct(源), 替换布吉 = Table.ReplaceValue(删除重复, "布吉", "龙岗", Replacer.ReplaceText, {"行政区"}), 拆分地铁 = Table.SplitColumn(替换布吉, "地铁", Splitter.SplitTextByDelimiter(" ", 2), {"地铁线", "地铁站"}), 调整类型 = Table.TransformColumnTypes(拆分地铁, {{"面积", Int64.Type}, {"距离", Int64.Type}}) in 调整类型M 语言的优势是每一步都生成新表,处理链路可以回溯。SplitColumn 按空格拆分地铁字段,第二段里还混着距离文本,实际操作中需要再拆一次或配合正则提取数字。面积字段在源数据里是"85平米"这样的文本,TransformColumnTypes 转数值之前必须先把单位字符替换掉,否则会抛类型转换错误。
2.3 建模字段的取舍逻辑
清洗后的变量说明如下。
| 变量 | 符号 | 变量说明 |
|---|---|---|
| 租金 | price | 数值变量,每月所交的基本房租(元) |
| 租房方式 | rent | 字符型,整租或合租 |
| 电梯 | elevator | 字符型,有或没有 |
| 付款方式 | pay | 字符型,付1押1、付1押2等 |
| 户型 | layout | 字符型,3室1厅1卫、2室1厅1卫等 |
| 面积 | area | 数值变量,大于0的数(平方米) |
| 朝向 | towards | 字符型,朝西、东南等 |
| 楼层 | floor | 字符型,低层、中层或高层 |
| 装修 | decoration | 字符型,毛坯、简单装修、精装修、豪华装修 |
| 类型 | style | 字符型,普通住宅、平房、公寓、别墅或其他 |
| 小区 | community | 字符型,岸芷汀兰、金海花园等 |
| 行政区 | district | 字符型,南山、福田、罗湖、宝安、龙岗、龙华 |
| 商圈 | business_circle | 字符型,科技园、海岸城等 |
| 地铁线 | subway_line | 字符型,2号线、5号线等 |
| 地铁站 | subway_station | 字符型,科苑站、深大站等 |
| 距离 | distance | 数值型,到最近地铁站距离(米) |
建模前剔除了四个字段。标题和访问链接只是爬虫附带信息,与租金成因无关;小区有 3000 多个不同取值,按虚拟变量处理会让特征矩阵膨胀到三千多列,而且新样本里的小区未必在训练集中出现过,泛化能力差;户型与面积高度相关,保留面积即可;付款方式本质是押金担保,对租金影响微弱。这个取舍思路在真实项目里同样适用:特征不是越多越好,要先考虑取值数量和业务逻辑。
3. 可视化的探索与陷阱
3.1 房屋类型与行政区的租金分层
用 Excel 先画不同房屋类型的租金均价条形图。结果显示别墅均价 38402 元,远远超过其他类型;排在后面的依次是平房、其他、普通住宅和公寓。公寓价格垫底有些反常识,原因是公寓中合租占比高,拉低了整体均价。为了贴近毕业生和普通上班族的租房场景,后续分析排除了别墅,并限制租金在 800~8000 元区间,这样可视化结论不会被极端值带偏。
行政区维度的条形图也能看出明显梯度。
| 行政区 | 租金均价(元) |
|---|---|
| 南山 | 2705 |
| 福田 | 2372 |
| 罗湖 | 2302 |
| 宝安 | 2169 |
| 龙华 | 2049 |
| 龙岗 | 1708 |
南山比龙岗贵接近 1000 元,跟科技园和互联网公司分布直接相关。数据里不包含盐田、坪山和光明,原因是这三个区离地铁站较远,安居客在挂牌时没有标注与地铁站的距离信息,爬虫拿不到完整字段。做数据分析时遇到这种结构性缺失,要明确记录缺失原因,否则后续模型会引入偏差。
3.2 高德 API 地理编码与 Tableau 地图
小区粒度的租金排行容易受个别高端盘影响,报告用高德地图 API 把小区名称转成经纬度,再导入 Tableau 绘制气泡地图。颜色映射行政区,气泡大小映射小区租金均价,一张图就能看出深圳各片区租金的分布格局。
import requests import time def geocode(community, city="深圳"): url = "https://restapi.amap.com/v3/geocode/geo" params = { "key": "your_amap_key", "address": community, "city": city, } resp = requests.get(url, params=params, timeout=5) data = resp.json() if data["status"] == "1" and data["geocodes"]: location = data["geocodes"][0]["location"] # 返回格式为 "lng,lat" lng, lat = location.split(",") return float(lng), float(lat) return None, None communities = df["小区"].drop_duplicates().tolist() coords = {} for c in communities: coords[c] = geocode(c) time.sleep(0.2) # 控制请求频率,避免触发 QPS 限制地理编码有两个常见坑。第一,key 需要在高德开放平台申请,个人开发者默认配额有限,批量转换时必须在循环里加 sleep 控制节奏;第二,小区名重复率高,不同行政区可能都有"海景花园",只按名称编码会产生漂移,稳妥做法是把"行政区+小区名"拼接后再请求。单次请求限时 5 秒,网络抖动时抛异常会导致某个小区经纬度缺失,所以函数里要处理返回空值的情况,后续在 Tableau 中过滤缺失坐标即可。
气泡图比单纯看表格信息密度高得多,适合放进项目汇报或数据分析看板,这也是为什么可视化类工具在这类场景中特别受欢迎。
3.3 租房三要素与数值变量的可视化陷阱
租房三要素的条形图结果显示:整租均价 2850 元,合租 1367 元,整租是合租的两倍多;有电梯均价 2629 元,无电梯 2195 元;高层 3121 元,中层 2436 元,低层 2248 元。三个变量指向一致:居住品质越高,租金越贵。
但单变量对比存在一个本质问题:没有控制其他因素。高层的房子可能同时面积更大、装修更好、离地铁更近,它的高租金不能全部归因于楼层。面积与租金的关系在组合图中出现波动,10~40 平方米区间房屋占比高、均价稳步上升,超出这个区间后频数太低,均价走势抖动剧烈。更反常的是距离与租金的关系,居然出现"越远越贵"的走势。这不是理论错了,而是数据可视化在单维分析时天然无法控制混杂变量。要回答"每个因素到底贡献多少租金",必须进入回归建模阶段。
4. 多元线性回归、KNN 异常值与 Lasso 特征选择
4.1 模型设定与评价指标
根据前面的变量筛选,模型以租金 price 为因变量,以 rent、elevator、area、towards、floor、decoration、style、district、business_circle、subway_line、subway_station、distance 为自变量。多元线性回归的矩阵形式为 y = Xβ + ε,最小二乘估计量为 b = (X'X)⁻¹X'y。当分类变量较多时,X 的维度会迅速膨胀,这也是后续要用 Lasso 压缩特征的原因。
评价回归模型时,常用指标如下。
| 指标 | 含义 | 用途 |
|---|---|---|
| MAE | 真实值与估计值差值的平均 | 直观反映平均偏差 |
| MSE | 差值平方的平均 | 放大较大误差 |
| RMSE | MSE 开方 | 单位与因变量一致,跨模型可比 |
| R² | 1 - RSS/TSS | 自变量解释因变量变异的比例 |
| 调整 R² | 对特征数量加惩罚 | 比较不同特征数量的模型 |
用 sklearn 实现回归的代码结构如下。
from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score import pandas as pd df = pd.read_csv("data/clean_rent.csv", encoding="utf-8") X = df.drop(columns=["price", "title", "link", "pay", "layout", "community"]) y = df["price"] # 分类变量做独热编码,drop="first" 避免完全共线性 X = pd.get_dummies(X, drop_first=True) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = LinearRegression() model.fit(X_train, y_train) y_pred = model.predict(X_test) rmse = mean_squared_error(y_test, y_pred, squared=False) print(f"RMSE = {rmse:.1f}, R2 = {r2_score(y_test, y_pred):.3f}")get_dummies 把每个分类变量拆成若干 0/1 列,drop_first 去掉每个变量的第一个类别作为参照组,这样 X 矩阵列满秩,回归系数才有唯一解。random_state 固定后多次运行结果一致,方便复现。报告第一次运行得到 RMSE=936.7,R²=0.701,调整 R²=0.685,特征能解释约七成租金变异,但仍有提升空间。
4.2 用 KNN 距离检测并删除异常值
数据里存在"面积很大但租金很低"这类样本,会直接拉偏回归平面。KNN 检测异常值的思路很直接:异常点远离大部分正常点,它与最近 K 个邻居的平均距离必然偏大。计算出每个样本的 K 近邻平均距离后,用分位数法设定阈值,超过 Q3 + 1.5×IQR 的点判为异常值。
import numpy as np from sklearn.neighbors import NearestNeighbors def detect_outliers(features, k=5, multiplier=1.5): nn = NearestNeighbors(n_neighbors=k + 1) nn.fit(features) dist, _ = nn.kneighbors(features) avg_dist = dist[:, 1:].mean(axis=1) # 第一个点是自身,距离为0,跳过 q1, q3 = np.percentile(avg_dist, [25, 75]) iqr = q3 - q1 threshold = q3 + multiplier * iqr return np.where(avg_dist > threshold)[0] outlier_idx = detect_outliers(X_train[["area", "distance"]].values) X_train_clean = X_train.drop(index=outlier_idx) y_train_clean = y_train.drop(index=outlier_idx)k 和 multiplier 是两个关键参数。k 太小,平均距离受局部密度影响大,容易误伤正常样本;k 太大,异常点会被周围的正常点稀释,检测不出极端值。一般取 5~10。multiplier 沿用箱线图的 1.5 倍 IQR,想更保守可以取 3。因为租金模型主要依赖面积和距离两个连续变量,检测时只取这两列计算 KNN 距离,虚拟变量参与计算意义不大。
提示:删除异常值前先观察分布直方图,样本量小于一万时 KNN 检测效果不稳定,建议结合业务规则交叉验证。
删除异常值后重新拟合,RMSE 降到 655.6,R² 升到 0.818。但此时特征数有 330 个,虚拟变量过多既容易过拟合,又存在多重共线性,部分回归系数符号与实际相反,朝北的租金反而比朝南贵,这明显不合理。
4.3 Lasso 回归压缩特征
Lasso 在损失函数中加入 L1 范数惩罚项 λ‖ω‖₁,可以把不重要的回归系数压缩到 0,天然适合做特征选择。
from sklearn.linear_model import Lasso from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler # 连续变量标准化,让惩罚对每个特征公平 lasso_pipeline = make_pipeline( StandardScaler(), Lasso(alpha=0.001, max_iter=100000) ) lasso_pipeline.fit(X_train_clean, y_train_clean) coef = lasso_pipeline.named_steps["lasso"].coef_ active_cols = X_train_clean.columns[coef != 0]alpha 是 Lasso 的惩罚强度。alpha 越大,被压缩到 0 的系数越多,模型越简单,但可能欠拟合;alpha 太小则接近普通线性回归。报告采用 0.001 量级的 alpha 后,特征从 330 个降到 111 个。实际项目里我更推荐用 LassoCV 做交叉验证选择最优 alpha,而不是手调。
Lasso 之后 RMSE=641.6,R²=0.807,调整 R²=0.803。R² 比上一轮略降,但 RMSE 下降说明泛化能力更好。再按 p 值 ≤ 0.1 筛掉不显著变量后重新拟合,RMSE=644.0,R²=0.805,调整 R²=0.802,模型更精简,性能损失很小。
| 阶段 | 特征数 | RMSE | R² | 调整 R² |
|---|---|---|---|---|
| 初始线性回归 | 330 | 936.7 | 0.701 | 0.685 |
| KNN 删除异常值后 | 330 | 655.6 | 0.818 | 0.808 |
| Lasso 筛选后 | 111 | 641.6 | 0.807 | 0.803 |
| p 值 ≤ 0.1 筛选后 | 更少 | 644.0 | 0.805 | 0.802 |
这组对比清晰展示了每一步操作的价值:KNN 异常值处理贡献了最大幅度的 RMSE 下降,Lasso 和 p 值筛选主要是精简模型、控制过拟合。
5. 回归系数的业务解读与改进方向
5.1 看懂系数,回答业务问题
最终模型通过 F 检验,关键回归系数可以直接换算成业务语言。
| 变量 | 系数 | 业务含义 |
|---|---|---|
| 面积 | +55.28 | 面积每增加 1 平方米,月租金平均上涨 55 元 |
| 距离 | -0.058 | 距地铁站每增加 100 米,月租金平均下降 6 元 |
| 整租(参照:合租) | +452.62 | 整租比合租平均贵 453 元 |
| 无电梯(参照:有电梯) | -70.91 | 无电梯比有电梯平均便宜 71 元 |
| 高层(参照:中层) | +46.47 | 高层比中层平均贵 46 元 |
| 宝安(参照:南山) | -838.33 | 宝安比南山平均便宜 838 元 |
| 松岗商圈 | -1104.42 | 松岗比万众城商圈平均便宜 1104 元 |
距离系数是 -0.058,每远 1 米递减不到 6 分钱,但换算成 100 米就是 6 元,500 米就是 30 元,说明地铁距离对租金的影响相对温和。面积是最强的连续变量,55 元的边际效应在 800~8000 元租金区间内占比可观。这些系数可以直接用于房源比价,比如两个房源相差 20 平方米和 500 米距离,粗略估算价差在 20×55 - 5×6 = 1070 元左右。
5.2 高基数分类变量的业务级压缩
回归结果里商圈和地铁站的虚拟变量数量仍然庞大,报告提出了几个改进方向,对实际项目很有借鉴意义:朝向按深圳气候特点划分为优、良、差,朝南、东南为优,朝北为差;小区按总建面积、坐落位置、开发商划分为高档、中档和普通;商圈按商业发展程度划分大、中、小;地铁站按人流量、是否始发站或中转站划分为热门和冷门。这样处理的本质是把高基数分类变量做归并,让模型更简洁、泛化能力更强。业务方有成熟分类体系时优先直接采用,没有的话可以先聚类再打标签。
5.3 用相对误差口径验证租金模型
回归评估通常看 RMSE 和 R²,业务方更关心"预测准不准"。报告定义了一个实用口径:满足 |ŷ - y| / y ≤ α 的样本视为预测有效,统计占比得到准确率 φ = n / m。
| α | 准确率 |
|---|---|
| 0.05 | 12.51% |
| 0.10 | 25.42% |
| 0.15 | 38.97% |
| 0.20 | 49.16% |
| 0.25 | 58.77% |
| 0.30 | 67.49% |
α=0.3 时准确率约 67%,等于面对 1000 元的房源,模型预测结果落在 700~1300 元范围内的概率约三分之二。这个口径比抽象指标更贴近业务沟通,可以直接用作日报或数据大屏上的 KPI。验证时建议按租金分层评估,低价房源的相对误差天然偏大,整租和合租样本也应分别计算准确率,避免单一指标掩盖局部失真。
本文还有配套的精品资源,点击获取