Python租房数据可视化:从安居客采集到PDF实验报告全链路实践
2026/9/19 12:04:49 网站建设 项目流程

简介:一份基于安居客真实租房数据的完整分析与可视化实验报告,以深圳为例,面向 Python 爬虫、数据分析与回归建模初学者,展示从数据采集到结论输出的全过程。报告以 10000 余条租房信息为样本,分别比较单线程、多线程和 Scrapy 三种爬虫实现,并用 Power Query 完成去重、字段拆分、文本转数值等清洗工作;随后借助 Excel 和 Tableau 绘制房屋类型均价、行政区房租、小区租金地图等图表,并建立多元线性回归模型,通过 KNN 检测异常值、Lasso 筛选变量,最终量化面积、地铁站距离、楼层等因素对租金的影响。资源为 1 个 PDF 文件,压缩包约 912KB,已有 2545 人学习。读者可从中获得完整实验思路、建模细节与可视化方法,便于迁移到其他城市或租房平台的数据分析任务。

1. 把安居客租房数据做成 PDF 实验报告,核心是链路的完整度

拿到"安居客租房数据分析及可视化实验报告.pdf"这个标题,多数人第一反应是"又要写爬虫了"。但真正做过数据分析项目的人会清楚,这个标题的工程重心根本不在爬虫,而在一条完整的数据分析链路:从租房数据采集和清洗,到核心指标计算,再到可视化图表呈现,最后落成一份结构化的 PDF 报告。中间任何一环脱节,最终交付物就是一张没有说服力的 Excel 截图堆砌。

这个标题最适合两类场景:一是学校或培训机构的数据分析课程实验,需要一份自带可视化产物的实验报告;二是求职者把"数据分析 + 可视化 + 报告自动化"串成一个可展示的作品集项目。相比单纯做数据大屏或写一篇爬虫博客,这种 PDF 实验报告的差异点在于:它要求分析结论、图表、参数说明和代码逻辑同时出现在一个文档里,且图表风格统一、文字排版干净。这也意味着,你需要在一开始就把"报告长什么样"当作需求输入,而不是等分析做完了再补。

下面我以国内租房数据为背景,把从字段清洗到 PDF 渲染的完整链路拆开讲,重点放在数据口径的选择、可视化参数设计和 PDF 生成方案选型上。你拿到手后,可以直接替换数据源和字段名,跑通自己的实验报告。

2. 实验数据怎么来:安居客租房数据的采集与清洗口径

2.1 先想清楚拿哪些字段,再去写采集代码

安居客租房列表页展示的字段通常包含:房源标题、租金(元/月)、户型(几室几厅)、面积(㎡)、朝向、楼层、小区名、所在区域、发布时间、标签(近地铁/拎包入住等)。详情页还能拿到更多,比如房屋年限、看房时间、配套信息,但对一个数据分析实验报告来说,列表页的这些字段已经覆盖了 80% 的分析维度。

直接抓列表页并用 BeautifulSoup 解析是最快的路径。一个最小可用的采集脚本如下:

import requests from bs4 import BeautifulSoup import pandas as pd def fetch_rent_list(city_url, pages=5): """抓取安居客租房列表页,提取核心字段""" records = [] for page in range(1, pages + 1): url = f"{city_url}rent/p{page}/" 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" } resp = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") items = soup.select(".zu-itemmod") for item in items: records.append({ "title": item.select_one(".zu-info h3").get_text(strip=True), "rent": item.select_one(".zu-side .price").get_text(strip=True), "detail": item.select_one(".zu-info .details-item").get_text(" ", strip=True) }) return pd.DataFrame(records)

代码逻辑很简单:遍历指定页数,用 CSS 选择器定位房源卡片,抽出标题、租金和详情文本。其中rent字段取出的是字符串,后面需要拆出数值;detail是"2室1厅 58㎡ 南 20/32层"这种拼接文本,也需要单独拆分。在实际抓取时必须设User-Agent,否则很容易触发基础反爬;如果页面结构变动导致.zu-itemmod匹配不到,优先打开浏览器开发者工具重新定位选择器。

2.2 出租信息最脏的 4 个字段,不洗没法分析

列表页抓回来的数据不会直接可用,我实践下来有四个重灾区:

  1. 租金字段:文本可能是"4500元/月"、"面议"、"1.5万元/月",甚至带"押一付三"前缀。需要用正则把数字和单位拆开,统一折算成元/月。
  2. 面积字段:详情文本里是"58㎡",但也可能写"建面58㎡"、"58平"或"58平米",必须统一抽取数字。
  3. 户型字段:"2室1厅"需要拆成bedroomslivingrooms两个数值列,方便后续按居室数分组。
  4. 区域字段:标题里通常包含"朝阳区"或"海淀区"字样,但位置不一定统一在开头或结尾,需要用区域字典做包含匹配。

