☰
深圳租房数据分析实战:Python爬虫、Power Query清洗与回归建模
2026/10/12 1:20:11 网站建设 项目流程

简介:这份PDF实验报告面向数据分析初学者与需要完成课程设计的学生,以深圳租房市场为案例,完整演示从数据采集到建模的Python数据分析与可视化流程。资源包共1个PDF文件,约912KB,内容涵盖爬虫实现、数据清洗、图表绘制与回归建模等模块,适合作为数据分析课程的参考范例或自学练手项目。报告基于安居客平台爬取的10000余条租房信息,对比了单线程、多线程与Scrapy三种爬虫方案,并借助Power Query完成去重、单位转换、楼层与地铁信息拆分等预处理。可视化部分使用Excel与Tableau呈现房屋类型、行政区及小区租金均价差异,并绘制地图展示空间分布。建模环节采用多元线性回归,结合KNN异常值检测与Lasso变量筛选,量化了面积、地铁距离、租房方式、电梯与楼层对租金的影响。目前已有2546人学习,可为毕业生或预算有限的租房人群提供选房参考,也为读者提供一套可复用的数据分析与可视化示例。

1. 深圳租房数据怎么挖:一份能跑通的实验报告拆解

毕业季一到,深圳的租房市场就进入白热化。同一个小区,朝南和朝北差几百块;离地铁站多走两百米,租金可能就掉一档。这些直觉判断到底对不对、能差多少,光靠中介一张嘴说不清楚。这份《安居客租房数据分析及可视化实验报告》干的就是把直觉变成数字的事:用 Python 爬下深圳 10000 多条租房信息,经 Power Query 清洗后,用 Excel 和 Tableau 做可视化,最后用多元线性回归把租金拆解成面积、租房方式、电梯、楼层、行政区、地铁距离等变量的函数。它适合两类人:一类是想照着复现一套「爬取—清洗—可视化—建模」完整链路的 Python 数据方向学习者,另一类是关心深圳租金定价逻辑、想拿数据支撑租房决策的从业者。下面我按自己拆项目的习惯,把这份报告里真正能落地的部分拎出来讲透。

2. 数据获取与清洗:三种爬虫选型与 Power Query 七个清洗步骤

2.1 单线程、多线程、Scrapy 到底怎么选

报告里对三种爬虫方式做了对比,这个对比不是凑字数,它直接决定你后面能不能在合理时间内拿到一万条数据。单线程爬虫就是最朴素的requests加循环,写起来十分钟,跑起来一整天。它的瓶颈在于每发一个请求都要等服务器响应,CPU 大部分时间在空转。多线程爬虫用ThreadPoolExecutor把请求并发出去,速度能提上来,但 Python 的 GIL 决定了它只对 IO 密集型任务有效——爬虫恰好就是 IO 密集型。Scrapy 框架则是把并发、去重、重试、中间件全部封装好了,代价是学习曲线陡,你得理解它的引擎、调度器、下载器、Spider 和 Pipeline 五个组件怎么串起来。

我的建议是:如果你只是复现这份报告、拿一万条数据,多线程爬虫是性价比最高的选择。Scrapy 适合你要长期维护、要爬几十万条、要处理反爬策略的场景。单线程只适合调试阶段验证解析逻辑。

下面是一个多线程爬虫的骨架,基于requests和ThreadPoolExecutor:

