
简介一份基于安居客真实租房数据的完整分析与可视化实验报告以深圳为例面向 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, pages5): 抓取安居客租房列表页提取核心字段 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, headersheaders, timeout10) 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(stripTrue), rent: item.select_one(.zu-side .price).get_text(stripTrue), detail: item.select_one(.zu-info .details-item).get_text( , stripTrue) }) return pd.DataFrame(records)代码逻辑很简单遍历指定页数用 CSS 选择器定位房源卡片抽出标题、租金和详情文本。其中rent字段取出的是字符串后面需要拆出数值detail是2室1厅 58㎡ 南 20/32层这种拼接文本也需要单独拆分。在实际抓取时必须设User-Agent否则很容易触发基础反爬如果页面结构变动导致.zu-itemmod匹配不到优先打开浏览器开发者工具重新定位选择器。2.2 出租信息最脏的 4 个字段不洗没法分析列表页抓回来的数据不会直接可用我实践下来有四个重灾区租金字段文本可能是4500元/月、面议、1.5万元/月甚至带押一付三前缀。需要用正则把数字和单位拆开统一折算成元/月。面积字段详情文本里是58㎡但也可能写建面58㎡、58平或58平米必须统一抽取数字。户型字段2室1厅需要拆成bedrooms和livingrooms两个数值列方便后续按居室数分组。区域字段标题里通常包含朝阳区或海淀区字样但位置不一定统一在开头或结尾需要用区域字典做包含匹配。清洗代码建议写成函数链每步只做一件事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], axis1) 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_yuan和area都算一个单位面积价格列叫做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], q5, labels[低价, 中低价, 中价, 中高价, 高价] ) except ValueError: df[price_tier] 样本不足 return dfqcut这行的关键点是如果样本里有大量重复的同价位数据比如一堆 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(fnamesimhei.ttf) plt.rcParams[font.family] sans-serif plt.rcParams[axes.unicode_minus] False sns.set_theme(stylewhitegrid, fontzh_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(fcharts/{filename}, dpiFIG_DPI, bbox_inchestight) 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, edgecolorwhite) for bar, price in zip(bars, prices): ax.text(bar.get_x() bar.get_width() / 2, bar.get_height(), f{price:.0f}, hacenter, vabottom, fontsize9) ax.set_title(各区域租金中位数对比单位元/月, fontsize14) ax.set_xlabel(区域) ax.set_ylabel(租金中位数) ax.tick_params(axisx, rotation30) fig.tight_layout() return figbbox_inchestight是最容易被忽视的参数它会把图表周围多余的白边裁掉确保插入 PDF 时图表在页面上尽量大、不留空白。ha和va参数控制数值标注在柱顶的居中方式这样不会因为数字位数不同而错位。整套模板处理完成后你在写实验报告时只需要传入一个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(ffile://{html_path}, wait_untilnetworkidle) await page.pdf(pathpdf_path, formatA4, print_backgroundTrue, 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_untilnetworkidle是确保页面图片全部加载完成再导出这对嵌入了大量本地图片的报告尤其重要。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 classpage h2{{ section.title }}/h2 div classanalysis {{ section.analysis }} /div div classchart-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 的核心是叙事节奏不是图表密度。本文还有配套的精品资源点击获取