简介:面向Python数据分析与可视化课程设计与期末大作业的完整实践项目,以中国城市PM2.5值监测数据为分析对象,覆盖数据清洗、统计分析和可视化展示等环节,适合需完成数据分析或可视化任务的新手参考。整个资源包共21个文件,打包后约6.13MB,包含Python主程序、5个城市的PM2.5监测CSV数据、13个可视化HTML页面及2份Word文档,源码带详细注释,部署简单,便于快速运行并查看效果。已有1510人学习下载。通过该项目可掌握Pandas、Matplotlib等常用库的实战用法,并产出完整的项目报告和可视化图表,其中HTML页面可独立展示各地PM2.5变化趋势,配套的Word文档对综合实践项目进行说明,既可作为课程设计提交材料,也可作为高分课程设计的参考范例。
1. 如果手头有一份中国城市PM2.5监测数据,想看清哪几个月污染最重、哪些城市逐年好转,但又不想手动扒Excel,这个标题就是用Python把这件事完整做一遍的典型路径。它不单单是画几张图,而是从数据清洗到统计聚合,再到可视化展示和报告输出,形成一条可复现的数据分析流水线。
很多人学完pandas和matplotlib后卡在“真实数据不知道从哪下手”。这个项目的核心价值在于:用PM2.5这个有明确时空属性的环境数据,把数据分析与可视化的每个步骤都落到可执行代码上。你得到的不只是几张图,而是一套能够应对后续任何结构化数据的处理框架,这也是“源码+报告”这类项目最值得拆解的地方。
它适合两类人:一是刚入门、想找一个覆盖全流程的Python数据分析案例的开发者,按步骤可以完整跑通;二是已经写过业务代码、但想系统整理数据分析流程的工程师,这里面的参数细节和坑位可以帮你少走弯路。下面我从数据清洗开始,逐步拆解整个分析链路。
2. 数据清洗与标准化:把PM2.5原始站点数据变成可分析DataFrame
拿到原始数据时,最常见的情况是一份CSV或Excel,里面每行记录一个监测站点的城市、时间、PM2.5浓度。可现实数据永远不会干净:城市名一会儿“北京”一会儿“北京市”,日期有时是字符串有时是时间戳,PM2.5列还混着负数、-999这种哨兵值。数据清洗不是可有可无的环节,它直接决定后续聚合和可视化是否可信。
我一般会先建一个标准化流程,把加载、类型修正、缺失值处理拆成几个函数,这样换任何城市级别的数据都能复用。下面从第一步开始写起。
2.1 加载数据与类型修正的三步操作
import pandas as pd # 1. 加载CSV,把日期列和城市列类型校准 df = pd.read_csv( "pm25_data.csv", encoding="utf-8-sig", parse_dates=["date"], dtype={"city": "string", "pm25": "float64"} ) # 2. 看数据结构与缺失量 print(df.info()) print(df.isnull().sum()) # 3. 统一时间粒度,方便后续按年月聚合 df["year"] = df["date"].dt.year df["month"] = df["date"].dt.month df["day"] = df["date"].dt.day这里有几个参数值得说明。parse_dates=["date"]是把date列解析成datetime64类型,这样后面才能用.dt提取年月。dtype={"city": "string", "pm25": "float64"}直接指定列类型,比读进来再转更快,而且能避免城市列被误读成float。df.info()和df.isnull().sum()是快速检查列类型和缺失量的固定组合,我每次加载数据都会先跑这两行。
如果你手头的文件编码不是UTF-8,encoding="utf-8-sig"兼容带BOM的CSV,能规避国内Excel导出文件常见的首列乱码问题。时间粒度提取到年、月、日后,后续按不同层次聚合就不用反复分析了。
2.2 缺失值与异常值处理的三个必调参数
PM2.5数据里的缺失去哪儿了?一部分是监测设备离线,直接整行NaN;一部分是异常哨兵值,比如-1、-999,这类数值如果不处理,均值会被拉出几十。处理时不能全局一刀切,因为城市之间的污染水平差异很大,全局填充会扭曲真实分布。下面这段是我常用的处理策略:
# 1. 删除城市和日期为空的行 df.dropna(subset=["city", "date"], inplace=True) # 2. 处理哨兵值:-1和负值都视为缺失 df.loc[df["pm25"] <= 0, "pm25"] = None # 3. 按城市分组,向前填充,最多补3个连续缺失 df["pm25"] = ( df.groupby("city")["pm25"] .transform(lambda x: x.fillna(method="ffill", limit=3)) ) # 4. 剩余缺失值用城市中位数兜底 df["pm25"] = df["pm25"].fillna(df.groupby("city")["pm25"].transform("median"))按城市分组是这里的关键。直接用全局中位数填充会让低污染城市的浓度被人为拉高;分组后再填充,每个城市保留自己的尺度。ffill加limit=3只补短时段缺失,超过3个连续空缺就放弃,避免长段猜测。最后的median比均值更抗异常,是针对偏态分布更稳妥的兜底方案。
| 方法 | 常用参数 | 适用场景 |
|---|---|---|
dropna | subset,thresh | 关键字段缺失,如城市、日期为空 |
fillna | method="ffill",limit | 短时段连续缺失,用前值填充 |
clip | lower=0 | 负值压到0或剔除,用于异常哨兵值 |
这里没直接用clip,是因为-999这种值如果压到0,会参与均值计算,造成假低值;先置为None再填充是更合理的路径。整个清洗过程做完,df.info()里的非空行数与原始数据对比,就能算出数据有效率,这份信息可以写进报告的数据说明中。
3. 统计分析与时空聚合:用Python摸清PM2.5的分布规律
清洗完的数据还是细粒度的日观测值,要回答“哪些城市污染最重”“冬季和夏季差多少”“多年趋势如何”,必须做聚合。pandas里两把最常用的工具是groupby和pivot_table:前者适合按一个或多个键分组后做运算,后者适合把分组结果变成二维透视表,方便横向比较。
这阶段的代码并不复杂,复杂的是选对聚合函数和判断结果可信度。很多人习惯只看均值,但在污染数据里,均值容易受极端事件影响,需要用中位数和分位数校验。
3.1 groupby 与聚合函数选型
# 1. 按城市和年份聚合年均值 yearly = ( df.groupby(["city", "year"], as_index=False) .agg({"pm25": ["mean", "median", "count"]}) ) # 2. 列出年均PM2.5最高的10个城市 top_cities = yearly["pm25"]["mean"].nlargest(10).index print(top_cities)agg里的列表参数一次算多个指标,省掉多次groupby的开销。as_index=False让分组字段保留为普通列,后续拼接图表数据时更方便。注意聚合结果会产生多层列索引,yearly["pm25"]["mean"]这把“pm25子表”里的“mean列”取出来;如果觉得多层索引绕,可以在agg前先df[["city", "year", "pm25"]]选列,再给列重命名。
| 聚合函数 | 用途 | 注意点 |
|---|---|---|
mean | 平均污染水平 | 易受重污染日影响 |
median | 稳健的中心趋势 | 分布偏态时更可靠 |
std | 波动程度 | 需要和均值一起看 |
count | 有效样本量 | 样本少于100的结果要谨慎 |
quantile | 90分位数等极端情况 | 关注高浓度事件占比 |
count常被忽略,但很有价值。如果一个城市在某年份只有几天有效数据,它的均值再高也不能说明问题。我通常会在聚合后写一个过滤规则,把有效样本量不足某个阈值的城市和年份剔除。
3.2 用pivot_table构建城市年度对比表
# 1. 转换为城市为行、年份为列的透视表 pivot = pd.pivot_table( df, values="pm25", index="city", columns="year", aggfunc="mean" ) # 2. 计算城市年均值的变化幅度 pivot["change"] = pivot.apply( lambda row: (row.iloc[-1] - row.iloc[0]) / row.iloc[0], axis=1 )pivot_table与groupby的差别在于输出结构:它直接生成一张城市行、年份列的二维表,横向对比很直观。values指定要聚合的数值列,aggfunc支持传入mean、median、sum等。这里pivot["change"]计算的是每个城市首尾年份的变化率,正值表示上升,负值表示下降。
透视表有个问题是城市多、年份少时会产生大量NaN。比如某城市某年数据缺失,单元格就是空的,直接算change会得到NaN。我一般会先对pivot做一次fillna(0),或者用dropna(subset=[首年, 末年])只保留首尾年份都有数据的城市。你要做趋势分析时,这个清洗步骤不能省。
4. 可视化图表与交互大屏:Python可视化库的选型与参数调优
分析结果要让人看懂,可视化比表格有优势得多。Python里的可视化库不少,但选型逻辑不复杂:静态图片用matplotlib,统计分布类图表用seaborn,需要网页交互或做可视化大屏就用pyecharts。三者并不互斥,一套分析报告里完全可以混用。
不要一开始就追求炫酷大屏。先画几张静态图确认数据模式,再套到交互组件里,这样排错成本最低。
4.1 matplotlib、seaborn、pyecharts怎么选
import matplotlib.pyplot as plt import seaborn as sns # 用seaborn画不同年份PM2.5分布箱线图 sns.boxplot(data=df, x="year", y="pm25") plt.xticks(rotation=45) plt.title("PM2.5 Distribution by Year") plt.savefig("distribution.png", dpi=150)seaborn的boxplot一行就能展示各年份的分布、中位数和异常点。画这类图时year列必须是数值或类别,如果已经是datetime对象,需要先转化成整数或字符串。plt.savefig的dpi参数建议不低于150,否则放到报告里会发虚。
| 库 | 优势 | 典型场景 | 注意点 |
|---|---|---|---|
| matplotlib | 底层控制力强 | 论文级图表、复杂布局 | 代码量大,中文需设字体 |
| seaborn | 统计图表封装好 | 分布、回归、热力图 | 输出尺寸受matplotlib控制 |
| pyecharts | 交互式、网页友好 | 大屏、地图、动态图表 | 地图依赖js资源,城市名需统一 |
4.2 用pyecharts做全国PM2.5分布地图
from pyecharts import options as opts from pyecharts.charts import Map # 构造 [("北京市", 45.6), ("上海市", 38.2)] 格式 data = [tuple(x) for x in city_avg[["city", "pm25"]].values] map_chart = ( Map() .add("PM2.5 年均值", data, "china") .set_global_opts( title_opts=opts.TitleOpts(title="中国城市PM2.5年均值分布"), visualmap_opts=opts.VisualMapOpts( min_=0, max_=100, range_color=["#2ecc71", "#f1c40f", "#e74c3c"] ) ) ) map_chart.render("pm25_map.html")pyecharts的Map组件里"china"是最常用的中国地图底图。但这个组件有个常见坑:渲染出的HTML里地图区域空白。原因是地图的js资源没加载完整,要么网络环境无法访问资源,要么本地缺少地图注册文件。解决方式是把项目需要的地图包提前下载到本地,然后引入到HTML中。visualmap_opts的min_和max_建议先跑df["pm25"].quantile([0.05, 0.95])看数据分布,不要硬编码0到100,否则大部分城市颜色几乎一样,图就失去了区分度。
4.3 组装一个小型可视化面板
from pyecharts.charts import Line, Bar, Grid line = ( Line() .add_xaxis(years) .add_yaxis("平均浓度", avg_values) ) bar = ( Bar() .add_xaxis(city_names) .add_yaxis("年均值", city_values) ) grid = ( Grid() .add(line, grid_opts=opts.GridOpts(pos_top="10%")) .add(bar, grid_opts=opts.GridOpts(pos_bottom="60%")) ) grid.render("panel.html")Grid可以把折线图和柱状图组合到同一个页面,这个页面本身就是一个小型可视化面板。组装时注意每个series的name不能重复,否则后一个会覆盖前一个。做更完整的大屏时,我会把每个图封装成独立函数,接收DataFrame参数,这样图表可以独立调试,也能通过Page组件组合到一张长页面上。
5. 报告生成与踩坑记录:把分析结果固化
分析做完还不够,最终要输出一份可读的报告。常见做法是生成HTML报告,里面嵌入图表、表格和结论,不需要打印成PDF也能在浏览器里顺畅查看。
5.1 用Jinja2模板自动生成HTML报告
from jinja2 import Template template = Template(""" <h1>中国城市PM2.5分析报告</h1> <p>数据区间:{{ start }} - {{ end }}</p> <img src="distribution.png" /> <h2>Top 10 高浓度城市</h2> <table> {% for city in top_cities %} <tr><td>{{ city[0] }}</td><td>{{ city[1] }}</td></tr> {% endfor %} </table> """) html = template.render( start="2020-01-01", end="2020-12-31", top_cities=list(top_values.items()) ) with open("report.html", "w", encoding="utf-8") as f: f.write(html)Jinja2的循环语法和Python很像,注意city[0]取城市名,city[1]取数值。报告中嵌入的图片路径用相对路径,HTML文件与图片同级目录才能正常显示。模板里也可以直接插入pyecharts生成的HTML内容,但要处理js的加载顺序。
5.2 三个高频踩坑点
| 踩坑点 | 现象 | 解决方式 |
|---|---|---|
| 日期解析失败 | 时间列变成object,聚合时报错 | 用pd.to_datetime并指定format,比自动解析快几倍 |
| 城市名不统一 | 地图匹配不上,图表大面积空白 | 建立“北京-北京市”别名映射字典,统一后再画图 |
| 中文乱码 | matplotlib标题和坐标轴显示方块 | 设置plt.rcParams["font.sans-serif"]为中文字体,并加axes.unicode_minus=False |
这套流程走完后,建议把所有模块拆成clean.py、analyze.py、visualize.py、report.py四个脚本,放入同一个项目目录。换任何城市级别的PM2.5数据,只需要修改读取路径和清洗规则,就能直接复用整套分析与可视化链路。
本文还有配套的精品资源,点击获取