ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

南京二手房数据项目源码:从采集清洗到可视化全链路实战

南京二手房数据项目源码:从采集清洗到可视化全链路实战

简介:这份资源是面向计算机相关专业学生与项目实战学习者的Python数据采集与可视化完整项目包,以南京二手房为分析对象,可用于课程设计、期末大作业或毕业设计参考。项目围绕二手房数据的抓取、清洗与可视化展开,包含原始数据、清洗后数据及多版本编码文件,便于对比数据预处理前后的差异。压缩包共157个文件,约40.02MB,其中18个py脚本承载采集与绘图逻辑,18个csv与15个html、11个js构成数据与可视化页面,另有65个png图表、pptx汇报文件及ini、txt等配置说明,结构完整、层次清晰。目前已有618人学习下载。代码经过严格调试,下载即用,可直接运行并复现分析流程,同时配套PPT资料,方便整理答辩思路与展示成果,适合需要快速搭建项目框架、理解数据采集到可视化全链路的学习者参考借鉴。

1. 南京二手房数据项目:从 CSV 到可视化,这套源码能省你多少时间

如果你正在做 Python 课程设计或期末大作业,选题是「数据采集 + 可视化分析」,大概率会遇到一个尴尬局面:爬虫代码能跑,但数据清洗完就不知道怎么出图;或者图表能画,但数据字段对不上、中文乱码、坐标轴挤成一团。这套南京二手房项目源码,核心价值就在于把「采集 → 清洗 → 存储 → 可视化」整条链路串起来了,而且附带了从原始 CSV 到清洗后 CSV 的完整中间产物,你能清楚看到每一步数据长什么样。

项目面向计算机相关专业的课程设计和毕业设计场景,代码经过调试,下载后按依赖装好就能运行。它不是一个只贴几行爬虫的 demo,而是包含多份数据文件、可视化脚本和 PPT 资料的完整交付物。适合两类人:一是需要快速搭出可演示系统的同学,二是想拿真实数据练 pandas 和 matplotlib 的入门者。下面我按实际拆包顺序,把数据文件、清洗逻辑、可视化实现和常见翻车点逐个讲透。

2. 数据文件与采集链路:ershoufang-origin 到 clean 的字段演变

2.1 原始 CSV 的编码差异与字段结构

拿到压缩包后,第一件事不是急着跑代码,而是先看清数据文件的命名规律。项目里出现了ershoufang-origin-utf8.csv、ershoufang-origin-ansi.csv、ershoufang-clean-utf8-v1.1.csv、ershoufang-clean-ansi-v1.1.csv以及ershoufang - 20000.csv、ershoufang.csv这几类文件。命名里的origin表示采集后的原始数据,clean表示清洗后的版本,utf8和ansi是两种编码格式,v1.1是清洗规则迭代后的版本号。

原始数据通常包含这些字段:小区名称、户型、面积、朝向、楼层、装修情况、挂牌价格、单价、区域、经纬度等。南京二手房数据有个特点,鼓楼、玄武、建邺这几个区的单价跨度很大,从两万多到七八万都有,做可视化时如果不做区域分组,图表会被极端值拉扁。ershoufang - 20000.csv这个文件名里的 20000 大概率是采集条数或价格筛选阈值,实际使用时先head看一眼确认。

import pandas as pd # 先探测编码,不要直接指定 utf-8,ansi 文件用 utf-8 读会报 UnicodeDecodeError raw_utf8 = pd.read_csv('ershoufang-origin-utf8.csv', encoding='utf-8') print(raw_utf8.shape) print(raw_utf8.columns.tolist()) print(raw_utf8.head(3)) # ansi 编码在 Windows 下常见,用 gbk 或 gb18030 读取 raw_ansi = pd.read_csv('ershoufang-origin-ansi.csv', encoding='gb18030') print(raw_ansi.shape)

这段代码的关键点是编码探测。很多同学拿到 CSV 直接pd.read_csv不写 encoding,在 Windows 上默认走 cp936,遇到 utf-8 文件就乱码;在 Linux 或 Mac 上默认 utf-8,遇到 ansi 文件又报错。稳妥做法是先读前几行试编码,或者用chardet检测。shape和columns是必看项,确认行数和字段名跟预期一致,避免后面清洗时字段对不上。

2.2 采集脚本的请求构造与翻页逻辑

项目源码里的采集部分,常见做法是用requests配合BeautifulSoup或lxml解析列表页。南京二手房平台(如链家、安居客)的列表页 URL 通常带page参数,翻页就是改这个参数。请求头里必须带User-Agent,否则大概率返回 403 或空页面。采集频率要控制,time.sleep(1)到time.sleep(3)是常见区间,太快会被限流,太慢两万条数据要跑很久。