import requests from concurrent.futures import ThreadPoolExecutor, as_completed from bs4 import BeautifulSoup import csv import time 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_page(url): """抓取单页并解析出租房列表""" try: resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") items = [] for card in soup.select(".zu-itemmod"): title = card.select_one(".zu-info h3 a") price = card.select_one(".zu-side strong") detail = card.select(".details-item") items.append({ "title": title.get_text(strip=True) if title else "", "price": price.get_text(strip=True) if price else "", "layout": detail[0].get_text(strip=True) if len(detail) > 0 else "", "area": detail[1].get_text(strip=True) if len(detail) > 1 else "", "floor": detail[2].get_text(strip=True) if len(detail) > 2 else "", "community": detail[3].get_text(strip=True) if len(detail) > 3 else "", }) return items except Exception as e: print(f"抓取失败 {url}: {e}") return [] def crawl_pages(base_url, max_page=50, workers=8): """多线程抓取列表页""" all_data = [] urls = [f"{base_url}/p{i}/" for i in range(1, max_page + 1)] with ThreadPoolExecutor(max_workers=workers) as executor: futures = {executor.submit(fetch_page, u): u for u in urls} for future in as_completed(futures): result = future.result() all_data.extend(result) time.sleep(0.3) # 控制频率,避免触发封禁 return all_data if __name__ == "__main__": data = crawl_pages("https://sz.anjuke.com/rent/", max_page=50) with open("rent_raw.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title", "price", "layout", "area", "floor", "community"]) writer.writeheader() writer.writerows(data) print(f"共抓取 {len(data)} 条记录")

这段代码的关键参数有三个。max_workers=8控制并发线程数,太高容易被目标站限流,太低速度上不去,8 到 12 是比较稳的区间。time.sleep(0.3)放在每次任务完成后,用来降低请求频率,这不是可选项,是保命项。timeout=10防止某个请求卡死拖垮整个线程池。解析部分用的是 BeautifulSoup 的 CSS 选择器,实际页面结构会变,你需要打开开发者工具确认.zu-itemmod这类类名是否还对得上。

2.2 Power Query 清洗的七个步骤拆解

拿到原始 CSV 之后,报告里用 Power Query 做了七步清洗。很多人觉得清洗就是删删改改,其实这七步每一步都在为后面的建模铺路。我按自己的理解重新拆一遍。

第一步,删除重复项和不完整记录。重复项通常是翻页时产生的,不完整记录是某些字段为空。这两步做完,数据量可能从一万多降到八九千,这是正常的。

第二步,去除面积单位并转数值。原始数据里面积是「89㎡」这种格式,你需要用「替换值」把「㎡」替换成空,再把列类型改成小数。这一步不做,后面回归模型里面积就是个字符串,直接报错。

第三步,楼层分低中高。原始数据是「低层(共30层)」「中层(共18层)」这种,用「提取」功能取括号前的文字,再映射成低、中、高三个类别。报告里就是这么处理的,后面回归系数里楼层_高层能算出 +46 元,靠的就是这一步。

第四步,小区拆成行政区、商圈、小区三级。原始数据里小区字段可能是「南山-科技园-岸芷汀兰」这种格式,用「拆分列」按分隔符拆开。这一步的价值在于,后面你可以按行政区做可视化,也可以把商圈作为回归模型的自变量。

第五步,布吉替换成龙岗。这是数据源本身的问题,安居客把布吉单独列了一个行政区,但实际上布吉属于龙岗区。如果不做这个替换,回归模型里会多出一个只有几百条记录的行政区虚拟变量,系数不稳定。

第六步,地铁拆成地铁线、地铁站、距离。原始数据可能是「2号线-科苑站-步行500米」,同样用拆分列处理。距离要转成以米为单位的数值,后面回归系数里距离的系数是 -0.058,意思是每远 100 米租金少 5.8 元。

第七步,距离转数值。这一步单独拎出来说,是因为「步行500米」「步行1.2公里」这种格式不统一,你需要先统一单位再转数值。报告里最终距离的系数是显著的,说明这一步做对了。

提示:Power Query 的每一步操作都会生成 M 代码,你可以在「高级编辑器」里看到完整脚本。如果你要反复跑这套流程,建议把 M 代码导出保存,下次直接粘贴就能复用,不用重新点一遍。

3. 可视化与建模:从条形图到 Lasso 回归的完整链路

3.1 Excel 和 Tableau 各自该画什么图

报告里用了 Excel 和 Tableau 两个工具做可视化,这不是重复劳动,两者分工不同。Excel 适合快速出统计图,比如不同房屋类型的租金均价条形图、各行政区租金对比图。Tableau 适合做交互式仪表盘和地图,比如各小区租金分布地图。

先说 Excel 部分。报告里画了不同房屋类型的租金均价,结果别墅以 38402 元遥遥领先,公寓反而垫底只有 1993 元。这个结果乍看反直觉,但报告给了解释:公寓多数是合租房,其他类型多数是整租房,所以均价被拉低了。这就是可视化的价值——它让你看到反直觉的现象,然后逼你去想背后的原因。

再看 Tableau 部分。报告用高德地图 API 获取了各小区的经纬度,然后在 Tableau 里画了气泡地图,颜色表示行政区,气泡大小表示租金高低。这个图的做法是:先用 Python 调高德 API 拿到经纬度,存成 CSV,然后在 Tableau 里把经纬度字段拖到行和列,把行政区拖到颜色,把租金拖到大小。

import requests import pandas as pd def geocode(address, api_key): """调用高德地理编码 API 获取经纬度""" url = "https://restapi.amap.com/v3/geocode/geo" params = {"key": api_key, "address": address, "city": "深圳"} resp = requests.get(url, params=params, timeout=5) data = resp.json() if data["status"] == "1" and data["geocodes"]: loc = data["geocodes"][0]["location"] lng, lat = loc.split(",") return float(lng), float(lat) return None, None # 读取小区列表,逐个获取经纬度 df = pd.read_csv("communities.csv") df["lng"], df["lat"] = zip(*df["name"].apply(lambda x: geocode(x, "你的高德API_KEY"))) df.to_csv("communities_geo.csv", index=False, encoding="utf-8-sig")

这段代码的关键在于高德 API 的 key 需要你自己申请,免费额度每天够用。city参数限定深圳,避免同名小区匹配到其他城市。返回的location格式是「经度,纬度」,注意顺序别搞反。

3.2 多元线性回归的变量设定与虚拟变量处理

报告里的回归模型以租金为因变量,自变量包括面积、距离两个定量变量,以及租房方式、电梯、朝向、楼层、装修、类型、行政区、商圈、地铁线、地铁站十个定性变量。定性变量要转成虚拟变量才能进模型,这是多元线性回归的基本操作。

虚拟变量的处理有个坑:如果你有 k 个类别,只需要 k-1 个虚拟变量,否则会陷入「虚拟变量陷阱」,导致完全共线性。报告里明确说了,租房方式以合租为基本变量,电梯以有为基本变量,朝向以东北为基本变量,楼层以中层为基本变量,装修以毛坯为基本变量,类型以公寓为基本变量,行政区以南山为基本变量,商圈以万众城为基本变量,地铁线以11号线为基本变量,地铁站以上塘站为基本变量。这意味着所有回归系数的解释都是相对于这些基本变量的。

用 Python 的statsmodels做回归,代码大致如下:

import pandas as pd import statsmodels.api as sm # 读取清洗后的数据 df = pd.read_csv("rent_clean.csv") # 对定性变量做独热编码,drop_first=True 自动丢弃每个变量的第一个类别作为基本变量 cat_cols = ["rent", "elevator", "towards", "floor", "decoration", "style", "district", "business_circle", "subway_line", "subway_station"] df_encoded = pd.get_dummies(df, columns=cat_cols, drop_first=True) # 定量变量 num_cols = ["area", "distance"] X = df_encoded[num_cols + [c for c in df_encoded.columns if c not in num_cols + ["price"]]] X = sm.add_constant(X) y = df_encoded["price"] # 训练集测试集拆分 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) # 拟合 OLS 模型 model = sm.OLS(y_train, X_train).fit() print(model.summary()) # 在测试集上预测并计算 RMSE y_pred = model.predict(X_test) from sklearn.metrics import mean_squared_error import numpy as np rmse = np.sqrt(mean_squared_error(y_test, y_pred)) print(f"测试集 RMSE: {rmse:.1f}")

drop_first=True是get_dummies的关键参数,它自动把每个定性变量的第一个类别作为基本变量丢弃,避免完全共线性。sm.add_constant给模型加上截距项,不加的话回归线强制过原点,结果会偏。random_state=42保证每次拆分结果一致,方便复现。

报告里第一次回归的 RMSE 是 936.7,R² 是 0.701。这个结果不算好,原因是数据里有异常值,比如面积很大但租金很低的记录。接下来报告用 KNN 原理检测并删除了异常值,RMSE 降到 655.6,R² 升到 0.818。

3.3 KNN 异常值检测与 Lasso 特征筛选

KNN 检测异常值的思路很朴素:正常样本周围应该有很多邻居,异常样本周围邻居很少或者很远。具体做法是计算每个样本到它最近 K 个样本的平均距离,然后用 IQR 方法设定阈值,超过阈值的就判定为异常。

from sklearn.neighbors import NearestNeighbors import numpy as np def knn_outlier_detection(X, k=5, threshold=1.5): """基于 KNN 平均距离和 IQR 检测异常值""" nbrs = NearestNeighbors(n_neighbors=k + 1).fit(X) distances, _ = nbrs.kneighbors(X) # 取最近 k 个邻居的平均距离(排除自身) avg_dist = distances[:, 1:].mean(axis=1) # IQR 阈值 q1, q3 = np.percentile(avg_dist, [25, 75]) iqr = q3 - q1 upper = q3 + threshold * iqr mask = avg_dist <= upper return mask # 对定量变量做异常值检测 from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_num = scaler.fit_transform(df[["area", "distance", "price"]]) mask = knn_outlier_detection(X_num, k=5) df_clean = df[mask] print(f"删除异常值: {len(df) - len(df_clean)} 条,剩余 {len(df_clean)} 条")

k=5是邻居数,太小对噪声敏感,太大异常值会被淹没,5 到 10 是常用范围。threshold=1.5是 IQR 的标准倍数,对应正态分布下约 99.3% 的置信区间。StandardScaler标准化是必须的,因为面积和距离的量纲差了几个数量级,不标准化的话距离计算会被面积主导。

删除异常值后,报告又用 Lasso 回归做特征筛选。Lasso 的 L1 正则化会把不重要的变量系数压缩到 0,从而实现自动特征选择。报告里特征从 330 个降到 111 个,RMSE 从 655.6 微降到 641.6,R² 从 0.818 微降到 0.807。R² 降了一点,但模型复杂度大幅降低,这是值得的。

from sklearn.linear_model import Lasso from sklearn.preprocessing import StandardScaler # 标准化特征 scaler = StandardScaler() X_scaled = scaler.fit_transform(X_train) # Lasso 回归,alpha 控制正则化强度 lasso = Lasso(alpha=1.0, max_iter=10000) lasso.fit(X_scaled, y_train) # 筛选系数不为 0 的特征 selected = X_train.columns[lasso.coef_ != 0] print(f"Lasso 筛选后剩余特征: {len(selected)} 个")

alpha=1.0是正则化强度,越大筛选越狠。实际调参时可以用交叉验证选最优 alpha,LassoCV就是干这个的。max_iter=10000防止迭代不收敛。

3.4 回归系数的业务解读

报告最终给出的回归系数里,有几个特别值得关注。面积系数 55.28,意思是其他条件不变,面积每多 1 平方米,月租金平均多 55 元。距离系数 -0.058,意思是离地铁站每远 100 米,月租金平均少 5.8 元。租房方式_整租系数 452.62,意思是整租比合租平均贵 452 元。电梯_没有系数 -70.91,意思是没电梯比有电梯平均便宜 71 元。楼层_高层系数 46.47,意思是高层比中层平均贵 46 元。行政区_宝安系数 -838.33,意思是宝安比南山平均便宜 838 元。

这些数字和直觉基本吻合,但有几个点需要注意。距离的系数是 -0.058,看起来很小,但乘以 1000 米就是 -58 元,乘以 2000 米就是 -116 元,累积效应不小。行政区_龙岗的系数是 -655.66,比宝安的 -838.33 还要高一些,说明龙岗的租金水平相对宝安更接近南山,这和龙岗中心城的发展有关。

报告还给出了不同 α 值下的预测准确率。当 α=0.3 时,也就是预测租金在真实租金 ±30% 范围内算准确,准确率是 67.49%。当 α=0.5 时,准确率跳到 88.57%。这意味着模型对租金的预测大致能落在真实值的一半到一倍之间,对于快速估价够用了,但精确到几百块以内还做不到。

4. 避坑与排查:这份实验报告里没写但你会遇到的问题

4.1 爬虫被封 IP 或返回空数据

现象:跑了几十页之后,requests.get返回 403 或者返回的 HTML 里没有租房列表。

原因:目标站检测到你的请求频率过高,或者识别出你是爬虫。常见触发条件是请求间隔太短、User-Agent 是默认的python-requests、没有携带 Referer。

解决:第一,在请求头里加上真实的 User-Agent 和 Referer。第二,每次请求后time.sleep随机 0.5 到 1.5 秒,不要用固定值。第三,如果还是被封,用代理池轮换 IP,但注意代理质量参差不齐,免费代理基本不能用。第四,降低并发数,max_workers从 8 降到 4 试试。

4.2 Power Query 拆分列后数据类型错乱

现象:拆分完小区字段后,行政区列里出现了「南山」「福田」这样的文本,但商圈列里混进了数字,或者距离列转数值时全部变成 null。

原因:原始数据的分隔符不统一,有的用「-」,有的用「|」,有的用空格。另外,距离字段里可能有「暂无数据」这样的文本,转数值时直接报错。

解决:在拆分之前,先用「替换值」把各种分隔符统一成一种。对于距离字段,先用「条件列」把「暂无数据」替换成 null,再转数值。转数值时如果还有报错,用「替换错误」功能把错误值替换成 null。

4.3 虚拟变量陷阱导致回归结果完全相反

现象:回归跑出来,面积的系数是负数,意思是面积越大租金越低,这明显违背常识。

原因:定性变量做独热编码时没有丢弃基本变量,导致完全共线性。statsmodels在这种情况下会给出警告,但如果你忽略了警告,系数就会乱掉。

解决:用pd.get_dummies(drop_first=True)自动丢弃每个变量的第一个类别。如果你手动做独热编码,记住 k 个类别只保留 k-1 个虚拟变量。另外,用model.summary()看有没有「Condition Number」过大的警告,过大就说明有共线性。

4.4 KNN 异常值检测删太多或删太少

现象:k=5时删了几百条,k=10时只删了几十条,不知道哪个对。

原因:KNN 异常值检测对 k 值敏感,k 太小会把正常但稀疏的样本误判为异常,k 太大则检测不出真正的异常。

解决:不要只用一个 k 值,用 3、5、7、10 分别跑一遍,看删除比例是否稳定。如果删除比例在 5% 到 15% 之间波动,说明数据质量还行。如果某个 k 值下删除比例突然跳到 30% 以上,说明这个 k 值不合适。另外,IQR 的 threshold 也可以调,1.5 是标准值,3.0 是宽松值,先用 1.5 跑,如果删太多再放宽。

4.5 Lasso 的 alpha 选太大导致重要变量被误删

现象:Lasso 跑完,面积这个明显重要的变量系数变成了 0。

原因:alpha 太大,正则化惩罚过强,把重要变量也压缩掉了。

解决:用LassoCV做交叉验证选 alpha,而不是手动拍一个值。LassoCV会自动在多个 alpha 里选交叉验证误差最小的那个。另外,Lasso 之前一定要标准化特征,否则量纲大的变量会被过度惩罚。

5. 进阶技巧:把回归模型变成可复用的租金估价工具

报告里的模型最终是以实验报告的形式呈现的,但如果你想让它的价值再往前走一步,可以把它封装成一个简单的租金估价函数。输入面积、距离、租房方式、电梯、楼层、行政区等参数,输出预测租金。这样你下次看房的时候,可以快速判断中介报价是否离谱。

import numpy as np import pandas as pd def estimate_rent(area, distance, rent_type="整租", elevator="有", floor="中层", district="南山", decoration="精装修", style="普通住宅", model=None, feature_columns=None): """ 基于训练好的回归模型估算租金 参数说明: area: 面积(平方米) distance: 距地铁站距离(米) rent_type: 整租或合租 elevator: 有或没有 floor: 低层、中层或高层 district: 行政区 decoration: 装修情况 style: 房屋类型 """ # 构造输入向量,默认所有虚拟变量为 0(即基本变量) input_dict = {"const": 1.0, "area": area, "distance": distance} for col in feature_columns: if col in ["const", "area", "distance"]: continue input_dict[col] = 0.0 # 设置对应的虚拟变量为 1 if rent_type == "整租": input_dict["rent_整租"] = 1.0 if elevator == "没有": input_dict["elevator_没有"] = 1.0 if floor == "高层": input_dict["floor_高层"] = 1.0 elif floor == "低层": input_dict["floor_低层"] = 1.0 if district != "南山": key = f"district_{district}" if key in input_dict: input_dict[key] = 1.0 if decoration == "简单装修": input_dict["decoration_简单装修"] = 1.0 elif decoration == "豪华装修": input_dict["decoration_豪华装修"] = 1.0 if style == "普通住宅": input_dict["style_普通住宅"] = 1.0 elif style == "平房": input_dict["style_平房"] = 1.0 # 按特征列顺序构造数组 X_input = np.array([input_dict.get(col, 0.0) for col in feature_columns]) prediction = model.predict(X_input.reshape(1, -1))[0] return prediction # 示例:估算南山一套 60 平米、距地铁 500 米的整租普通住宅 # pred = estimate_rent(60, 500, model=model, feature_columns=X_train.columns) # print(f"预测租金: {pred:.0f} 元/月")

这个函数的核心逻辑是:先构造一个所有虚拟变量为 0 的输入向量(对应所有基本变量),然后根据传入的参数把对应的虚拟变量置为 1,最后用训练好的模型预测。feature_columns是训练时X_train.columns的顺序,必须严格一致,否则预测结果会错位。

这个工具的价值在于,它把一份静态的实验报告变成了一个可以随时调用的估价器。你可以在看房前输入几个关键参数,得到一个参考价,再和中介报价对比。如果报价比预测值高出 30% 以上,要么是房源有特殊优势(比如全新装修、带家具),要么就是报价虚高。

不过要提醒一句,这个模型的 R² 是 0.805,意味着它只能解释租金变异的 80.5%,还有约 20% 的变异来自模型没纳入的因素,比如房屋的具体装修质量、采光、噪音、小区物业管理水平等。所以它适合做快速参考,不适合做精确谈判依据。

从那以后我每次拿到一份数据分析报告,都会先问自己三个问题:数据从哪来、清洗做了什么、模型假设是什么。这三个问题答不上来,后面的可视化再漂亮也是空中楼阁。这份安居客租房实验报告在这三点上都交代得比较清楚,这也是我愿意花时间拆它的原因。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询