
简介一份以深圳为研究样本的安居客租房数据分析与可视化实验报告适合正在学习Python爬虫、数据图表分析与回归建模的读者。报告从安居客网站获取10000余条真实租房信息比较了单线程、多线程和Scrapy三种爬虫实现方式的优缺点之后用Power Query完成删除重复项、处理缺失记录、拆分地铁与小区字段、转换面积单位等数据清洗工作再用Excel和Tableau绘制不同房屋类型、行政区、小区的房租均价图表直观呈现各地段租金差异。在建模环节报告构建了以面积、租房方式、房屋类型等为自变量的多元线性回归模型通过KNN原理检测并剔除异常值用Lasso回归筛选影响因素最终量化得到面积每增加1平方米租金平均上涨55元、距地铁站每增加100米租金下降6元、整租比合租平均贵453元等结论对毕业生或收入有限人群的租房决策有实际参考价值。整个资源为单个PDF文件大小约912KB已有2546人学习。1. 把安居客租房数据拆成一份可复现的分析报告爬虫、Tableau 可视化与多元回归每到毕业季租房就是绕不开的话题。你可能在安居客上翻过几百套房源心里大概知道“南山贵、龙岗便宜、面积大价格高”但很少有人把这种感觉量化成系数。这份实验报告做了一件很实在的事用 Python 爬下深圳 10000 多条租房信息经过 Power Query 清洗、Excel 和 Tableau 可视化再建立多元线性回归模型最终得出“面积每增加 1 平米租金涨 55 元”“离地铁远 100 米租金降 6 元”这类可量化的结论。它不是那种只贴几张图表的演示文稿而是完整走完了“数据获取→清洗→可视化→建模→异常值处理→特征筛选”全流程对想练手 Python 爬虫、数据可视化或者入门回归分析的人都很友好。这篇文章不会泛泛介绍报告内容而是把它拆成你能照着复现的步骤、参数和踩坑记录。2. 数据获取与清洗单线程、多线程与 Scrapy 的选择逻辑2.1 三种爬虫方式的适用场景报告里用了三种方式实现爬虫单线程、多线程和 Scrapy 框架。这三者不是简单的“哪个快用哪个”而是对应不同阶段的真实需求。单线程爬虫适合用来摸清页面结构。第一次写爬虫时你并不知道安居客的列表页和详情页到底有哪些字段先用 requests 加 BeautifulSoup 跑一个单线程脚本把标题、租房方式、电梯、地铁、租金、付款方式、户型、面积、朝向、楼层、装修、类型、小区这些字段逐个打印出来确认选择器没写错再考虑提速。报告里提到的字段其实很有讲究标题和访问链接在后续分析中没用但爬虫阶段必须保留因为详情页里才有完整的户型、朝向、装修信息列表页只有部分摘要。多线程爬虫是第二步。当你确认字段能正确解析后用 concurrent.futures.ThreadPoolExecutor 或者 threading 模块把请求并发出去能把采集速度提升几倍。但要注意多线程在 IO 密集型任务上确实快可线程锁和队列管理会增加代码复杂度特别是往 CSV 或数据库写结果时容易重复写或漏写。报告里总结的“线程加锁解锁消耗资源”就是这么来的。Scrapy 是第三步也是我最推荐的生产级方案。它自带请求调度、并发控制、去重、中间件和 Item Pipeline不需要自己管理线程池。缺点也很明显上手曲线比 requests 陡中间件、Spider、Pipeline 的概念一开始容易绕晕。适合的目标是你要爬的页面数量大、需要断点续爬、后续可能要扩展爬虫到其他城市或平台。2.2 Power Query 清洗细节与字段说明数据爬下来了但距离能建模还很远。报告里的清洗步骤看似简单每条都是实用经验1. 删除重复项、删除信息收集不完整的记录 2. 去除面积的单位并将文本转换成数值 3. 将楼层分为低、中、高三种情况 4. 将小区细分为行政区、商圈、小区 5. 将行政区为布吉的替换成龙岗 6. 将地铁细分为地铁线、地铁站、距离 7. 将距离转化成以米为单位的数值删除重复项不用多说但要注意“重复”的定义同一条房源链接重复出现是重复不同链接但房源标题和面积完全一致也可能是重复。报告没有细说判断标准我的习惯是以链接为唯一键去重再做一次标题加面积的复合去重。缺失记录的处理要看比例如果某个字段缺失超过 30%直接删掉整列可能更合理如果只是零星缺失删除整行也不影响样本量。面积的单位去除是个容易翻车的点。安居客页面里面积后面跟着“平米”或“㎡”如果你只做 replace 空格可能漏掉全角字符或特殊空格。稳妥做法是用正则提取数字部分再强制转 float。楼层分档也有学问。报告把楼层分为低、中、高三种很多新手会直接按 1-3 层低、4-6 层中、7 层以上高来分但深圳城中村和公寓楼的实际层高差别很大。更合理的做法是看总楼层数用相对位置划分比如前三分之一为低层中间为中层后三分之一为高层。不过报告没有采用这种方式它的回归结果里“高层比中层房租平均多 46 元”是在它自己的分档定义下成立的。行政区替换布吉为龙岗这是数据质量修复的典型案例。安居客的字段里“布吉”是历史行政区划后来已经并入龙岗区如果不做替换模型里就会多出一个只有少量样本的虚拟变量既增加特征维度又让系数解释变得混乱。这里报告的处理是正确的。地铁字段拆分是建模的关键步骤。原始字段可能是“1号线 深大站 200米”这种混合文本需要用正则或 split 拆成三个独立字段subway_line、subway_station、distance。拆分后距离要统一转成以米为单位的数值因为原始数据里可能有的记录是“0.2公里”有的是“200米”不统一会让距离系数完全失真。最终纳入模型的变量有租金因变量、租房方式、电梯、付款方式、户型、面积、朝向、楼层、装修、类型、小区、行政区、商圈、地铁线、地铁站、距离。报告在建模阶段排除了一些字段这个决策过程值得仔细看。3. 从 Excel 到 Tableau房租可视化中的业务判断3.1 不同房屋类型的均价对比数据清洗完成后第一步是画图。报告用条形图对比了别墅、平房、其他、普通住宅、公寓的房租均价结果是别墅 38402 元远超其他类型。这里有一个容易忽略的判断公寓均价排在末尾“令人奇怪的是公寓一般为小区房价格偏高均价表现却排名末尾”。报告给出的解释是“公寓多数是合租房而其他类型多数是整租房”。这个解释在建模阶段得到了验证。公寓里合租比例高拉低了整体均价普通住宅里整租比例高均价自然上去。这说明可视化时看到的“类型差异”不一定是类型本身造成的而是类型与其他变量租房方式混杂在一起。这也是为什么报告后面要把别墅排除掉、把租金限制在 800 到 8000 元再做分析。样本人群定位很明确毕业生和收入一般的上班族别墅样本量小且单价极端会把均值和回归系数拉偏。3.2 行政区域与小区分布的地图化展示行政区的均价对比显示南山 2705 元、福田 2372 元、罗湖 2302 元、宝安 2169 元、龙华 2049 元、龙岗 1708 元。南山比龙岗贵了近 1000 元这在深圳是符合直觉的。报告中排除盐田、坪山、光明的理由也值得注意采集数据时需要获取房屋与最近地铁站的距离这三个区离地铁站远安居客网站没有将其包括在内。这说明数据源本身就有覆盖范围的限制分析结论只适用于样本覆盖的区域。比较出彩的一步是用高德地图 API 把小区经纬度拿到然后在 Tableau 里绘制地图颜色表示行政区气泡大小表示租金高低。这一步的实用性在于表格里的标高均价只能看出数字大小地图能直接看出南山科技园、福田 CBD 这些高租金片区的地理聚集效应。如果你要复现记得在高德开放平台申请 Web 服务 API 的 key免费额度足够处理报告里的小区数量。请求返回的是 JSON解析出 geocode 里的 location 字段即可。3.3 租房三要素与数值变量的可视化陷阱报告对租房方式、电梯、楼层做了对比条图整租均价比合租高出 2 倍多有电梯比没电梯贵高层比中层贵。这三个结论单独看都成立但放在一起如果直接拿来做定价决策就会出问题——因为整租的房屋面积通常更大有电梯的小区通常更新楼层和朝向也是关联的。在没有控制其他变量的情况下可视化图只能给你看趋势方向不能用来量化影响大小。报告在画完这些图后明确写了一句“为了更加合理地分析各个因素对房租的影响下面建立多元线性回归模型”说明作者自己也清楚单变量可视化的局限。数值变量的可视化翻车点更多。报告把面积和房租的关系画成组合图柱状图是各面积段的频数折线是房租均价走势。面积在 10 到 40 平方米范围内房屋占比高均价呈上升趋势但超出这个范围后频数太低均价波动大趋势图失去说服力。还有一点报告里提到了合租房的面积和租客实际居住的房间面积存在较大出入可能出现面积大但租金低的情况导致趋势图在某些区间表现不佳。地铁距离与房租的关系更有意思。按直觉应该是离地铁越近越贵但报告画出来发现这个关系并不明显。它给了两个可能的原因一是做单变量可视化时没法保证其他指标相等二是距离在地铁的定价中权重不高房租高低优先参考其他指标。这两个解释都对而且指向同一个结论必须做多变量分析才能剥离混杂因素。4. 多元线性回归建模从最小二乘法到 330 个虚拟变量4.1 模型设定与变量预处理多元线性回归的第一步是设定因变量和自变量。报告以租金 price 为因变量以 rent、elevator、area、towards、floor、decoration、style、district、business_circle、subway_line、subway_station、distance 为自变量。这里面有两个关键的变量处理决策小区变量被删除了付款方式被排除了。小区被删除的原因是数据里有 3000 多个不同小区如果全部转化为虚拟变量特征数量会爆炸训练速度变慢且新数据里的小区如果没在训练集中出现过就无法预测。这个判断很有实际意义。3000 个小区对应 3000 个虚拟变量在 10000 多条样本下每个虚拟变量的正样本可能只有几条模型会严重过拟合。报告后来实际的虚拟变量数量是 330 个这已经包含了行政区、商圈、地铁线、地铁站的所有组合如果再加入小区数量会超过 3000。付款方式被排除的理由是“租客交付押金的比例对房租影响不大”。这个决策可以讨论但报告的逻辑是合理的付款方式更多是资金流安排与房屋本身特征无关。户型没有直接加入模型而是保留面积这个数值变量因为户型和面积高度共线同时放入会造成严重的多重共线性。最终模型表达式price β0 β1*rent β2*elevator β3*area β4*towards β5*floor β6*decoration β7*style β8*district β9*business_circle β10*subway_line β11*subway_station β12*distance ε4.2 用 Python 训练模型与评估指标解读把数据分为训练集和测试集后用 Python 跑线性回归得到第一轮结果RMSE 936.7R² 0.701调整 R² 0.685。RMSE 的单位是元936.7 意味着平均预测误差接近 937 元对于均价 2000 到 2700 元的深圳租房市场这个误差太大模型基本不可用。R² 0.701 说明面积、区域、地铁这些因素只能解释房租变异的 70.1%还有 30% 的变异来自其他因素。代码层面的大致流程是import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score # data 是清洗后的 DataFrame # 将字符型变量转化为哑变量 df pd.get_dummies(data, columns[rent, elevator, towards, floor, decoration, style, district, business_circle, subway_line, subway_station], drop_firstTrue) X df.drop(price, axis1) y df[price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) model LinearRegression() model.fit(X_train, y_train) pred model.predict(X_test) rmse np.sqrt(mean_squared_error(y_test, pred)) r2 r2_score(y_test, pred) print(fRMSE: {rmse:.1f}, R2: {r2:.3f})pd.get_dummies 是虚拟变量转换的标准做法drop_firstTrue 是为了避免哑变量陷阱——如果一组分类变量有 k 个类别只需要 k-1 个虚拟变量否则会出现完全共线性。random_state42 是固定随机种子保证每次跑出来的训练集测试集划分一致方便复现。第一轮效果不好的原因报告判断为“数据中存在异常值譬如面积很大的房屋租金很低”。这类异常可能来自真实挂牌价老破小的大面积但租金不高也可能来自数据爬取时的字段错位。无论哪种都需要先检测再处理不能直接删除所有看起来“不合理”的数据。4.3 用 KNN 距离检测并删除异常值报告用了 KNN 原理来做异常值检测这个思路比单纯用 Z-score 或 IQR 更稳健一些。核心逻辑是一个样本点如果离它的 K 个最近邻居的平均距离很远那它大概率是异常值。计算完每个样本的 K 近邻平均距离后再用分位数找阈值。import numpy as np from sklearn.neighbors import NearestNeighbors # 选取参与计算的数值特征 features df[[area, distance, price]].values # 计算每个样本点与最近 K 个邻居的平均距离 k 5 nbrs NearestNeighbors(n_neighborsk).fit(features) distances, indices nbrs.kneighbors(features) avg_dist distances[:, 1:].mean(axis1) # 去掉自身距离 # 用 IQR 方法判断异常 Q1, Q3 np.percentile(avg_dist, [25, 75]) IQR Q3 - Q1 upper_bound Q3 1.5 * IQR outlier_mask avg_dist upper_bound print(f检测到异常值数量: {outlier_mask.sum()}) # 过滤异常值后重新建模 df_clean df[~outlier_mask]这里的距离特征用的是 area、distance、price 三个数值变量它们量纲不同面积是几十到几百距离是几十到几千租金是几百到几千。如果不做标准化欧氏距离会被数值量级大的变量主导KNN 的邻居选择就失去意义。常见做法是用 StandardScaler 或 MinMaxScaler 先做归一化再做 KNN。报告没有提到这一步但如果你复现时发现异常值检测效果不好第一件事就是检查特征缩放。异常值处理后的回归结果明显改善RMSE 从 936.7 降到 655.6R² 从 0.701 提升到 0.818调整 R² 从 0.685 提升到 0.808。R² 0.818 的意思是自变量能解释租金的 81.8% 变异对于租房这类受主观因素影响较大的数据这个解释力度已经算不错了。但报告并没有就此收手而是敏锐地指出新问题模型包含 330 个特征变量容易过拟合变量间存在多重共线性导致部分回归系数取值与实际相反。特征数量 330 和样本量 10000 的比值虽然不算极端但其中大量虚拟变量的正样本极少模型会记住那些只出现几次的商圈和地铁站组合泛化能力差。接下来的 Lasso 回归就是为了解决这个。5. 避坑指南租房数据建模时绕不开的四个大坑5.1 爬虫数据格式不统一现象爬下来的面积字段既有“55平米”又有“55㎡”还有“55 平方米”距离字段有的是“200米”有的是“0.2公里”甚至有的写“步行5分钟”。强行用 pd.to_numeric 会直接报错或变成缺失值。原因安居客的不同房源页面模板可能由不同的中介维护填写格式不一致爬虫解析时如果用字符串截取很容易把单位残留带入字段。解决用正则统一提取数字部分。面积用re.findall(r\d\.?\d*, text)取第一个数字距离先判断是否包含“公里”再乘以 1000。清洗后立刻画分布直方图看到面积有 0 或负数一定是解析出了问题。5.2 分类变量不加 drop_first 导致共线性现象模型跑出来部分系数符号与常识相反比如朝向朝南反而比朝北便宜或者某条地铁线的系数大到离谱。用df.corr()看数值变量时没有异常但模型系数明显不合理。原因pd.get_dummies 默认生成的虚拟变量数量等于类别数量没有多 collapse 一个基准组造成完全多重共线性。最小二乘估计在这种情况下不稳定系数的数值和符号都可能失真。解决所有分类变量在转换时都要加 drop_firstTrue。报告里也提到以“合租、有电梯、东北朝向、中层、毛坯、公寓、南山、万众城商圈、11号线、上塘站”为基本变量实际效果和 drop_first 可以结合起来确保基准组的选择有业务意义。5.3 异常值不能一刀切删除现象KNN 检测出异常值后直接删除模型是变好了但你会发现预测结果对某些真实房源严重低估。比如大冲村的低价小户型被当成异常值删掉了导致模型完全没见过这种类型的样本。原因KNN 异常值检测会把“远离大部分邻居”的样本视为异常但远离不代表不可能。有些真实的低价房源比如握手楼、低楼层、无电梯、郊区也满足“距离远”的条件如果粗暴删除相当于人为剔除了数据的真实分布。解决先看被删样本的业务特征。如果异常值的面积和租金组合明显违背市场规律比如 500 平米月租 1000 元删掉没问题如果只是“比平均价低但低得有道理”建议保留或单独建子模型。报告里删除的是 800 到 8000 元之外的数据这个范围本身就是业务筛选而不是纯粹的统计筛选。5.4 距离变量不缩放单位的隐患现象模型里距离的系数计算出来是 -0.058看起来作用很小。但如果把距离单位从米换成千米系数会变成 -58面积系数还是 55两个变量的重要性排序就会改变。原因线性回归的系数大小依赖变量量纲不能直接对比系数大小来判断特征重要性。距离以米为单位时数值范围是 0 到 2000面积是 20 到 120同样的真实影响距离的系数必然小。解决要么先做标准化再回归用标准化系数比较重要性要么像报告那样在解读时将距离换算成“每 100 米”的效果把系数乘以 100 再解释这样既保留了原始单位的可读性又能与面积系数放在同一量级下比较。报告里“距离每增加 100 米减少 6 元”就是用这个方式得出的。6. Lasso 回归筛选特征与模型收敛从 330 维压缩到 111 维6.1 压缩估计量与 L1 惩罚的作用异常值处理后模型效果提升了但 330 个特征仍然偏多。报告引入 LassoLeast Absolute Shrinkage and Selection Operator来解决过拟合和多重共线性。Lasso 在普通最小二乘的损失函数后面加了一个 L1 范数惩罚项它的特点是可以把不重要的系数直接压到 0从而实现自动特征选择。from sklearn.linear_model import Lasso from sklearn.preprocessing import StandardScaler # 标准化数值特征保证 L1 惩罚对每个特征公平 scaler StandardScaler() num_cols [area, distance] X_train_scaled X_train.copy() X_test_scaled X_test.copy() X_train_scaled[num_cols] scaler.fit_transform(X_train[num_cols]) X_test_scaled[num_cols] scaler.transform(X_test[num_cols]) # 用 Lasso 回归alpha 控制惩罚强度 lasso Lasso(alpha10, max_iter10000, random_state42) lasso.fit(X_train_scaled, y_train) # 筛选系数不为 0 的特征 selected_cols X_train_scaled.columns[lasso.coef_ ! 0] print(fLasso 筛选后特征数量: {len(selected_cols)})标准化这一步很重要。Lasso 的 L1 惩罚对量纲敏感如果不做标准化面积和距离这种数值大的变量会被优先保留虚拟变量只有 0/1 取值系数容易被压缩为 0导致商圈、地铁站这些有效变量被错误淘汰。报告里筛选后特征数量从 330 降到 111这个结果说明大量虚拟变量确实是噪声或重复信息。alpha 是 Lasso 的超参数控制惩罚强度。alpha 越大被压缩为 0 的系数越多特征越少。报告没有具体给出 alpha 值但可以根据期望的特征数量用交叉验证来选择from sklearn.model_selection import GridSearchCV param_grid {alpha: [1, 5, 10, 20, 50]} lasso_cv GridSearchCV( Lasso(max_iter10000, random_state42), param_grid, cv5, scoringneg_root_mean_squared_error ) lasso_cv.fit(X_train_scaled, y_train) print(f最优 alpha: {lasso_cv.best_params_[alpha]})GridSearchCV 会尝试不同的 alpha每次做 5 折交叉验证用负 RMSE 作为评分标准。最终选择的 alpha 是让验证集 RMSE 最小的那个。6.2 Lasso 之后的线性回归与 p 值筛选Lasso 筛选特征后报告用筛出的变量重新跑了一次普通线性回归结果是 RMSE 641.6R² 0.807调整 R² 0.803。相比异常值处理后R² 略有下降从 0.818 降到 0.807但测试集 RMSE 也在降从 655.6 降到 641.6特征数量从 330 降到 111。R² 降低而 RMSE 降低说明模型在测试集上的预测更准了只是训练集上的解释力被牺牲了一点——这是典型的偏差-方差权衡改善。接着报告按 p ≤ 0.1 进一步筛选变量。这一步的逻辑是Lasso 的作用是压缩系数和特征选择但它不提供显著性检验筛选出来的 111 个特征里有些其实不显著比如朝向_东南的 p 值为 0.178不能拒绝系数为 0 的原假设。用 p 值做第二轮筛选剔除不显著的变量让模型更精简。import statsmodels.api as sm # 用 Lasso 筛选后的特征重新拟合 OLS ols sm.OLS(y_train, sm.add_constant(X_train_scaled[selected_cols])) result ols.fit() # 筛选 p 值 0.1 的特征 sig_cols result.pvalues[result.pvalues 0.1].index.tolist() sig_cols [c for c in sig_cols if c ! const] print(fp0.1 筛选后特征数量: {len(sig_cols)})最终模型 RMSE 644.0和 Lasso 后的 641.6 几乎持平但特征更少模型更简洁。6.3 用准确率评估模型的实际预测能力报告提出了一个业务导向的评估方式假设预测值与真实值满足|ŷ - y| / y ≤ α则认为预测准确准确率 预测准确的样本数 / 总样本数。这个定义比 RMSE 更直观——直接回答“租金 1000 元模型预测在 700 到 1300 元之间的概率有多大”。从报告的结果看α 0.3 时准确率 67.49%α 0.5 时准确率 85.57%。用我自己的话说模型有接近七成把握把预测误差控制在 30% 以内。这个表现和 R² ≈ 0.8 是吻合的。RMSE 644 元在平均租金 2000 到 2700 元的市场上依然偏大但考虑到租房定价中的主观因素这已经是比较可用的水平。最终报告的核心系数可以整理成一张解读表变量变化租金变化说明面积 1 平方米平均 55 元数值变量系数最稳健距离 100 米平均 -6 元换算成 100 米单位后解读整租 vs 合租平均 453 元租住方式影响最大的因素之一无电梯 vs 有电梯平均 -71 元楼层便利性的真实价值高层 vs 中层平均 46 元采光和通风的溢价宝安 vs 南山平均 -838 元区域差异在模型中依然显著面积系数 55 和距离系数 -0.058 都是显著性极高的变量p 值为 0说明这两个变量对租金有很强的解释力。整租 vs 合租的 453 元差值在可视化阶段已经能看出来整租 2850 元 vs 合租 1367 元但可视化看到的差距是 1483 元模型控制其他变量后缩减到 453 元——这就是回归分析的价值所在剔除了面积、区域混杂后的纯租住方式效应。从那以后我每次做类似的城市租房分析都会在数据清洗阶段就把“单位统一”和“基准组设计”放进检查清单甚至在爬虫脚本里先跑一轮廉价的多线程版本摸清字段格式再用 Scrapy 全量采集。报告里用到的题型让我少走了非常多弯路。希望这篇文章能帮你在做自己的数据分析项目时也避开这些坑。本文还有配套的精品资源点击获取