
简介面向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, encodingutf-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-8encodingutf-8-sig兼容带BOM的CSV能规避国内Excel导出文件常见的首列乱码问题。时间粒度提取到年、月、日后后续按不同层次聚合就不用反复分析了。2.2 缺失值与异常值处理的三个必调参数PM2.5数据里的缺失去哪儿了一部分是监测设备离线直接整行NaN一部分是异常哨兵值比如-1、-999这类数值如果不处理均值会被拉出几十。处理时不能全局一刀切因为城市之间的污染水平差异很大全局填充会扭曲真实分布。下面这段是我常用的处理策略# 1. 删除城市和日期为空的行 df.dropna(subset[city, date], inplaceTrue) # 2. 处理哨兵值-1和负值都视为缺失 df.loc[df[pm25] 0, pm25] None # 3. 按城市分组向前填充最多补3个连续缺失 df[pm25] ( df.groupby(city)[pm25] .transform(lambda x: x.fillna(methodffill, limit3)) ) # 4. 剩余缺失值用城市中位数兜底 df[pm25] df[pm25].fillna(df.groupby(city)[pm25].transform(median))按城市分组是这里的关键。直接用全局中位数填充会让低污染城市的浓度被人为拉高分组后再填充每个城市保留自己的尺度。ffill加limit3只补短时段缺失超过3个连续空缺就放弃避免长段猜测。最后的median比均值更抗异常是针对偏态分布更稳妥的兜底方案。方法常用参数适用场景dropnasubset,thresh关键字段缺失如城市、日期为空fillnamethodffill,limit短时段连续缺失用前值填充cliplower0负值压到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_indexFalse) .agg({pm25: [mean, median, count]}) ) # 2. 列出年均PM2.5最高的10个城市 top_cities yearly[pm25][mean].nlargest(10).index print(top_cities)agg里的列表参数一次算多个指标省掉多次groupby的开销。as_indexFalse让分组字段保留为普通列后续拼接图表数据时更方便。注意聚合结果会产生多层列索引yearly[pm25][mean]这把“pm25子表”里的“mean列”取出来如果觉得多层索引绕可以在agg前先df[[city, year, pm25]]选列再给列重命名。聚合函数用途注意点mean平均污染水平易受重污染日影响median稳健的中心趋势分布偏态时更可靠std波动程度需要和均值一起看count有效样本量样本少于100的结果要谨慎quantile90分位数等极端情况关注高浓度事件占比count常被忽略但很有价值。如果一个城市在某年份只有几天有效数据它的均值再高也不能说明问题。我通常会在聚合后写一个过滤规则把有效样本量不足某个阈值的城市和年份剔除。3.2 用pivot_table构建城市年度对比表# 1. 转换为城市为行、年份为列的透视表 pivot pd.pivot_table( df, valuespm25, indexcity, columnsyear, aggfuncmean ) # 2. 计算城市年均值的变化幅度 pivot[change] pivot.apply( lambda row: (row.iloc[-1] - row.iloc[0]) / row.iloc[0], axis1 )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(datadf, xyear, ypm25) plt.xticks(rotation45) plt.title(PM2.5 Distribution by Year) plt.savefig(distribution.png, dpi150)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_optsopts.TitleOpts(title中国城市PM2.5年均值分布), visualmap_optsopts.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_optsopts.GridOpts(pos_top10%)) .add(bar, grid_optsopts.GridOpts(pos_bottom60%)) ) 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 srcdistribution.png / h2Top 10 高浓度城市/h2 table {% for city in top_cities %} trtd{{ city[0] }}/tdtd{{ city[1] }}/td/tr {% endfor %} /table ) html template.render( start2020-01-01, end2020-12-31, top_citieslist(top_values.items()) ) with open(report.html, w, encodingutf-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_minusFalse这套流程走完后建议把所有模块拆成clean.py、analyze.py、visualize.py、report.py四个脚本放入同一个项目目录。换任何城市级别的PM2.5数据只需要修改读取路径和清洗规则就能直接复用整套分析与可视化链路。本文还有配套的精品资源点击获取