清洗代码建议写成函数链,每步只做一件事:

import re def clean_room_data(df): """对抓取结果做字段拆分和单位统一""" def parse_rent(text): if "面议" in text: return None nums = re.findall(r"[\d.]+", text.replace("万", "")) if not nums: return None val = float(nums[0]) return val * 10000 if "万" in text else val def parse_detail(detail): rent_area = re.search(r"([\d.]+)\s*(㎡|平|平米)", detail) rooms = re.search(r"(\d)室(\d)厅", detail) return { "area": float(rent_area.group(1)) if rent_area else None, "bedrooms": int(rooms.group(1)) if rooms else None, "livingrooms": int(rooms.group(2)) if rooms else None, } df["rent_yuan"] = df["rent"].apply(parse_rent) detail_expanded = df["detail"].apply(parse_detail).apply(pd.Series) df = pd.concat([df, detail_expanded], axis=1) df = df.dropna(subset=["rent_yuan", "area"]) return df

两个子函数的写法比较克制,但每一行都有实际意义:parse_rent里先替换"万"再乘 10000,是为了避免正则取到小数点前一位导致量级错误;parse_detail用两个独立正则分别抽取面积和居室数,防止同一个正则里互相干扰。dropna这一步过滤掉"面议"和无面积的无效房源,这类数据如果不过滤,后面算出来的区域均价会被拉低很多。

一个容易忽略的坑是重复房源。同一个小区的同一套房子可能被多个中介重复发布,标题和面积完全一致。建议在清洗后按区域 + 户型 + 面积去重,只保留每天最后一次抓到的记录。如果实验历时较长,还可以加一个crawl_date字段,方便后续做时间维度分析。

3. 实验指标体系与数据口径:区域均价、性价比和价格分层

3.1 计算区域均价前,先决定用均值还是中位数

租房数据天然带长尾分布,一个小区出现几套 300㎡ 的大平层,就能把所在区域的均价拉高 20%。在做数据分析与可视化实验时,最常见的错误是直接对所有房源算算术平均。我一般会同时计算均值和中位数,并在报告里说明口径:

指标计算方式适合场景对异常值的敏感度
区域均价rent_yuan.sum() / rent_yuan.count()描述市场整体水平高,大户型会把均价拉高
区域租金中位数rent_yuan.median()反映普通租客的真实可选择性低,更稳健
每平米租金rent_yuan / area跨区域、跨户型比较中,面积误差会放大偏差

实际写实验报告时,我会把rent_yuanarea都算一个"单位面积价格"列,叫做price_per_sqm。这个字段比绝对租金更能说明问题,因为它消除了面积这个变量。比如朝阳区绝对均价高,但如果算每平米价格,可能反而是海淀区的某些商圈更贵。这正是实验报告里值得写的一段分析结论。

3.2 用五分位数做价格分层,而不是拍脑袋定区间

固定区间(比如 3000 以下、3000-5000、5000-8000)虽然直观,但不同城市价格分布差异很大,固定区间一换城市就失效。更通用做法是用pd.qcut做五分位分层,把样本按订单量均等切分:

import pandas as pd def add_price_tier(df): """按租金五分位数给房源打标签,用于价格分层分析""" try: df["price_tier"] = pd.qcut( df["rent_yuan"], q=5, labels=["低价", "中低价", "中价", "中高价", "高价"] ) except ValueError: df["price_tier"] = "样本不足" return df

qcut这行的关键点是:如果样本里有大量重复的同价位数据(比如一堆 3000 元房源),qcut会报Bin edges must be unique错误。所以用try-except兜底是必须的,不是可选项。分层之后,你可以按层统计户型分布、面积中位数或者区域占比,这些交叉分析都是当前"数据分析项目"类博客的常见看点。

有了price_tier之后,一个很有信息量的图表是"各价格分层下,不同户型的中位面积"的箱线图。它能回答"同样租 5000 块,你在一居室和两居室之间能差多少面积"这种问题,比单纯堆砌"平均租金是多少"要有价值得多——报告评审人看到这种交叉分析会认为你在认真思考业务,而不是把字段挨个展示一遍。

3.3 数据分析项目里最容易漏掉的对比基准

租房的绝对价格没有意义。同一个小区,朝南 vs 朝北可能差 800 元;同一栋楼,高区和低区差 10%;同一区域,步行到地铁 200 米和 800 米可能差 15%。所以实验报告里,除了区域均价柱状图之外,至少要有"同区域不同朝向租金对比"或"同户型不同面积段单价对比"中的一张。

