ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

安居客租房数据分析实战:从爬虫清洗到可视化的完整工程链路

安居客租房数据分析实战:从爬虫清洗到可视化的完整工程链路 简介一份关于安居客租房数据分析与可视化的实验报告以深圳为研究范围通过Python爬取安居客10000多条租房信息完整呈现了从数据获取、处理、可视化到线性回归建模的流程适合学习Python爬虫、Excel/Tableau可视化及回归分析的读者参考。报告对比了单线程、多线程与Scrapy三种爬虫方式的优缺点展示了Power Query清洗数据的步骤并使用KNN检测删除异常值、Lasso回归筛选自变量最终得出面积、地铁距离、租房方式、电梯与楼层对租金的具体影响。资源为单个PDF文件共912KB内容为图文并茂的实验报告目前已有2546人学习下载。读者可从报告中获得一套可复用的数据分析实验思路包括爬虫方案选择、数据预处理细节、可视化图表绘制和模型结论解读对完成类似房源数据分析课题具有直接参考价值。1. 安居客租房数据分析实验一份 PDF 报告背后的可复制项目市面上能看到不少「租房数据分析」的作业真正把它做成实验报告的人往往不是卡在可视化而是卡在数据源头安居客房源字段乱、重复多、页面结构说变就变清洗环节就能劝退一半新人。把这个标题拆开看它是一条从爬虫到清洗、再到分析和可视化的完整链路这也是我认为它值得照着做一遍的原因。它不依赖某个特定数据集数据自己抓、指标自己定、图表自己画做完之后你手里不是一份 PDF而是一套可以换城市、换平台复用的方法论。这篇文章适合正在学 Python 数据分析、想练手爬虫与可视化或者需要给课程/项目交付一份实验报告的人。我会按「取数—清洗—分析—可视化—排错」的顺序把每个环节的参数、边界和踩过的坑讲透。2. 把安居客房源页变成结构化数据采集与清洗的取舍2.1 先定字段再写爬虫租房分析到底需要哪些列很多人一上来就写爬虫抓到什么存什么结果列表页、详情页字段混杂后面分析时才发现缺了关键信息。做数据分析的第一步是把要分析的业务问题拆成字段需求。以「租房数据分析和可视化」为例最核心的问题大约是城市各区域租金分布如何、户型与面积对价格的影响有多大、哪些板块性价比高。对应下来至少要保留以下字段。字段名说明数据分析用途title房源标题文本特征可提取地铁/精装等关键词district行政区或板块区域维度聚合layout户型如 2室1厅户型维度分析area面积单位平方米计算单价、面积分布orientation朝向朝向与价格的关系floor楼层信息低/中/高楼层偏好分析total_price月租金核心目标变量unit_price每平米月租消除面积影响后的比价指标publish_time发布日期房源新鲜度与价格关系建议在爬虫阶段就把字段名固定下来后续清洗、分析和可视化都用同一套命名。我在实际项目中反复吃过字段名不一致的亏——上一版叫 area下一版叫 acreage代码改起来不复杂但排查半天很费时间。字段名统一后哪怕换到链家、贝壳的数据也只需要改解析层清洗和分析代码几乎不动。2.2 解析页面用 requests 加 BeautifulSoup 抽取房源卡片采集层最常见的做法是直接请求安居客租房列表页用 BeautifulSoup 解析 HTML 卡片。要注意的是列表页和详情页的字段分布在不同的 DOM 位置如果只抓列表页单价、朝向、楼层这些信息可能拿不全。我的习惯是先用浏览器开发者工具查看列表页的渲染结构确认哪些字段在服务端 HTML 里哪些是 JS 动态加载的。对于关键字段缺失的房源再进详情页补抓但详情页请求量会放大很多倍初期不建议全量做。下面是抓取列表页并解析核心字段的最小实现。这里只演示结构化的过程抓取频率、UA 设置等合规事项放在第 5 章避坑部分说明。import requests from bs4 import BeautifulSoup import pandas as pd import time def fetch_list_page(city_code, page): url fhttps://{city_code}.zu.anjuke.com/fangyuan/p{page}/ headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 return resp.text def parse_list_html(html): soup BeautifulSoup(html, lxml) items soup.select(.zu-itemmod) rows [] for it in items: title_tag it.select_one(.zu-info h3 a) addr_tag it.select_one(.zu-info address) detail_tag it.select_one(.zu-info .details-item) if not title_tag or not addr_tag: continue row { title: title_tag.get_text(stripTrue), district: addr_tag.get_text(stripTrue).split( )[0] if addr_tag else None, detail: detail_tag.get_text(stripTrue) if detail_tag else None, } # detail 形如 2室1厅 / 58.6㎡ / 南 北 / 15层 / 精装 parts row[detail].split(/) if row[detail] else [] row[layout] parts[0].strip() if len(parts) 0 else None row[area] parts[1].replace(㎡, ).strip() if len(parts) 1 else None row[orientation] parts[2].strip() if len(parts) 2 else None row[floor_info] parts[3].strip() if len(parts) 3 else None rows.append(row) return rows all_rows [] for page in range(1, 4): # 先抓 3 页验证 html fetch_list_page(sh, page) rows parse_list_html(html) all_rows.extend(rows) time.sleep(1) # 控制请求频率避免高频访问 df pd.DataFrame(all_rows) df.to_csv(raw_anjuke.csv, indexFalse, encodingutf-8-sig) print(df.shape)代码逻辑方面fetch_list_page负责请求页面parse_list_html负责从每个房源卡片div.zu-itemmod里提取标题、地址和详情字符串再把详情按/拆成户型、面积、朝向等字段。这里我特意先把 detail 整串存下来再拆分因为列表页的结构经常微调保留原始串方便后面重新解析。参数设置上p{page}是页码参数城市代码sh表示上海换成其他城市时注意城市域名前缀。User-Agent是请求头里最基本的身份标识不能缺。time.sleep(1)是必要的节流哪怕只为了不给自己添麻烦也建议保留。另外raw_anjuke.csv用utf-8-sig编码是避免 Excel 打开 CSV 时中文乱码的一个惯例做法。2.3 清洗不是删空值面积、单价、朝向的标准化处理爬下来的数据大概率不干净常见现象有面积字段带㎡符号、单价缺失、朝向里混入「南 北」这种多朝向、楼层信息里带「共18层」等。清洗的目标是按照分析需求把字段转成可计算的类型同时保留足够的原始信息。import pandas as pd import numpy as np df pd.read_csv(raw_anjuke.csv) # 面积去掉非数字字符转 float df[area] df[area].str.replace(㎡, ).str.strip() df[area] pd.to_numeric(df[area], errorscoerce) # 楼层信息拆出总层数与所在层级 def parse_floor(info): if not isinstance(info, str): return None, None # 例低层/共18层 if 共 in info and 层 in info: level info.split(/)[0].strip() total info.split(共)[1].replace(层, ).strip() return level, int(total) return info, None df[[floor_level, total_floors]] df[floor_info].apply(lambda x: pd.Series(parse_floor(x))) # 朝向多朝向合并为第一个主朝向并统计朝向数量 df[orientation_main] df[orientation].str.split( ).str[0] # 单价优先用页面字段缺失时用 total_price / area 计算 if unit_price not in df.columns: df[unit_price] np.nan df[unit_price] df[unit_price].fillna(df[total_price] / df[area]) # 过滤异常值面积小于 5 平或大于 500 平单价大于 500 元/平 df df[(df[area] 5) (df[area] 500)] df df[(df[unit_price] 0) (df[unit_price] 500)] print(df.info()) print(df[[layout, area, unit_price]].describe())这段代码覆盖了四个清洗要点。数值化方面pd.to_numeric配合errorscoerce可以把转换不了的值变成 NaN这样后面可以用dropna或fillna统一处理。楼层解析函数说明了一个经验不要试图一次性把所有字段清洗到位楼层信息这种复合字段拆成所在层级和总层数两列后续分析更灵活。朝向处理上多朝向抄作split( ).str[0]取主朝向但单独保留orientation原字段随时可回溯。面积和单价过滤阈值是分析前的最后一道防线。5 到 500 平、单价低于 500 元/平这两个区间是我结合多年租房数据观察总结的经验范围不同城市可以按需调整一线城市核心区单价会更高二线城市可以适当放宽面积上限。如果清洗后数据的describe()里出现明显离谱的均值或中位数优先检查过滤条件而不是急着做可视化——这一步不验后面所有图表都会失真。3. 探索性分析租金分布、区域价差与户型规律3.1 先用描述性统计给数据「验身」拿到清洗后的数据别急着画图。先跑一组描述性统计确认数据的量级、分布形态和缺失情况这相当于给数据做体检。租房数据的核心变量有三个total_price、unit_price和area它们直接决定了后续可以做什么分析。import pandas as pd df pd.read_csv(cleaned_anjuke.csv) summary df[[total_price, unit_price, area]].describe(percentiles[0.25, 0.5, 0.75, 0.9]) print(summary) # 分区间的房源量检查是否覆盖了目标区域 district_counts df[district].value_counts() print(district_counts.head(15))describe(percentiles[...])默认只给四分位数我习惯额外看 90 分位租金分布普遍右偏90 分位能暴露尾部高价房源的比例。value_counts则用来检查区域覆盖度——如果目标分析三个区结果却集中在某一个区说明爬虫页面翻得不够或者筛选条件把其他区的数据过滤掉了。观察结果时重点看两点中位数和均值差距大不大如果差距大说明数据右偏严重后面做可视化时更适合用箱线图而非均值折线count行看有多少缺失若单价缺失占比超过 10%补算逻辑要回到清洗阶段重查。这些判断不需要统计学功底但能帮你避免被一两套豪宅房源带偏整张图表的结论。3.2 区域维度不同板块的租金中位数与单位面积租金区域分析是租房项目里最容易被写进报告的部分因为结论直观哪个区贵、哪个区便宜、差距多大。但直接用平均价对比不太稳妥租金受面积和户型影响极大平均价会偏向大面积房源。更稳的是用中位数和单位面积租金双维度交叉。import pandas as pd df pd.read_csv(cleaned_anjuke.csv) # 按区域聚合房源量、总价中位数、单价中位数 district_stats df.groupby(district).agg( listing_count(title, count), median_total_price(total_price, median), median_unit_price(unit_price, median), mean_area(area, mean) ).sort_values(median_unit_price, ascendingFalse) print(district_stats.round(1)) # 只看房源量大于 20 的区域避免小样本噪声 district_stats district_stats[district_stats[listing_count] 20] print(过滤后区域数量:, len(district_stats))groupby加agg实现区域聚合我习惯同时输出房源量目的是标记数据稀疏区域。个别板块可能只有三五条房源中位数算出来没意义报告里硬写反而显得不专业。房源量阈值20是经验值数据量大时可以提高到 50核心原则是小样本区域的结论不参与对比。区域分析里还有一个容易被忽略的角度median_unit_price和median_total_price排名不一致的地方往往就是面积段差异大的区域。比如 A 区总价中位数高但单价低通常说明该区域大户型占多B 区总价低但单价高很可能以小单间或公寓为主。这两组数字对照着解读报告的说服力会比单看总价强不少。3.3 户型与面积段砍价空间和挂牌规律的隐藏信息户型分析不能只做「户型分布 Top10」就完事那样信息量很少。更有价值的两个方向是一室户的单价溢价、以及同户型内面积与总价的回归关系。这些结论能直接回答「一室户和两室户哪个性价比高」这种看房人常问的问题。import pandas as pd import numpy as np df pd.read_csv(cleaned_anjuke.csv) # 户型分组计算各户型的房源量与单价中位数 layout_stats df.groupby(layout).agg( count(title, count), median_area(area, median), median_unit_price(unit_price, median) ).sort_values(count, ascendingFalse) print(layout_stats.head(10)) # 面积段分箱看不同面积段的单位租金 bins [0, 30, 50, 70, 90, 120, 200] labels [0-30, 30-50, 50-70, 70-90, 90-120, 120] df[area_bin] pd.cut(df[area], binsbins, labelslabels) area_bin_stats df.groupby(area_bin, observedFalse).agg( count(title, count), median_unit_price(unit_price, median) ) print(area_bin_stats)户型提取时注意一个问题layout字段的文本并不规范比如「1室0厅」「1室1厅」「整租 1室1厅」并存。如果用value_counts直接统计会被拆成好几行。建议先做一次简单的规范化把整租、押一付一这类前缀去掉再统一格式。代码里pd.cut对面积分箱observedFalse是为了避免分箱后出现空类别时报警告这在 pandas 2.x 版本里尤其要注意。面积段与单价的关系往往是倒 U 型的小面积段单价偏高因为总价低、转手快大面积段单价回落改善型区域相对划算。这个规律在你的数据里是否成立能直接影响报告里「刚需区域」和「改善区域」的论证。如果画出的图表不符合预期优先回查是不是某个面积段的数据量太少或者异常值没清洗干净。4. 可视化落地从 Matplotlib 到 ECharts 大屏4.1 可视化选型静态图、交互图和大屏各自的适用场景实验报告类项目容易在可视化环节失控要么贴一堆 Matplotlib 默认样式图要么直接上大屏但实际数据量根本撑不起大屏的信息密度。选型应该按输出场景来定报告里的静态图表用 Matplotlib/Seaborn 足够重点是标注清晰和结论突出需要交给领导或客户交互查看的用 ECharts 输出 HTML 文件可视化大屏这种形式适合持续更新的监控界面不适合一次性实验结论。可视化方式工具选择典型输出适用阶段静态图表Matplotlib SeabornPDF/Word 插图实验报告主体交互图表ECharts → HTML可点选的图表页面汇报演示、自用探索数据大屏ECharts 前端框架实时看板数据持续更新后这套分工的边界很重要。实验报告的核心价值是「数据结论」不是「页面炫酷」。我见过不少项目把大量时间花在配置大屏动画上最后数据只有几百条做出的大屏反而显得空。正确做法是先用 Matplotlib 把核心结论画清楚有余力时再用 ECharts 做交互补充大屏属于后面进阶的事。4.2 用 Matplotlib 和 Seaborn 输出实验报告核心图表实验报告里最常见的图表组合是租金分布直方图、区域单价箱线图、户型单价柱状图。这三张图能支撑大部分结论。关键是细节标题、轴标签、数值标注缺一个都会让图表看起来像半成品。import matplotlib.pyplot as plt import seaborn as sns import pandas as pd plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False df pd.read_csv(cleaned_anjuke.csv) fig, axes plt.subplots(1, 2, figsize(14, 5)) # 图1总价分布直方图 axes[0].hist(df[total_price], bins40, color#4C72B0, alpha0.8) axes[0].axvline(df[total_price].median(), colorred, linestyle--, labelf中位数 {df[total_price].median():.0f}) axes[0].set_xlabel(月租金元) axes[0].set_ylabel(房源数量) axes[0].set_title(租房总价分布) axes[0].legend() # 图2区域单价箱线图取房源量前8的区域 top_districts df[district].value_counts().head(8).index plot_df df[df[district].isin(top_districts)] sns.boxplot(dataplot_df, xdistrict, yunit_price, axaxes[1]) axes[1].set_xticklabels(axes[1].get_xticklabels(), rotation45, haright) axes[1].set_xlabel(区域) axes[1].set_ylabel(单位面积月租元/㎡) axes[1].set_title(各区域单位租金分布) plt.tight_layout() plt.savefig(rent_analysis_core.png, dpi150) plt.show()字体配置SimHei和Microsoft YaHei分别覆盖 Windows 和 macOS 环境axes.unicode_minus解决坐标轴负号乱码这是 Matplotlib 中文图表的两个固定配置每次写都要带上。直方图里的axvline画中位数参考线比只用分布形状更能传递数据量级。箱线图用top_districts限定前 8 个区域避免小区域样本量少导致箱体奇形怪状。dpi150是报告插图的最低要求低于这个值印刷或放大后会明显发虚。tight_layout也不是可选项不调用的话中文标签经常被裁切这是 Matplotlib 老生常谈的坑。如果环境里运行报字体缺失可以改用plt.rcParams[font.sans-serif] [WenQuanYi Zen Hei]或直接指定系统中文字体路径具体以你的系统已装字体为准。4.3 把数据导出成 JSON交给 ECharts 做区域热力与散点Matplotlib 图表适合打印但缺少交互能力。实验报告如果要「能点能查」我一般把聚合后的数据导出为 JSON再写一个 ECharts HTML 模板。ECharts 的优势在于生态成熟、图表类型全、纯前端零依赖是数据可视化项目里最不容易翻车的工具。import pandas as pd import json df pd.read_csv(cleaned_anjuke.csv) # 导出区域聚合数据供 ECharts 柱状图/地图使用 district_agg df.groupby(district).agg( count(title, count), avg_unit_price(unit_price, mean), median_price(total_price, median) ).round(1).reset_index() with open(district_agg.json, w, encodingutf-8) as f: json.dump(district_agg.to_dict(orientrecords), f, ensure_asciiFalse, indent2) # 导出原始核心字段供散点图使用 scatter_data df[[area, total_price, unit_price, district]].dropna() with open(scatter_data.json, w, encodingutf-8) as f: json.dump(scatter_data.to_dict(orientrecords), f, ensure_asciiFalse, indent2)to_dict(orientrecords)把 DataFrame 转成列表字典ensure_asciiFalse保证中文不转义成\u序列。ECharts 的散点图上横轴面积、纵轴总价、点大小或颜色映射单价区域交互式的体验能比静态图更直观地暴露异常点。ECharts HTML 模板的核心配置如下。!DOCTYPE html html head meta charsetutf-8 title租房数据分析/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idscatter stylewidth: 100%; height: 500px;/div script fetch(scatter_data.json) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(scatter)); chart.setOption({ title: { text: 面积与月租金关系, left: center }, tooltip: { trigger: item }, xAxis: { name: 面积(㎡), type: value }, yAxis: { name: 月租金(元), type: value }, series: [{ type: scatter, data: data.map(d [d.area, d.total_price]), symbolSize: 8, itemStyle: { color: #5470C6, opacity: 0.6 } }] }); }); /script /body /html这段 HTML 的核心是fetch读取本地 JSON再映射成 ECharts 的scatter系列。注意fetch不能在file://协议下直接读本地 JSON会遇到跨域问题需要本地起一个静态服务器或者直接把 JSON 数据内嵌到 HTML 里。我通常用python -m http.server在项目目录起服务浏览器访问localhost:8000即可查看。5. 租房数据项目的 4 个典型翻车点与排查记录5.1 爬虫请求被限流数据出现整页缺失现象跑完爬虫后CSV 里的房源量明显少于页面显示量且缺失集中在连续的几个页码上。原因请求频率过高或 UA 特征明显被服务端限制访问。安居客的列表页有反爬机制高频请求会触发验证码或返回空页面。这类问题不会在第一次请求时暴露往往跑了几十页后才出现。解决单页请求间隔从 1 秒放宽到 2 到 3 秒加上随机抖动轮换 User-Agent从常见浏览器 UA 池里随机选择。更稳的做法是加入重试机制请求失败后等待 10 秒再重试一次仍失败就用代理池但代理池对学习项目来说不是必备项。控制抓取频率是最原始的道德底线也是保证数据完整性的工程手段。import random import time import requests def robust_request(url, retries3): for attempt in range(retries): try: time.sleep(random.uniform(2, 3)) resp requests.get(url, headersrandom_ua(), timeout10) if resp.status_code 200 and zu-itemmod in resp.text: return resp.text except requests.RequestException: pass return None5.2 面积字段混入「暂无数据」导致单价计算负值现象清洗后unit_price出现负值或超大值箱线图里出现离谱的异常点。原因列表页的面积字段有时不是数字而是「暂无数据」等占位文本。pd.to_numeric转出来是 NaN但后续用total_price / area计算单价时如果 area 为 0 或 NaN 没处理会算出无穷大或错误值。解决在计算单价前先 dropna 面积字段或者用df[area].replace(0, np.nan)把 0 替换掉再计算。更彻底的方法是在解析阶段就把非数字面积过滤掉不让它进 DataFrame。排查时优先看df[df[unit_price] 0]的原始记录往往能直接定位到哪一步出了问题。5.3 经纬度获取失败地图可视化只剩几个孤点现象ECharts 地图上只有零星几个点大部分房源无法定位。原因安居客列表页里的地址是文字描述不带经纬度。如果后续要做地图散点图需要拿到小区名称后调用地理编码服务该服务有每日配额限制小批量数据还好数据量一大就会报配额超限。解决先只对区域维度做聚合把每个区域的中心点坐标硬编码成配置而不是逐套房源做地理编码。对小区级别的定位分批请求、加延时并做好失败缓存——已解析过的地址存到本地文件不重复请求。地图类的可视化是为「区域关系」服务的不必精确到每一栋楼聚合到区域中心点足够支撑报告结论。5.4 报告图表与数据库口径对不上数字前后矛盾现象正文写「平均租金 4800 元」图上中位数却是 4200 元观众一眼看出矛盾。原因分析过程用了多份中间文件清洗前一份、清洗后一份、可视化前又导出一份各自字段或过滤条件不一致导致不同图表基于不同数据集。解决建立单一数据流。清洗后统一保存为一个cleaned_anjuke.csv后续所有分析和可视化都从这份文件读入不再回退到 raw 数据。每个中间结果用shape和记录数做一致性校验图表生成脚本里打印「数据源: cleaned_anjuke.csv 共 N 条」避免引用了过期文件。这套习惯听着基础却是实验报告可信度的重要支撑。6. 把实验报告变成可持续更新的数据看板验证方法与进阶技巧实验报告交付后还有一个更实际的问题房源数据是不断变化的这份分析过两周还能用吗我的做法是给项目加一层轻量验证用增量采集的数据回测之前的结论看区域排名和中位数是否发生明显偏移。具体操作是把每周新抓的数据追加到清洗后的表里然后重跑区域聚合脚本如果某个区域的租金中位数变化超过 10%就把该区域标记出来。这个验证方法不复杂但能让你的报告从「一次性作业」升级为「可更新的监测工具」。进阶方向上值得做两件事。第一是用cron或任务计划程序每周固定时间跑一次采集脚本配合git做数据版本管理这样任何时候都能回溯某周的数据排查数据异常时非常有底气。第二是给 ECharts 页面加上时间筛选器把不同周次的数据叠加展示。至于可视化大屏等数据积累到 3 个月以上再考虑用 ECharts 加一个简单的 HTML 看板足够了不必一开始就上重型前端框架。最后分享一个我在多个项目里反复确认过的习惯数据分析项目里最难的不是算法而是数据口径的一致性和可重复性。我踩过太多「换了台电脑结论就对不上」的坑后来强制自己把清洗、分析、可视化三个环节拆成独立脚本每个环节的输入输出都是文件脚本之间不共享内存变量。这个习惯让我的实验报告在事后半年再跑一遍依然能复现出当时的图表和结论。希望这份从采集到可视化的完整路径能帮到你——照着它走一遍你收获的不只是一份 PDF 报告还有一套可以复用到任何租房数据项目里的工程方法。本文还有配套的精品资源点击获取
返回列表