
简介这份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和ThreadPoolExecutorimport 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, headersHEADERS, timeout10) 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(stripTrue) if title else , price: price.get_text(stripTrue) if price else , layout: detail[0].get_text(stripTrue) if len(detail) 0 else , area: detail[1].get_text(stripTrue) if len(detail) 1 else , floor: detail[2].get_text(stripTrue) if len(detail) 2 else , community: detail[3].get_text(stripTrue) if len(detail) 3 else , }) return items except Exception as e: print(f抓取失败 {url}: {e}) return [] def crawl_pages(base_url, max_page50, workers8): 多线程抓取列表页 all_data [] urls [f{base_url}/p{i}/ for i in range(1, max_page 1)] with ThreadPoolExecutor(max_workersworkers) 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_page50) with open(rent_raw.csv, w, newline, encodingutf-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_workers8控制并发线程数太高容易被目标站限流太低速度上不去8 到 12 是比较稳的区间。time.sleep(0.3)放在每次任务完成后用来降低请求频率这不是可选项是保命项。timeout10防止某个请求卡死拖垮整个线程池。解析部分用的是 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, paramsparams, timeout5) 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, indexFalse, encodingutf-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_firstTrue 自动丢弃每个变量的第一个类别作为基本变量 cat_cols [rent, elevator, towards, floor, decoration, style, district, business_circle, subway_line, subway_station] df_encoded pd.get_dummies(df, columnscat_cols, drop_firstTrue) # 定量变量 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_size0.3, random_state42) # 拟合 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_firstTrue是get_dummies的关键参数它自动把每个定性变量的第一个类别作为基本变量丢弃避免完全共线性。sm.add_constant给模型加上截距项不加的话回归线强制过原点结果会偏。random_state42保证每次拆分结果一致方便复现。报告里第一次回归的 RMSE 是 936.7R² 是 0.701。这个结果不算好原因是数据里有异常值比如面积很大但租金很低的记录。接下来报告用 KNN 原理检测并删除了异常值RMSE 降到 655.6R² 升到 0.818。3.3 KNN 异常值检测与 Lasso 特征筛选KNN 检测异常值的思路很朴素正常样本周围应该有很多邻居异常样本周围邻居很少或者很远。具体做法是计算每个样本到它最近 K 个样本的平均距离然后用 IQR 方法设定阈值超过阈值的就判定为异常。from sklearn.neighbors import NearestNeighbors import numpy as np def knn_outlier_detection(X, k5, threshold1.5): 基于 KNN 平均距离和 IQR 检测异常值 nbrs NearestNeighbors(n_neighborsk 1).fit(X) distances, _ nbrs.kneighbors(X) # 取最近 k 个邻居的平均距离排除自身 avg_dist distances[:, 1:].mean(axis1) # 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, k5) df_clean df[mask] print(f删除异常值: {len(df) - len(df_clean)} 条剩余 {len(df_clean)} 条)k5是邻居数太小对噪声敏感太大异常值会被淹没5 到 10 是常用范围。threshold1.5是 IQR 的标准倍数对应正态分布下约 99.3% 的置信区间。StandardScaler标准化是必须的因为面积和距离的量纲差了几个数量级不标准化的话距离计算会被面积主导。删除异常值后报告又用 Lasso 回归做特征筛选。Lasso 的 L1 正则化会把不重要的变量系数压缩到 0从而实现自动特征选择。报告里特征从 330 个降到 111 个RMSE 从 655.6 微降到 641.6R² 从 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(alpha1.0, max_iter10000) lasso.fit(X_scaled, y_train) # 筛选系数不为 0 的特征 selected X_train.columns[lasso.coef_ ! 0] print(fLasso 筛选后剩余特征: {len(selected)} 个)alpha1.0是正则化强度越大筛选越狠。实际调参时可以用交叉验证选最优 alphaLassoCV就是干这个的。max_iter10000防止迭代不收敛。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_firstTrue)自动丢弃每个变量的第一个类别。如果你手动做独热编码记住 k 个类别只保留 k-1 个虚拟变量。另外用model.summary()看有没有「Condition Number」过大的警告过大就说明有共线性。4.4 KNN 异常值检测删太多或删太少现象k5时删了几百条k10时只删了几十条不知道哪个对。原因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普通住宅, modelNone, feature_columnsNone): 基于训练好的回归模型估算租金 参数说明 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 fdistrict_{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, modelmodel, feature_columnsX_train.columns) # print(f预测租金: {pred:.0f} 元/月)这个函数的核心逻辑是先构造一个所有虚拟变量为 0 的输入向量对应所有基本变量然后根据传入的参数把对应的虚拟变量置为 1最后用训练好的模型预测。feature_columns是训练时X_train.columns的顺序必须严格一致否则预测结果会错位。这个工具的价值在于它把一份静态的实验报告变成了一个可以随时调用的估价器。你可以在看房前输入几个关键参数得到一个参考价再和中介报价对比。如果报价比预测值高出 30% 以上要么是房源有特殊优势比如全新装修、带家具要么就是报价虚高。不过要提醒一句这个模型的 R² 是 0.805意味着它只能解释租金变异的 80.5%还有约 20% 的变异来自模型没纳入的因素比如房屋的具体装修质量、采光、噪音、小区物业管理水平等。所以它适合做快速参考不适合做精确谈判依据。从那以后我每次拿到一份数据分析报告都会先问自己三个问题数据从哪来、清洗做了什么、模型假设是什么。这三个问题答不上来后面的可视化再漂亮也是空中楼阁。这份安居客租房实验报告在这三点上都交代得比较清楚这也是我愿意花时间拆它的原因。希望帮到你。本文还有配套的精品资源点击获取