关于这个阈值,不同城市差别较大,所以我不建议写死,而是用数据算。常见做法是:把每个商圈的房源按 500 米、1000 米、1500 米三档归类,分别计算三档的每平米租金中位数,看趋势线是否单调变化。房价和地铁距离的关系不是线性的,多数城市表现为"前 500 米内涨幅最大,超过 1200 米后趋平"。

要注意的是:采集详情页拿到经纬度的工作量远大于列表页,而且安居客详情页结构变动频繁,定位坐标的正则可能随时失效。如果你的实验周期只有一两天,我建议数据来源选一个有坐标系的基础数据集,或者手动给重点商圈打距离标签,不要在这块死磕。

4. Python 数据可视化链路:选型、配色与输出参数

4.1 静态图表优先,交互式图表只做补充

在 PDF 报告里做可视化,最佳策略是"静态为主、交互为辅"。Matplotlib + Seaborn 能覆盖实验报告 95% 的场景:柱状图、箱线图、散点图、热力图。交互式图表(比如 Plotly)适合在网页端探索数据,但导出到 PDF 时经常遇到两个问题:一是图片清晰度撑不住 A4 页面,二是中文字体渲染异常。所以我的方案是:用 Plotly 做探索性的快速分析,找到结论后再用 Matplotlib 复现正式版图表。

一个适合 PDF 报告的可视化项目,图表风格应当统一。具体做法是在脚本开头设置全局参数:

import matplotlib.pyplot as plt import seaborn as sns from matplotlib import font_manager # 设置中文字体和全局风格,避免 PDF 中文乱码 zh_font = font_manager.FontProperties(fname="simhei.ttf") plt.rcParams["font.family"] = "sans-serif" plt.rcParams["axes.unicode_minus"] = False sns.set_theme(style="whitegrid", font=zh_font.get_name()) # 统一定义输出的图片尺寸和分辨率 FIG_WIDTH, FIG_HEIGHT, FIG_DPI = 8, 5, 200

这里有三处参数值得单独解释。axes.unicode_minus必须设为 False,否则坐标轴负号会显示成方块,这是 Python 数据分析与可视化里最常见的字体坑。figsize建议在 8x5 到 10x6 之间,尺寸太小会放大字体失真,太大又会浪费 PDF 版面。dpi设为 200 是平衡文件大小和打印清晰度的经验值,150 也够用,再高对报告没有实际收益。

4.2 一套模板函数覆盖所有报告图表

写实验报告最耗时的环节是同样的坐标轴标签、标题、网格线在每个图里重复写。正确的做法是封装一个统一的绘图函数,把数据和标题传进去,自动生成风格一致的图表:

def save_chart(fig, filename): """输出报告用图,带白边裁剪并压缩""" fig.savefig(f"charts/{filename}", dpi=FIG_DPI, bbox_inches="tight") plt.close(fig) def plot_region_avg_price(region_stats): """区域均价柱状图,自动在柱顶标注数值""" fig, ax = plt.subplots(figsize=(FIG_WIDTH, FIG_HEIGHT)) regions = region_stats.index prices = region_stats["median_price"].values bars = ax.bar(regions, prices, color="#4C72B0", edgecolor="white") for bar, price in zip(bars, prices): ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height(), f"{price:.0f}", ha="center", va="bottom", fontsize=9) ax.set_title("各区域租金中位数对比(单位:元/月)", fontsize=14) ax.set_xlabel("区域") ax.set_ylabel("租金中位数") ax.tick_params(axis="x", rotation=30) fig.tight_layout() return fig

bbox_inches="tight"是最容易被忽视的参数,它会把图表周围多余的白边裁掉,确保插入 PDF 时图表在页面上尽量大、不留空白。hava参数控制数值标注在柱顶的居中方式,这样不会因为数字位数不同而错位。整套模板处理完成后,你在写实验报告时只需要传入一个region_statsDataFrame,就能获得风格统一的全部图表。

4.3 哪些图表值得放进租房分析实验报告

不是所有图表都值得占用 PDF 空间,我的筛选标准是"一张图讲清一个结论":

  • 区域价格分布箱线图:比柱状图信息量大,能同时展示中位数、四分位距和异常值。这个图能直观反映哪些区域内部差价大。
  • 户型-租金小提琴图:展示"整租一居到三居"的租金分布形态,比单纯条形图更细腻。
  • 面积与租金散点图:加一条拟合线,能看出每平米单价的变化趋势,适合判断是否存在"面积溢价"。
  • 热力图(区域 x 户型均价):适合用一个 8x4 的矩阵把两个维度一次性展示,是数据分析与挖掘类课程里容易被加分的一张图。

最大误区是每张图都做,最后报告变成图集。实验报告要克制,一页最多放两张图,并且每张图前后都要有至少两行分析文字,解释这张图证明了什么。

5. 从图表到 PDF:实验报告自动生成的 3 种技术路线

5.1 方案对比:ReportLab、HTML 转 PDF 和 Markdown 工具链