import requests from bs4 import BeautifulSoup import time import csv headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } def fetch_page(page_num): url = f'https://nj.example.com/ershoufang/pg{page_num}/' resp = requests.get(url, headers=headers, timeout=10) resp.encoding = 'utf-8' if resp.status_code != 200: print(f'page {page_num} status {resp.status_code}') return [] soup = BeautifulSoup(resp.text, 'lxml') items = soup.select('.sellListContent li') rows = [] for item in items: title = item.select_one('.title a') price = item.select_one('.totalPrice span') unit = item.select_one('.unitPrice span') rows.append({ 'title': title.get_text(strip=True) if title else '', 'total_price': price.get_text(strip=True) if price else '', 'unit_price': unit.get_text(strip=True) if unit else '' }) return rows all_rows = [] for pg in range(1, 101): rows = fetch_page(pg) all_rows.extend(rows) print(f'page {pg} got {len(rows)} rows') time.sleep(2) with open('ershoufang.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=['title', 'total_price', 'unit_price']) writer.writeheader() writer.writerows(all_rows)

这段采集代码的逻辑说明:headers里的User-Agent是必须项,缺了会被识别为脚本请求;timeout=10防止某个请求卡死拖垮整个循环;resp.encoding = 'utf-8'显式指定编码,避免中文乱码;select选择器要根据目标页面实际 DOM 结构调整,不同平台类名不一样。翻页范围range(1, 101)对应一百页,每页约二十条,总量两千条左右,如果要凑两万条需要扩大范围或按区域拆分采集。time.sleep(2)是礼貌间隔,实际跑的时候根据目标站点响应速度调整,但不要低于 1 秒。

提示:采集前先手动翻几页确认 URL 规律和 DOM 结构,不要直接套用示例选择器。平台改版后类名会变,这是最常见的翻车原因。

3. 数据清洗与字段处理:从原始 CSV 到 clean-v1.1 的转换规则

3.1 价格、面积字段的数值化与异常值处理

原始数据里的价格和面积通常是带单位的字符串,比如「320万」「89.5平米」「3室2厅」。做可视化之前必须把这些转成数值类型,否则 matplotlib 没法画。清洗的核心步骤包括:去掉单位、转 float、处理缺失值、剔除极端异常值。clean-v1.1这个版本号说明清洗规则至少迭代过一次,常见改动是增加了异常值过滤或修正了某个字段的解析逻辑。

import pandas as pd import numpy as np df = pd.read_csv('ershoufang-origin-utf8.csv', encoding='utf-8') # 价格字段:去掉「万」字,转 float df['total_price'] = df['total_price'].astype(str).str.replace('万', '', regex=False) df['total_price'] = pd.to_numeric(df['total_price'], errors='coerce') # 面积字段:去掉「平米」等后缀 df['area'] = df['area'].astype(str).str.extract(r'(\d+\.?\d*)') df['area'] = pd.to_numeric(df['area'], errors='coerce') # 单价字段:去掉「元/平米」并转数值 df['unit_price'] = df['unit_price'].astype(str).str.replace('元/平米', '', regex=False) df['unit_price'] = pd.to_numeric(df['unit_price'], errors='coerce') # 剔除面积或价格为空的行 df = df.dropna(subset=['total_price', 'area', 'unit_price']) # 用 IQR 方法过滤单价异常值 q1 = df['unit_price'].quantile(0.25) q3 = df['unit_price'].quantile(0.75) iqr = q3 - q1 lower = q1 - 1.5 * iqr upper = q3 + 1.5 * iqr df = df[(df['unit_price'] >= lower) & (df['unit_price'] <= upper)] print(f'after clean: {df.shape}') df.to_csv('ershoufang-clean-utf8-v1.1.csv', index=False, encoding='utf-8')

逻辑说明:str.replace去掉单位后,pd.to_numeric配合errors='coerce'把无法转换的值变成 NaN,再用dropna剔除。str.extract用正则从「89.5平米」里抽出数字部分,比直接 replace 更稳,因为面积字段的格式可能不统一。IQR 过滤是常见的异常值处理方式,把超出 1.5 倍四分位距的数据去掉,避免个别天价房源把图表拉变形。参数lower和upper可以根据实际数据分布调整,如果发现过滤后数据量骤减,说明阈值太紧,改成 3 倍 IQR 试试。

3.2 区域字段标准化与户型拆分

南京的行政区划字段在原始数据里可能有多种写法,比如「鼓楼」「鼓楼区」「南京鼓楼」混在一起。做区域维度的可视化之前,需要统一成标准名称。户型字段「3室2厅」可以拆成「室数」和「厅数」两个数值列,方便后续做户型分布统计。

# 区域标准化:去掉「区」「南京」等前后缀 df['district'] = df['district'].astype(str).str.replace('南京', '', regex=False) df['district'] = df['district'].str.replace('区', '', regex=False) df['district'] = df['district'].str.strip() # 户型拆分:从「3室2厅」提取室数和厅数 df['room'] = df['layout'].astype(str).str.extract(r'(\d+)室') df['hall'] = df['layout'].astype(str).str.extract(r'(\d+)厅') df['room'] = pd.to_numeric(df['room'], errors='coerce') df['hall'] = pd.to_numeric(df['hall'], errors='coerce') # 按区域统计均价 district_avg = df.groupby('district')['unit_price'].mean().sort_values(ascending=False) print(district_avg)

区域标准化这步看起来简单,但实际数据里经常有「江宁」「江宁区」「南京江宁区」三种写法并存,不统一的话 groupby 会分成三组,统计结果就错了。户型拆分用正则提取数字,errors='coerce'处理「未知」「暂无」这类非标准值。groupby后的mean()默认跳过 NaN,但如果某个区域全是 NaN,结果里会出现空值,需要额外处理。

注意:清洗后的文件同时保留了 utf8 和 ansi 两个版本,这是为了兼容不同操作系统。Windows 下用 Excel 打开 utf8 文件可能乱码,ansi 版本就是给这种情况准备的。写代码时统一用 utf8 版本,避免编码问题。

4. 可视化实现与图表选型:matplotlib 出图的关键参数

4.1 区域均价柱状图与横坐标拥挤处理

区域均价对比是最常见的可视化需求,用柱状图最直观。但南京有十几个区,如果全画上去,横坐标标签会挤成一团,这是 Python 画图的高频问题。解决办法有三种:旋转标签、只画 Top N、或者用横向柱状图。我一般用横向柱状图,区域名放纵轴,可读性最好。

import matplotlib.pyplot as plt import matplotlib # 设置中文字体,Windows 用 SimHei,Mac 用 Arial Unicode MS matplotlib.rcParams['font.sans-serif'] = ['SimHei'] matplotlib.rcParams['axes.unicode_minus'] = False district_avg = df.groupby('district')['unit_price'].mean().sort_values(ascending=True) fig, ax = plt.subplots(figsize=(10, 6)) bars = ax.barh(district_avg.index, district_avg.values, color='#4C72B0') ax.set_xlabel('均价(元/平米)') ax.set_title('南京各区二手房均价对比') # 在柱状图末端标注数值 for bar in bars: width = bar.get_width() ax.text(width, bar.get_y() + bar.get_height()/2, f'{width:.0f}', ha='left', va='center', fontsize=9) plt.tight_layout() plt.savefig('district_avg.png', dpi=150) plt.show()

逻辑说明:font.sans-serif设置中文字体,不设的话中文会显示成方块,这是新手最常遇到的玄学问题。axes.unicode_minus = False解决负号显示异常。barh是横向柱状图,区域名在纵轴不会拥挤。ax.text在每根柱子末端标数值,ha='left'让文字左对齐到柱子末端。tight_layout自动调整边距,防止标签被裁掉。dpi=150保证保存的图片清晰度够用。

4.2 价格分布直方图与户型饼图

除了区域对比,价格分布和户型占比也是课程设计里常要求的图表。价格分布用直方图看集中区间,户型用饼图看占比。直方图的bins参数决定柱子数量,太少看不出分布细节,太多又显得杂乱,一般设 20 到 30 之间。

fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 价格分布直方图 axes[0].hist(df['unit_price'], bins=25, color='#55A868', edgecolor='white') axes[0].set_xlabel('单价(元/平米)') axes[0].set_ylabel('房源数量') axes[0].set_title('南京二手房单价分布') # 户型分布饼图,只取前 6 种户型 layout_counts = df['layout'].value_counts().head(6) axes[1].pie(layout_counts.values, labels=layout_counts.index, autopct='%1.1f%%', startangle=90) axes[1].set_title('户型占比分布') plt.tight_layout() plt.savefig('price_layout.png', dpi=150) plt.show()

直方图的bins=25是经验值,数据量在两万条左右时这个粒度比较合适。edgecolor='white'让柱子之间有白色分隔线,视觉上更清晰。饼图的autopct显示百分比,startangle=90让第一块从十二点方向开始,符合阅读习惯。head(6)只取前六种户型,避免饼图切片太多看不清。

提示:如果横坐标标签仍然拥挤,可以用plt.xticks(rotation=45)旋转,或者用ax.xaxis.set_major_locator控制刻度密度。但横向柱状图通常比旋转标签更优雅。

5. 避坑与排查:编码、字段、图表三类高频问题

5.1 中文乱码:现象是图表中文变方块,原因是字体未设置

现象:matplotlib 画出来的图里,标题和轴标签的中文全部显示成方块或问号。原因:matplotlib 默认字体不含中文字形,需要显式指定支持中文的字体。解决:在绘图前设置matplotlib.rcParams['font.sans-serif'],Windows 用SimHei,Mac 用Arial Unicode MS,Linux 用WenQuanYi Micro Hei。如果设置后仍不生效,检查字体是否真的安装在系统里,可以用matplotlib.font_manager.findfont确认。

5.2 CSV 读取报错:现象是 UnicodeDecodeError,原因是编码不匹配

现象:pd.read_csv报UnicodeDecodeError: 'utf-8' codec can't decode byte。原因:文件实际编码是 ansi/gbk,但读取时指定了 utf-8,或者反过来。解决:先用chardet.detect检测文件编码,或者直接用encoding='gb18030'读取 ansi 版本。gb18030 兼容 gbk 和 gb2312,覆盖面更广。如果不想改代码,用文本编辑器把文件另存为 utf-8 也行。

5.3 字段提取为空:现象是正则匹配后整列 NaN,原因是字段格式与预期不符

现象:用str.extract提取面积或户型后,整列都是 NaN。原因:原始数据里该字段的格式跟正则不匹配,比如面积写的是「89.5㎡」而不是「89.5平米」,或者户型写的是「三室两厅」而不是「3室2厅」。解决:先df['area'].head(20)看实际格式,再调整正则。中文数字和阿拉伯数字混用的情况,需要额外做映射转换。

5.4 图表保存空白:现象是 savefig 出来的图片是白图,原因是 show 和 savefig 顺序问题

现象:plt.savefig保存的图片打开后是空白,但plt.show弹出的窗口正常。原因:在某些后端下,plt.show()会清空当前 figure,如果先 show 再 savefig,保存的就是空图。解决:先savefig再show,或者用fig.savefig代替plt.savefig。这个坑在 Jupyter Notebook 里尤其常见。

5.5 采集被封:现象是请求返回 403 或验证码页面,原因是请求频率过高或缺少请求头

现象:采集脚本跑了几十页后开始返回 403,或者页面内容变成验证码。原因:请求频率太高被目标站点识别为异常流量,或者User-Agent缺失被直接拒绝。解决:增大time.sleep间隔到 3 到 5 秒,补全headers里的User-Agent、Referer等字段,必要时轮换 User-Agent。如果仍然被封,考虑降低采集量或分时段采集。

6. 进阶技巧:用 pyecharts 做交互式可视化与 PPT 联动

课程设计如果只交静态图,演示效果会打折扣。这套项目附带了 PPT 资料,说明作者考虑了答辩场景。我的习惯是在 matplotlib 静态图之外,再用 pyecharts 做一版交互式图表,答辩时鼠标悬停能看具体数值,观感提升明显。pyecharts 的安装用pip install pyecharts,生成的是 HTML 文件,浏览器直接打开就能交互。

from pyecharts.charts import Bar, Pie from pyecharts import options as opts # 区域均价交互柱状图 district_avg = df.groupby('district')['unit_price'].mean().sort_values(ascending=False).head(10) bar = ( Bar() .add_xaxis(district_avg.index.tolist()) .add_yaxis('均价', district_avg.values.round(0).tolist()) .set_global_opts( title_opts=opts.TitleOpts(title='南京各区二手房均价 Top10'), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=30)), yaxis_opts=opts.AxisOpts(name='元/平米') ) ) bar.render('district_avg_interactive.html')

这段代码的逻辑:Bar()创建柱状图对象,add_xaxis和add_yaxis分别设置横轴和纵轴数据,set_global_opts配置标题和轴标签。rotate=30旋转横轴标签防止拥挤,跟 matplotlib 的思路一致但参数写法不同。render输出 HTML 文件,可以直接嵌入 PPT 的超链接里,答辩时点击跳转。

pyecharts 和 matplotlib 的选型区别:matplotlib 适合生成静态图片插入论文或 PPT,pyecharts 适合现场演示交互。如果时间充裕,两个都做;如果只选一个,答辩场景优先 pyecharts,论文场景优先 matplotlib。另外,PPT 里的图表建议用清洗后的clean-v1.1数据重新生成一遍,保证数据口径一致,避免 PPT 里的数字和代码跑出来的对不上。

还有一个细节:pyecharts 生成的 HTML 文件默认引用在线 CDN 的 JS 库,如果答辩现场网络不稳定,图表可能加载不出来。解决办法是用pyecharts的离线资源模式,或者提前把 HTML 在本地浏览器打开确认能正常渲染。我一般会在答辩前一天把所有 HTML 文件在目标电脑上过一遍,确认字体和交互都正常。

从那以后我每次交付可视化项目,都会强制走一遍「换一台电脑打开 HTML」的流程,因为编码、字体、CDN 这三个东西在不同机器上的表现真的不一样。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进