资讯动态

Python二手房数据分析全流程:从爬虫采集到自动生成报告

发布时间:2026/9/23 21:10:38 来源:尧图企业网站定制
简介基于Python的二手房数据分析完整源码、文档说明与PPT资料是一份面向毕业设计、期末大作业及课程设计场景的高分项目整体围绕二手房数据的获取、清洗、统计分析与可视化展示展开。代码包含详细注释新手也能理解关键逻辑下载后简单部署即可运行系统功能完善、界面美观、操作便捷已通过严格调试。资料包共157个文件约48.05MB涵盖18个Python脚本、18个CSV数据文件、15个HTML页面、11个JS脚本、65张PNG图片并附有Word文档和PPT答辩材料目录结构清晰易检索。其中CSV文件提供原始数据与UTF-8/ANSI等多版本清洗后结果方便对照数据预处理过程Python脚本覆盖爬取、清洗、统计等环节HTML与JS构成可交互的可视化界面PNG展示图表与界面截图文档说明和PPT资料可直接用于答辩汇报。目前已有127人学习下载项目曾获导师认可适合直接用于毕业设计、期末大作业的参考与二次开发也可作为学习数据分析流程的完整案例。1. 二手房数据分析的高分点不在图多在链路完整当你打开搜索引擎键入“python 二手房数据分析 源码”时八成是课程设计或结课答辩临近了。这几年我看了不少类似项目多数不是死在爬虫代码上而是卡在链路不完整——数据是有的图也画了但一问“字段怎么清洗、单位怎么换算、结论和文档怎么对上”支支吾吾就露馅了。所谓完整指的是把数据采集、清洗整理、分析可视化、文档说明、答辩PPT 这五段串成一条能一遍跑通的流水线。这篇文章就按这条线往下拆我尽量把脚本、参数、踩坑写在对应位置新手能跟着走熟手也能对照自己的写法做减法。2. 数据从哪来手写抓取脚本还是直接用现成数据集2.1 为什么建议先抓真实页面而不是直接下载全量数据集做二手房数据分析最常用到的两个数据来源是链家、贝壳这类公开房源站。很多人第一步就想省事去下载一份网上的“全国二手房数据集”字段看着挺全真跑起来才发现对不上——价格单位有的是万元、有的是每平米单价区域字段还是历史行政区划时间跨度也不透明。答辩时老师问一句“数据怎么来的”你只能说是开源数据集这就丢了第一印象分。我的建议是课设和实战项目都优先自己抓一小批真实页面。一个城市挑两个区域抓两三页房源列表凑一两百条记录足够把后续所有分析跑通。数据规模不大但来源、字段含义、时效性都清楚遇到问题也容易解释。抓取时记住一个原则低频、小批量、只拿自己需要的字段演示用途不要去做全站遍历也不要长时间高频请求。提示这里讨论的是公开网页上展示的挂牌信息抓取前留意目标站点的访问条款个人分析用途保持合理频率即可。2.2 最小可跑的列表页采集脚本requests BeautifulSoup抓二手房列表页常见做法是 requests 拿 HTMLBeautifulSoup 解析不需要上浏览器模拟。链家城市的二手房列表页结构类似页面里每条房源都在一个列表容器里标题、总价、单价各对应一个节点。下面这个脚本把城市、区域、页码参数化抓完直接存 CSV方便后续清洗。import requests from bs4 import BeautifulSoup import time import csv def fetch_list(city: str, district: str, page: int) - list: 抓取列表页的房源摘要信息。 url fhttps://{city}.lianjia.com/ershoufang/{district}/pg{page}/ headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: print(f{city}-{district}-page{page} 状态码: {resp.status_code}) return [] soup BeautifulSoup(resp.text, html.parser) items soup.select(.sellListContent li) rows [] for item in items: title_tag item.select_one(.title a) total_tag item.select_one(.totalPrice span) # 例468 unit_tag item.select_one(.unitPrice span) # 例41236元/平 if not title_tag or not total_tag: continue rows.append({ city: city, district: district, page: page, title: title_tag.get_text(stripTrue), total_price: total_tag.get_text(stripTrue), # 单位万 unit_price: unit_tag.get_text(stripTrue) if unit_tag else , }) return rows if __name__ __main__: all_rows [] # 演示只抓两个区域、各两页控制总量 for d in [xicheng, dongcheng]: for p in range(1, 3): all_rows fetch_list(bj, d, p) time.sleep(2) # 低频请求避免给目标站点造成压力 with open(data/raw_lianjia.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter( f, fieldnames[city, district, page, title, total_price, unit_price] ) writer.writeheader() writer.writerows(all_rows) print(保存, len(all_rows), 条)代码逻辑不复杂但有几个地方要说明。城市和区域都是拼音参数比如北京西城区就是bj加xicheng具体拼法会因站点栏目结构有差异首次运行时先打印一两页响应内容确认 URL 路径能带出数据再去跑全量。select(.sellListContent li)选的是房源列表里的所有列表项类名会随着前端改版而变化抓不到时别急着怀疑代码先看目标页面当前实际结构。select_one(.totalPrice span)只取第一个匹配节点避免把页面的其他价格文本混进来。total_price保存的是去掉“万”字之后的数字文本比如页面显示“468万”这里存的是“468”真正转成数值放到清洗阶段处理这样抓取脚本和分析逻辑分离后面改单位换算时不用回头改爬虫。sleep(2)是显式请求间隔时间越长越稳妥。压缩到 0.5 秒也不是不能用但遇到页面响应异常时很难分清是自己的网络问题还是被服务端限制。我在这个脚本里加了状态码判断和空列表兜底方便第一次联调时定位是路径写错还是解析失败。2.3 清洗与合并单位换算、缺失值、字段类型一次处理完抓下来的数据字段比想象中更像一锅粥。“468”看起来能转 float但有些房源显示“价格待定”单价字段有时带逗号、有时带“元/平”标题形如“南北 3室2厅 89.6平米”面积、朝向、户型全挤在一起。这些不处理干净后面所有聚合计算都会出错。下面这段清洗脚本目标是把原始 CSV 变成一行一房源、字段规整的 DataFrame。import pandas as pd df pd.read_csv(data/raw_lianjia.csv) # 1. 总价转数值去掉“万”字 df[total_price] ( df[total_price] .astype(str) .str.replace(万, , regexFalse) .astype(float) ) # 2. 单价转数值去掉单位和千分位逗号 df[unit_price] ( df[unit_price] .astype(str) .str.replace(元/平, , regexFalse) .str.replace(,, , regexFalse) .astype(float) ) # 3. 从标题提取面积、房间数、厅数、朝向 df[area] df[title].str.extract(r(\d(?:\.\d)?)平米).astype(float) df[rooms] df[title].str.extract(r(\d)室).astype(Int64) df[halls] df[title].str.extract(r(\d)厅).astype(Int64) df[direction] df[title].str.extract(r([东西南北]{1,4})) # 4. 关键字段缺失的直接丢弃保留可计算样本 df df.dropna(subset[total_price, unit_price, area]) df.to_csv(data/cleaned_lianjia.csv, indexFalse, encodingutf-8-sig) print(清洗后剩余, len(df), 条)这段代码里有三个细节值得较真。第一str.replace(万, , regexFalse)里的regexFalse表示按普通字符串匹配如果写成regexTrue“万”字本身没有正则特殊含义不影响结果但养成显式指定的习惯能避免下次替换“元/平”时把/当成边界符处理。第二extract(r(\d(?:\.\d)?)平米)中\d匹配整数部分(?:\.\d)?匹配可能出现的小数部分这样“89.6平米”和“90平米”都能提取。第三astype(Int64)保留可空整数房间数缺失时显示NA不会像普通 float 那样变成 1.0。清洗完再看数据量如果比原始数据少很多说明页面上有很多价格、面积缺漏的房源这类记录在后续分析里没有计算价值弃掉比强行填中位数更安全也能在答辩时解释清楚自己做了有效样本筛选。3. 让数据开口用 pandas 对比区域价格、面积和总价的关系3.1 先看总体分布再用中位数下结论拿到干净表格第一件要做的事不是画图而是跑描述统计。尤其是总价和单价看看均值和中位数差多少心里先有数。均值明显大于中位数说明样本右偏少数高总价房源把平均值拉上去了这时候写报告就不能说“平均单价是 X”最好用“中位数单价更接近多数房源的挂牌水平”。import pandas as pd import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False df pd.read_csv(data/cleaned_lianjia.csv) print(df[[total_price, unit_price, area]].describe()) fig, ax plt.subplots(figsize(8, 5)) ax.hist(df[total_price], bins30, alpha0.75) ax.set_xlabel(总价万) ax.set_ylabel(房源数量) ax.set_title(样本总价分布) fig.savefig(output/fig_dist_total_price.png, dpi150, bbox_inchestight)describe()输出的 count、mean、50% 分位数是答辩时最常被追问的四个数字建议截图或摘到文档里。直方图只负责给直观感受不要一画完就急着说“房价集中在这几个区间”先确认样本量够不够比如只有 50 条分布图意义就非常有限。3.2 区域均价对比groupby 聚合时把样本量也带出来区域是最值得做的维度。不同区域在房源数量上天然不均衡有的区只有零星几套挂牌算出来的均价很容易被极端值带着跑。所以我做 groupby 时习惯同时聚合出样本量、均值、中位数宁可多一列也不能让图表暴露盲点。region_stats ( df.groupby(district) .agg( sample_count(total_price, size), avg_total_price(total_price, mean), avg_unit_price(unit_price, mean), median_unit_price(unit_price, median), ) .reset_index() .sort_values(avg_unit_price, ascendingFalse) .round(1) ) print(region_stats) fig, ax plt.subplots(figsize(9, 5)) ax.bar(region_stats[district], region_stats[avg_unit_price]) ax.set_ylabel(挂牌单价均值元/平) ax.set_title(不同区域二手房挂牌单价对比) for i, v in enumerate(region_stats[avg_unit_price]): ax.text(i, v, f{v:.0f}, hacenter, vabottom) fig.autofmt_xdate(rotation45) fig.savefig(output/fig_region_bar.png, dpi150, bbox_inchestight)这段代码里agg()的列名就是最终 DataFrame 的列名比rename再改一次省事。sample_count必须看如果一个区域只有两条样本那它的均值参考价值就要打折写文档时最好加一句“样本量较少结论仅作参考”。柱状图顶部加数值标签方便PPT里直接读数字不用再回头翻表格。想再多看一层数据分布可以画箱线图df.boxplot(columnunit_price, bydistrict, figsize(10, 5))一眼能看出哪个区域内部差异大。有的区域既有老破小也有次新改善盘箱体长、离群点多这本身就是一个可以写进报告的分析点。3.3 面积与总价的关系散点图加相关系数结论才立得住面积和总价的关系是二手房分析里最好解释、也最容易被质疑的点。散点图画出来大多数样本会沿一条斜向上的带子分布但总有远低于趋势线的“低价大房”或者异常高的“高价小房”不解释清楚会被认为分析不严谨。fig, ax plt.subplots(figsize(8, 6)) ax.scatter(df[area], df[total_price], alpha0.4) ax.set_xlabel(建筑面积平米) ax.set_ylabel(挂牌总价万) ax.set_title(面积与总价的关系) fig.savefig(output/fig_area_total.png, dpi150, bbox_inchestight) corr df[area].corr(df[total_price]) print(面积与总价相关系数, round(corr, 3))alpha0.4是让散点透明一点样本重叠时也能看出密度差异。corr()算的是 Pearson 相关系数这个数字可以直接写进文档但要说清楚它衡量的是线性相关不代表因果关系。面积大的房子总价高是因为面积本身是重要定价因素还有地段、楼层、装修、楼龄这些变量没进模型所以散点图之后最好补一句“影响总价的因素是多维的面积只说明其中一个方向”。3.4 图表输出规范中文字体、dpi、图注一个不落很多人第一次跑 matplotlib 出图中文全变成方框原因是默认字体里没有中文字形。开头那两行plt.rcParams就是全局设置放多个字体名称程序会按顺序找可用的那个。Windows 下通常命中 SimHei 或 Microsoft YaHeimacOS 命中 PingFang SCLinux 服务器上如果都没有可以安装fonts-noto-cjk包再把 Noto Sans CJK SC 加进列表。dpi150是折中值屏幕上看清晰投屏也不会糊bbox_inchestight自动裁剪空白边避免 PPT 里图片四周留大片白。做图时把savefig和文件名的对应关系记下来后面生成报告和 PPT 都会按这些文件名找图命名一旦混乱交付阶段会反复修路径。4. 把源码、文档说明和答辩 PPT 组织成可交付的高分项目4.1 源码目录和 README 怎么写检查官如何判断“可运行”一个能拿高分的项目代码能跑只是前提更关键的是别人拿到手能按顺序复现。我不建议把所有逻辑堆在一个main.py里至少按功能拆出抓取、清洗、分析、报告、PPT 五个文件数据单独建目录图片和文档输出到output。house_price_project/ ├── src/ │ ├── crawler.py │ ├── clean.py │ ├── analyze.py │ ├── report.py │ └── slides.py ├── data/ │ ├── raw_lianjia.csv │ └── cleaned_lianjia.csv ├── output/ │ ├── figures/ │ └── analysis_report.docx ├── README.md └── requirements.txtrequirements.txt 里写清依赖比在 README 里用文字描述“需要 pandas、matplotlib”更标准requests beautifulsoup4 pandas matplotlib python-docx python-pptxREADME 开头放一段“运行顺序”评审老师通常会照着敲一遍pip install -r requirements.txt python src/crawler.py python src/clean.py python src/analyze.py python src/report.py python src/slides.py每个脚本之间通过 CSV 和图片文件传递数据不共享内存变量这样即使某一步挂掉前面的产出物还在排查成本低很多。这也是我在做项目时比较推荐的风格脚本之间用文件解耦而不是一个大的 Notebook 从上往下跑。4.2 用 python-docx 自动生成 Word 文档说明项目文档不需要从零手敲分析结论已经在那了用python-docx批量把段落和图片写入 Word既省时间也能保证报告里的数字和代码运行结果一致。from docx import Document from docx.shared import Inches from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent OUTPUT_DIR BASE_DIR / output doc Document() doc.add_heading(二手房数据分析项目说明, 0) doc.add_heading(1. 项目目标, level1) doc.add_paragraph(本项目采集二手房公开挂牌数据完成清洗、分析与可视化形成区域价格对比和面积-总价关系两类结论并同步产出文档与演示材料。) doc.add_heading(2. 数据说明, level1) doc.add_paragraph(数据来源为链家公开页面采集字段包括标题、总价、单价、区域、面积等。原始数据与清洗后数据分别保存在 data 目录下。) doc.add_heading(3. 分析结论, level1) sections [ (3.1 总价分布, 样本总价分布集中在 300-800 万区间。, fig_dist_total_price.png), (3.2 区域对比, 区域之间挂牌单价差异明显样本量较少的区域结论需谨慎。, fig_region_bar.png), (3.3 面积与总价, 面积与总价呈正相关散点基本呈线性聚集。, fig_area_total.png), ] for title, text, img in sections: doc.add_heading(title, level2) doc.add_paragraph(text) img_path OUTPUT_DIR / img if img_path.exists(): doc.add_picture(str(img_path), widthInches(6)) doc.save(str(OUTPUT_DIR / analysis_report.docx))关键点在Path(__file__).resolve().parent.parent不管当前 shell 在哪个目录运行都能正确找到项目根目录再拼出 output 下的图片路径。图片存在才插入否则跳过这样即使某张图生成失败文档生成脚本也不会整体崩掉。文档结构不要套太多层标题、来源、分析结论、图表、附注就够。写结论时要克制一图一段话是最稳的格式图片是用来支撑文字判断的不是用来撑版面的。4.3 用 python-pptx 把关键图表拼成答辩幻灯片PPT 同理用python-pptx按配置列表生成图片标题一句话、正文一句话、图表一张干净利落。答辩老师不指望你在 PPT 里把报告全文复述一遍他要看到的是逻辑主线。from pptx import Presentation from pptx.util import Inches from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent OUTPUT_DIR BASE_DIR / output slides_config [ (封面, 基于 Python 的二手房数据分析, None), (数据来源, 采集自链家公开页面字段标题、总价、单价、区域、面积, None), (总价分布, 样本总价集中在 300-800 万呈右偏分布, fig_dist_total_price.png), (区域对比, 中心区域挂牌均价更高样本量差异明显, fig_region_bar.png), (面积与总价, 面积与总价呈正相关散点近似线性聚集, fig_area_total.png), (总结, 数据链路完整结论可复现图表与文档自动生成, None), ] prs Presentation() for title_text, body_text, img_name in slides_config: slide prs.slides.add_slide(prs.slide_layouts[1]) slide.shapes.title.text title_text body slide.placeholders[1] body.text body_text if img_name: img_path OUTPUT_DIR / img_name if img_path.exists(): slide.shapes.add_picture( str(img_path), Inches(1), Inches(1.6), widthInches(8) ) prs.save(str(OUTPUT_DIR / project_slides.pptx))slides_config列表是图文对应关系新增一页就在列表里加一条不需要改生成逻辑。说明文字尽量压在 20 字以内答辩现场有口头扩展空间PPT 上写太多老师反而不知道该听你还是看屏幕。这套流程跑完你手里会有源码、清洗后的数据、几张核心图表、一份 Word 文档和一份 PPT。别人问起你可以说从数据采集到报告生成全链路都是脚本自动化的。这句话本身就是项目的加分项。5. 二手房数据分析避坑清单5 条本地复现时的常见翻车点5.1 抓取环节选择器失效、请求被限坑 1脚本跑完CSV 里一条数据都没有items是空列表。现象select(.sellListContent li)返回空程序不报错循环体根本没执行。原因目标站点前端改版或者不同城市页面用了不同的列表容器类名写死的选择器已经对不上当前 DOM 结构。解决先抓一页 HTML 到本地用浏览器开发者工具检查房源列表实际使用的容器类名再改select()参数。不要盲改先打印响应文本的前 500 个字符确认拿到的是正常页面而不是验证页。坑 2连续跑几页之后返回的状态码变成 302 或 403越往后越明显。现象前几页正常到第 5、6 页开始拿不到有效数据requests 返回的是跳转或拦截页面。原因请求频率超过对方容忍范围或者请求头太像脚本没有携带浏览器关键头字段。解决用干净的 User-Agenttime.sleep(2)起步不要开多线程并发。演示项目抓一两百条足够了慢一点问题不大断了大不了重跑千万不要去研究绕过策略投入产出比很差。5.2 清洗与分析环节编码和单位是高发区坑 3CSV 用 Excel 打开中文全部乱码用 pandas 读却正常。现象同一份文件pandas 显示中文没问题Excel 打开后“标题”列变成æ··之类。原因df.to_csv()默认用 utf-8 编码而 Excel 对无 BOM 的 utf-8 识别不友好会按本地编码解析。解决保存 CSV 时写encodingutf-8-sig。这个技巧在处理所有中文数据导出时都适用报告里也建议注明“所有输出文件统一使用 utf-8-sig”。坑 4清洗时astype(float)直接抛 ValueError把整个管道中断。现象total_price列里出现“价格待定”“暂无数据”等非数字文本df[total_price].astype(float)报错。原因页面上部分房源挂牌信息不完整默认文案混进了原本应该是数字的字段。解决先统一astype(str)兜底再用pd.to_numeric(..., errorscoerce)把非法值转成 NaN最后dropna筛掉。不要用 try-except 逐行判断pandas 的向量化处理一次全搞定。5.3 交付环节图片路径和运行目录坑 5Word 和 PPT 生成成功打开却发现图片是碎的日志里没有报错。现象从src目录启动脚本一切正常从项目根目录启动就找不到图片。原因代码里用了相对路径output/fig.png实际工作目录不同相对路径指向的位置也就不一样。解决在脚本开头统一用BASE_DIR Path(__file__).resolve().parent.parent拼绝对路径所有读图和存图操作都基于OUTPUT_DIR完成。这五条不是个别情况是我见过很多次的实际问题。尤其第 4 条和第 5 条前者让你一夜回到解放前后者让你在答辩现场对着白板说不出话。把这些代码级细节处理掉项目的可复现性会提升一个档次。6. 把项目做成可复用的管道加一个数据校验钩子进阶用法不是堆更多图表而是给分析管道加一道保险。清洗完数据之后顺手跑一个校验函数确认样本数量、字段完整度、单价合不合理再继续往下走。def validate_cleaned(df, min_rows: int 100) - None: assert len(df) min_rows, f样本过少当前 {len(df)} 条 assert df[total_price].notna().all(), 存在空总价 assert df[unit_price].between(1000, 100000).all(), 单价超出合理区间 print(f校验通过有效样本 {len(df)} 条)把这段函数放在clean.py末尾每次跑完清洗就执行一次。后续改了爬虫选择器或者正则表达式如果引入大规模空值校验会立刻报警而不是让脏数据悄悄流进后面的分析和报告。这个习惯救过我很多次尤其是隔了两周再回来改代码很容易忘记前面某一步做了什么假设。再往远一步区域、面积、总价这三列已经足够做一个基线回归模型用sklearn的LinearRegression预测总价然后在文档里和真实挂牌价做对比。但要注意二十到三十个特征都不到的自变量样本就两三百条模型结果只能当分析辅助不能当结论主体。答辩时把模型定位为“方向探索”比硬解释系数更稳。复盘我做这类项目的经验最大的教训是把项目当一个简化版数据中台来做而不是一次性的课程作业。能复现、能校验、能自动产出文档和 PPT这个项目交出去无论是评审还是面试官看到的都是标准工程化思维下的完整交付。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价