实验报告最后要落成 PDF,目前有 3 条主流路线,选型取决于你对版面控制的要求和已有技术栈:

方案工具链版面控制学习成本适用场景
底层绘制ReportLab最精确,逐元素定位固定模板的批量报告,页眉页脚要求严格
HTML 转 PDFJinja2 + Playwright/Chromium灵活,CSS 完全掌控图文混排、动态图表插入、颜色丰富的报告
Markdown 转换Pandoc + LaTeX/WeasyPrint中,格式依赖模板以文字为主的实验报告,图表只是辅助

我自己的实践经验是:数据分析类实验报告文字段落很多,图表的尺寸和位置又需要反复调整,用 HTML 转 PDF 效率最高。原因很简单——你在浏览器里改 CSS 是即时的,看到的效果基本就是 PDF 最终效果,不需要像 ReportLab 那样先构建坐标再预览。对于独立完成一个数据分析项目的人来说,Jinja2 模板 + Chromium 打印是性价比最高的组合。

用 Playwright 无头浏览器把 HTML 渲染成 PDF,是最接近"前端开发体验"的方式:

import asyncio from playwright.async_api import async_playwright async def render_pdf(html_path, pdf_path): """用 Chromium 无头模式渲染 HTML 为 PDF,支持自定义页边距""" async with async_playwright() as p: browser = await p.chromium.launch(args=["--no-sandbox"]) page = await browser.new_page() await page.goto(f"file://{html_path}", wait_until="networkidle") await page.pdf(path=pdf_path, format="A4", print_background=True, margin={"top": "15mm", "bottom": "15mm", "left": "15mm", "right": "15mm"}) await browser.close() asyncio.run(render_pdf("report.html", "report.pdf"))

print_background这个参数默认是 False,如果 HTML 里给图表容器设置了淡色背景,导出后这些背景会全部丢失,所以必须设为 True。margin的四个值统一设为 15mm,比浏览器默认的 10mm 更符合打印阅读习惯;如果你的图表有阴影效果,可以把左右边距放宽到 18mm 防止被裁切。wait_until="networkidle"是确保页面图片全部加载完成再导出,这对嵌入了大量本地图片的报告尤其重要。

5.2 用 HTML 模板解耦内容与样式

写报告 HTML 时,我习惯把样式写在单独的<style>块里,内容数据用 Jinja2 循环渲染。这样可以保证所有图表段落都是同样的结构,新增章节时只需要在数据源里加记录。

<style> .page { padding: 20px 0; border-bottom: 1px solid #eee; } .chart-box { margin: 20px auto; text-align: center; } .chart-box img { max-width: 100%; height: auto; } .analysis { font-size: 14px; color: #444; line-height: 1.8; } h2 { color: #2c3e50; border-left: 4px solid #4C72B0; padding-left: 10px; } </style> <div class="page"> <h2>{{ section.title }}</h2> <div class="analysis"> {{ section.analysis }} </div> <div class="chart-box"> <img src="{{ section.chart_path }}" alt="{{ section.title }}"> </div> </div>

渲染逻辑用 Jinja2 把实验数据循环填进去。每一个section就是一个分析版块,包含标题、分析文字和图表路径。这样的设计让报告的结构非常清晰:想要增加一个区域分析,只需要在 Python 的sections列表里追加一条记录,不需要改 HTML 模板。这就是 HTML 转 PDF 相对直接写 PDF 最大的优势——内容与展示完全分离。

5.3 PDF 实验报告的中文字体与分页实用技巧

中文字体是生成 PDF 时最大的隐患。Jinja2 渲染的 HTML 在 Chrome 里显示正常,不代表 Playwright 截图和生成 PDF 时字体也能正确嵌入。Linux 服务器上经常缺中文字体,会导致所有文字变成豆腐块。最稳妥的做法是把字体文件(如 simhei.ttf 或 NotoSansCJK)放到项目目录,并在 CSS 里用本地路径引用:

@font-face { font-family: "ReportFont"; src: url('./fonts/simhei.ttf') format('truetype'); } body { font-family: "ReportFont", sans-serif; }

分页方面,page-break-before: always可以让每个分析版块从新一页开始。对图表较多的报告,建议每个章节单独一页,避免图表被从中间截断。你可以在.page上加上page-break-inside: avoid,这样一个版块不会被拆到两页里。

在很多数据分析实验报告中,可视化大屏往往被当作展示终点,但作为 PDF 报告的组成材料,大屏截图反而很难排版。真正得体的做法是:把大屏拆成 2-3 张独立图表嵌入 PDF,让评审者既能看趋势,又能读结论。这一点在你动手写实验报告时值得反复提醒自己:PDF 的核心是叙事节奏,不是图表密度。

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

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

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

立即咨询