Python空气污染数据可视化分析:从pandas清洗到Flask交互看板实战
简介这套资源是基于Python的空气污染数据可视化分析系统面向计算机、人工智能、通信工程、物联网等相关专业的高校学生、教师及科研从业者尤其适合毕业设计、课程设计、作业或项目初期演示。系统围绕空气污染数据的采集、清洗、分析与可视化展开内置完整源码、Jupyter分析笔记和设计报告可帮助使用者快速掌握数据分析全流程也便于在此基础上扩展其他监测指标。压缩包共289个文件大小约104.47MB文件类型以Python脚本、ipynb分析笔记、CSV数据集、JSON配置、HTML可视化页面为主同时包含PDF设计报告、PPT演示文稿、图片与字体资源等分别对应后端处理、前端展示、数据存储和文档说明目录结构清晰便于按需检索。目前已有43人学习下载。整套资料提供可运行源码、可视化页面、设计报告及项目演示材料既适合完整复现学习也可直接用于课程答辩若基础薄弱还能获得远程指导和技术支持显著降低配置与运行门槛。1. 空气污染数据分析可视化为什么这是值得完整走一遍的 Python 实战选题“基于Python数据分析空气污染数据可视化分析系统”这个标题本质上是让你把一套环保数据完整走通pandas 做清洗和聚合matplotlib、seaborn 出分析图再用 Flask 加 ECharts 把结果变成能交互的数据看板最后配一份能讲清设计思路的报告。这个选题在课程设计和毕设里常年出现是因为数据公开、结论直观、涉及的技能栈都是 Python 数据分析与可视化里最常用的那批库。对新手来说这是一个从数据处理一路练到页面展示的完整闭环对找实习的人来说它又是一个能直接写进简历、面试时能讲出细节的项目。下面按我从数据预处理到源码封装的真实流程展开代码可以直接复制参数我已经按空气质量数据的习惯调好。2. 数据获取与预处理把 PM2.5 数据从原始表格变成可分析的 DataFrame这一章解决的是整个系统里最容易被跳过、但最影响结论的一步。很多人拿到源码包后先急着画图结果图出来了日均值曲线却因为时间戳重复或缺失值没处理而显得很奇怪。空气污染数据不像教学用的 iris 数据集那么干净它带缺失标记、单位混杂、时间格式五花八门把这一步做扎实后面的可视化才有意义。2.1 数据源选择公开数据集、环境监测 API 与课程自带文件怎么取舍常见的数据来源有三类我按推荐程度排一下。第一类是 UCI Machine Learning Repository 的 Air Quality 数据集。它采集自意大利某地2004 年 3 月到 2005 年 4 月逐小时记录包含 CO、NMHC、NOx、NO2、苯、温湿度等字段。它的优点是字段名规范、有明确的缺失标记便于练习 pandas 的高级清洗缺点是它本身没有 PM2.5 浓度列只有传感器响应值做空气质量指数计算时往往要自己补数据这一点在选题阶段就要先确认。第二类是国内环境监测总站和各地生态环境局发布的空气质量日报接口字段通常包含 AQI、PM2.5、PM10、SO₂、NO₂、O₃、CO 的小时值或日均值。这类数据更符合“国内空气污染”这个主题但接口格式因网站而异有的返回 JSON有的是 CSV。做多个城市对比时这类数据最合适只是要预留一部分时间做字段映射。第三类是课程给定的 Excel 或 CSV 文件。如果老师已经发了固定数据那就不用纠结源的问题直接把文件读入并记录好原始数据的单位、时间范围和数据量即可。数据结构上无论用哪个来源都要先搞清楚三件事时间字段是单列还是日期时间拆开、浓度单位是 μg/m³ 还是 mg/m³、缺失值用什么标记。UCI 数据里常见的是分号分隔、用-200表示传感器校准期间的无效值国内 CSV 常见的是 gbk 或 utf-8 编码缺失值为空。这些细节直接决定read_csv怎么写。2.2 pandas 读入与清洗分号分隔、缺失值和时间字段的预处理读写这一步最常见的翻车点是分隔符和缺失标记。UCI 数据用分号而不是逗号且行的末尾还带一个空列直接默认参数读进来会多出一列Unnamed。读取时把sep、na_values、parse_dates一次性配好能省掉后续大量修补工作。import pandas as pd # 读取 UCI Air Quality 数据文件用分号分隔-200 表示无效值 df pd.read_csv( AirQualityUCI.csv, sep;, na_values-200, decimal,, parse_dates{timestamp: [Date, Time]}, ) # 去掉全空列并清理列名空格 df df.dropna(axis1, howall) df.columns [col.strip() for col in df.columns] # 时间字段是 MM/DD/YYYY HH.MM.SS需指定格式 df[timestamp] pd.to_datetime(df[timestamp], format%m/%d/%Y %H.%M.%S) # 只保留整点时刻避免同一小时内多条记录导致聚合重复 df df[df[timestamp].dt.minute.eq(0)].copy() df df.sort_values(timestamp).reset_index(dropTrue) print(df.shape) print(df.dtypes)这里有几个参数值得说明。sep;是对应 UCI 的分隔符decimal,处理欧洲数据里用逗号表示小数点的习惯na_values-200把传感器故障值直接映射成 NaN。parse_dates将日期和时间两列拼成 timestamp 列这是预处理里最关键一步——如果这一步不做后面resample按天聚合时就会因为索引是字符串而报错。清洗完可以顺手把列重命名成容易认的英文名比如CO(GT)改为co_mg_m3NOx(GT)改为nox_ppb。单位不统一的问题我一般统一换算成 μg/m³ 之后再做相关性分析否则 CO 和 NOx 画在同一个坐标系里量纲差异会让图很难看。2.3 日均值聚合与 AQI 计算从小时浓度到污染等级空气污染可视化里最常用的时间粒度是“日均值”因为日报标准和 AQI 分级都以 24 小时为基准。按天重采样要特别注意直接用df[timestamp].dt.date分组再对所有数值列取平均这样写没错但更地道的做法是先设置时间索引再resample。# 将时间戳设为索引按天聚合小时数据为日均值 daily df.set_index(timestamp).resample(D).mean(numeric_onlyTrue) # 日均值中 CO 核心字段为空的行直接剔除避免画图断线 daily daily.dropna(subset[co_mg_m3]) # 30 日滚动均值用于观察长期趋势平滑掉单日波动 daily[pm25_roll30] daily[pm25_ug_m3].rolling(30, min_periods10).mean() # 输出时间范围和有效天数前几步的检查必须在画图前做 print(daily.index.min(), daily.index.max()) print(daily[[pm25_ug_m3]].isna().sum())resample(D)是按自然日聚合如果数据是北京时间直接聚合结果正确如果数据带 UTC 时区要先tz_localize再tz_convert到目标时区否则每日边界会整体偏移 8 小时这一步容易造成“日均值对不上官方日报”的尴尬。AQI 计算是另一个核心点。中国标准 HJ 633-2012 使用分段线性插值PM2.5 日均浓度的分档是 0-35 对应 IAQI 0-5035-75 对应 50-100后面依次是 75-115、115-150、150-250、250-350、350-500。计算单个污染物的 IAQI 后取六项污染物中最大的 IAQI 作为当天 AQI。写一个通用的calc_iaqi函数把分段表作为参数传入这样换污染物时只需要换表。# 中国 HJ 633-2012 中 PM2.5 的浓度-IAQI 分段表 pm25_table [ (0, 35, 0, 50), (35, 75, 50, 100), (75, 115, 100, 150), (115, 150, 150, 200), (150, 250, 200, 300), (250, 350, 300, 400), (350, 500, 400, 500), ] def calc_iaqi(concentration, table): 分段线性插值计算单项污染物的 IAQI for cl, ch, il, ih in table: if cl concentration ch: return round((ih - il) / (ch - cl) * (concentration - cl) il, 0) return 500 # 超过上限按最高等级处理 daily[pm25_iaqi] daily[pm25_ug_m3].apply( lambda x: calc_iaqi(x, pm25_table) if pd.notna(x) else None ) # 查看各污染等级的样本天数 import numpy as np bins [0, 50, 100, 150, 200, 300, 500] labels [优, 良, 轻度, 中度, 重度, 严重] daily[aqi_level] pd.cut(daily[pm25_iaqi], binsbins, labelslabels) print(daily[aqi_level].value_counts())apply是逐行调用函数数据量只有几百行时速度没问题但如果你扩展到多年逐小时数据建议改成pd.cut配合分箱或者用numpy.interp做插值避免循环过慢。到这里数据从“能读出来”变成了“能算结论”可以做可视化分析了。3. 可视化分析落地用 matplotlib、seaborn 和 pyecharts 画出污染规律预处理做完这一章的目标是让数据自己说话。选择图表类型时我遵循一个原则先回答“污染物浓度随时间怎么变”再回答“污染物之间有什么关系”最后才考虑“不同城市或季节差多少”。顺序反了很容易一开始就纠结地图配色等发现数据根本没有经纬度字段就白费力气。3.1 污染物时间趋势用 matplotlib 呈现日均值与 30 日滚动均值时间趋势是空气污染分析里最稳的一张图。把日均值和 30 日滚动均值画在一起既能反映单日波动又能展示冬季高、夏季低的长期规律。matplotlib 的两个核心参数是figsize和alpha前者控制画布比例后者控制透明白度滚动曲线压在原线上时才不会完全遮住细节。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(12, 5)) ax.plot(daily.index, daily[pm25_ug_m3], colorgray, alpha0.4, linewidth0.8, labelPM2.5 日均值) ax.plot(daily.index, daily[pm25_roll30], colorcrimson, linewidth2, label30 日滚动均值) ax.set_title(PM2.5 日平均浓度变化趋势, fontsize14) ax.set_xlabel(日期) ax.set_ylabel(浓度 (μg/m³)) ax.legend(frameonFalse) fig.tight_layout() plt.savefig(outputs/pm25_trend.png, dpi200) plt.show()alpha0.4是给原始日值用的灰度线条若隐若现让红色的滚动均值成为视觉焦点linewidth在对外展示时要至少 1.5否则打印出来断线严重。保存图片时用dpi200以上因为设计报告里插图会被放大默认 100 会显得发虚。这里不要用seaborn的默认主题直接叠加whitegrid网格线在空气污染数据上会干扰阅读。如果数据里有 O₃ 或 NO₂我通常会画两到三个子图把颗粒物和气态污染物分开避免单位不同造成右侧纵轴混淆。一个常见误区是直接用twinx()把不同量纲的曲线叠在同一个图里初学者往往因此做出“左边 200 右边 2”的误导图建议报告里尽量拆成独立子图。3.2 污染物相关性用 seaborn 画出相关系数矩阵和散点分布热力图能一眼看出哪些污染物同源。汽车尾气排放的 CO 和 NOx 通常强正相关而 O₃ 与 NO₂ 在光照条件下可能存在负相关这些结论适合写进设计报告的“结果分析”部分。seaborn.heatmap需要先算相关系数矩阵参数上注意把annotTrue打开否则矩阵里只有颜色没有数值读者很难量化。import seaborn as sns # 选择参与分析的数值列统一单位后再做相关 cols [co_mg_m3, nox_ppb, no2_ppb, pm25_ug_m3, rh, temp] corr daily[cols].corr() fig, ax plt.subplots(figsize(8, 6)) sns.heatmap(corr, annotTrue, fmt.2f, cmapvlag, center0, linewidths0.5, cbar_kws{shrink: 0.8}, axax) ax.set_title(污染物与气象因素相关系数矩阵, fontsize13) fig.tight_layout() plt.savefig(outputs/corr_matrix.png, dpi200) plt.show()cmapvlag适合有正负相关的数据中心白色对应相关系数为 0红色为正相关蓝色为负相关。相比RdBu_rvlag在色弱人群中辨识度更高一些。计算相关之前务必确认两列数据的采样时间完全对齐daily[cols].corr()遇到某一列缺失会整行剔除如果缺失集中在冬季最后算出的相关系数可能偏小这一点最好在报告里注明样本量。想做散点分布时seaborn.pairplot会输出一个多维网格变量太多时图会非常挤。我一般只挑 CO 与 PM2.5、温湿度与 PM2.5 两组来画数据量大时可以加plot_kws{alpha: 0.3, s: 6}控制点的透明度避免重叠到一片黑。3.3 空间与季度对比没有经纬度时的城市与季节可视化很多人一上来就想画地图但单一监测站的数据根本画不了空间分布。这里分两种情况如果数据里有城市或站点字段可以用 pyecharts 的Bar做城市浓度排名Map需要地图 JSON 文件配置稍繁琐建议先用条形图出结果如果只有单站数据那就退一步用箱线图按季度展示浓度分布。# 单站点数据按季度聚合的箱线图 import pandas as pd import matplotlib.pyplot as plt daily[quarter] daily.index.quarter daily[season] daily[quarter].map({1: 冬季, 2: 春季, 3: 夏季, 4: 秋季}) fig, ax plt.subplots(figsize(8, 5)) daily.boxplot(columnpm25_ug_m3, byseason, axax, gridFalse, patch_artistTrue, boxpropsdict(facecolorlightsteelblue)) ax.set_ylabel(PM2.5 浓度 (μg/m³)) ax.set_title(不同季节 PM2.5 浓度分布) fig.suptitle() plt.tight_layout() plt.savefig(outputs/season_box.png, dpi200) plt.show()daily.index.quarter是从时间索引直接提取季度的便捷方法比手动判断月份快很多。箱线图能同时展示中位数、四分位距和离群点冬季中位数高于夏季、上须明显拉长这是报告中可靠的结论来源。如果要做城市对比先按城市分组求年均值再画横向条形图这种图对读者最友好。4. 源码组织与系统封装把脚本变成可交互数据看板并交付设计报告数据分析和可视化的脚本跑通之后还差最后一步“产品化”。课程设计或毕设的评分者通常看重三件事源码能不能直接在别人机器上运行、设计报告是不是完整、结论和图表是否自洽。这一章讲我常用的源码组织方式以及怎么用 Flask 加 ECharts 把静态图升级成可交互的数据看板。4.1 源码目录怎么拆数据、处理、可视化、应用四模块分离一个可以复现的 Python 数据分析项目目录结构比代码本身更重要。我一般会分成data、src、outputs、report四个目录data放原始文件src放处理脚本outputs放生成的图片report放设计报告的 Markdown 或 Word 文档。这样拆的好处有三个。第一评分者拿到压缩包后能按目录快速定位第二数据处理和可视化分离后换数据源时不需要重写画图函数第三outputs里的图片可以直接用于报告避免“报告图和代码运行结果不一致”这种硬伤。src内部我习惯拆成三个脚本# preprocess.py读取原始数据清洗、重采样、计算 AQI输出 daily.csv # analyze.py读取 daily.csv生成趋势图、相关矩阵、季节性箱线图 # app.py加载 daily.csv提供 Flask 接口渲染交互看板 # 命令行入口保持简单 # python src/preprocess.py # python src/analyze.py # python src/app.py设计报告里对模块职责的描述可以直接对应到每个脚本的文件名和核心函数名。这里我特别建议把数据处理的中间结果daily.csv输出到data/processed目录而不是每次可视化都重新读原始数据。对空气污染这种小时级数据来说全量重跑会浪费时间而且如果preprocess.py有改动analyze.py用旧缓存会浪费 wrangling 时间。4.2 用 Flask 加 ECharts 把图表做成可交互数据看板如果你只交一组 PNG 图片项目完成度会显得偏低但如果把图表封装成一个本地网页评分观感会提升不少。常见的做法是后端用 Flask 提供 JSON 数据接口前端用 ECharts 画折线图、柱状图和仪表盘页面选择不同城市或不同污染物时通过 AJAX 刷新图表。# app.py 的核心路由返回日均值 JSON 数据 from flask import Flask, jsonify, render_template, request import pandas as pd app Flask(__name__) daily pd.read_csv(data/processed/daily.csv, parse_dates[date]) app.route(/) def index(): return render_template(index.html) app.route(/api/pm25) def api_pm25(): start request.args.get(start, 2004-03-11) end request.args.get(end, 2005-04-04) mask (daily[date] start) (daily[date] end) data daily.loc[mask] return jsonify({ dates: data[date].dt.strftime(%Y-%m-%d).tolist(), values: data[pm25_ug_m3].fillna(0).tolist(), }) if __name__ __main__: app.run(debugTrue, host127.0.0.1, port5000)前端页面里ECharts 的折线图只负责把后端返回的dates和values填进series.data一个setOption就能渲染。注意后端做空值填充时我用的是fillna(0)这只适合预览正式分析图里不建议这样做——缺失值填 0 会让趋势图出现尖峰真实做法是保留 nullECharts 会用connectNulls决定是否跨空值连线。这个结构的价值在于评分者可以从浏览器看到一个能筛选日期范围的页面而不仅仅是静态图。这一套做下来即便没有部署到公网也能在答辩现场用 localhost 演示稳定性远高于现场写 Jupyter Notebook。前端模板文件名按 Flask 约定放在templates/index.html静态资源放static/js/echarts.min.js即可。如果不想引入重型框架这已经是最小的可交互看板方案。4.3 设计报告的写作顺序需求分析、系统结构、结果与附录标题里提到的“设计报告”往往是评分权重最高的部分比代码本身更值得花时间。我按四段结构来写顺序是第一段讲选题背景和数据来源把数据的时间范围、站点数量、字段含义写清楚第二段画系统流程图说明数据从原始文件到清洗到可视化到网页展示的链路第三段贴核心代码并逐段解释这一段要控制篇幅只贴resample、AQI 计算、Flask 路由这三块第四段分析结果把第 3 章生成的图和 3 到 4 条结论一一对应放上去。报告中容易踩的坑是结论和图表对不上。比如图里画的是日均值文字却说“小时浓度超标频次”这种错误一眼就能被发现。正确的做法是先写结论再根据结论选图图和下的结论用同一份计算结果支撑不要让图表成为孤立装饰。5. 避坑与排查编码、时区、字体和性能的四个典型问题这个选题说不上难但坑非常典型。下面四条是我反复见过、自己也踩过的按“现象、原因、解决”列出来基本覆盖了从读取到出图的常见故障。5.1 CSV 乱码gbk 与 utf-8 编码互转的坑现象在 Windows 上用 pandas 读取国内空气质量 CSV打印前几行时中文列名变成乱码读取能成功但列名完全没法看用 Excel 打开却正常。原因多数国内环境数据网站导出的 CSV 用 gbk 编码而 pandas 默认按系统语言推断。在中文 Windows 上默认是 gbk读到 utf-8 文件会乱码反过来在 Linux 或 macOS 上默认 utf-8读到 gbk 文件也会乱码。解决读入时显式指定编码不要依赖默认值。先试encodinggbk如果报UnicodeDecodeError再换encodingutf-8还可以用errorsreplace先看有多少异常字符。写输出时统一用encodingutf-8-sig这样生成的 CSV 在 Excel 里打开不会乱码sig会写入 BOM 头Excel 才能正确识别。在 VS Code 跑脚本时编辑器右下角也要把文件编码改成和代码里一致否则会看到编辑器显示正常、pandas 读出来全是锟斤拷。5.2 UTC 时区导致日均值偏移 8 小时现象计算出的日均值和环境监测总站官方日报对不上官方显示某天 PM2.5 是 120自己算出来是 95且偏差不是均匀的有的天偏大有的天偏小。原因接口返回的时间戳是 ISO 8601 格式且带Z后缀pandas 解析后是 UTC 时间。直接按 UTC 自然日聚合相当于把北京时间每天 08:00 当作日界而环境监测日报按北京时间 00:00-24:00 统计两者边界错开了 8 小时跨天数据会被分到错误日期。解决加载时统一转换时区。pd.to_datetime(col, utcTrue).dt.tz_convert(Asia/Shanghai)转换后再resample(D)。如果数据源本身是本地时间不要再加tz_localize否则会出现“墙上时间不变但时区偏移”的问题。排查这类问题时直接抽一天手动数一下小时数比看报错信息更快。5.3 图里的中文全部变成方块现象matplotlib 绘图里标题、图例、坐标轴标签的中文都显示为小方块英文正常在 Linux 服务器上最明显。原因matplotlib 默认字体是 DejaVu Sans不包含中文字形系统里又没有让它去查找的中文字体。这个问题和代码本身无关属于运行环境缺字体。解决在画图前设置字体列表plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, Noto Sans CJK SC]并plt.rcParams[axes.unicode_minus] False解决负号显示问题。如果服务器上确实没有中文字体两种办法一是把系统字体文件拷贝到 matplotlib 字体目录后重建缓存二是图内全部用英文标签报告正文再用中文叙述。我一般倾向后一种省时间也不影响图表内容。5.4 数据量不大但代码跑得很慢循环里的神级操作现象总共几千行数据apply加一个自定义函数算 AQI竟然要跑好几分钟而官方报告早就出来了。原因apply本质是 Python 层的循环函数内部如果还逐行访问 DataFrame等于嵌套循环。pandas 虽然后端是 C但 Python 循环的开销主要在函数调用和索引解析上。解决优先用向量化操作。AQI 计算可以用pd.cut分箱日均聚合用resample30 日窗口用rolling这些函数底层都是 C 循环。如果必须逐行判断至少改成numpy.select或numpy.interp性能能提升一到两个数量级。画图慢另一个常见原因是plt.savefig的dpi设得过高300 和 200 视觉差别不大但文件体积和渲染时间差很多对外展示用 200 足够。6. 进阶收尾给分析结论加一层时间序列验证做完趋势、相关性和季节对比之后结论基本成立但答辩或报告里如果能补一段预测验证会让整个系统的可信度上一个台阶。我用 statsmodels 做季节性分解和简单 ARIMA 预测代码不多却能回答“这套分析只描述过去还是能预估未来”这个问题。from statsmodels.tsa.seasonal import seasonal_decompose # 按周重采样削弱日波动分解趋势、季节、残差 weekly daily[pm25_ug_m3].resample(W).mean().dropna() result seasonal_decompose(weekly, modeladditive, period52) result.trend.plot(label趋势) result.seasonal.plot(label季节) plt.legend() plt.show()period52对应一年 52 周如果数据不足一年把period改成 12 或 4 观察月度规律。分解图里能清楚看到冬季峰值和夏季谷值的周期性这和前面箱线图的结论互相印证。想做预测就用 ARIMA但注意空气污染数据往往带周期纯 ARIMA 对长期预测没什么价值短时预测还能看看。把数据切成训练集和测试集拟合后用 RMSE 对比预测与真实值from statsmodels.tsa.arima.model import ARIMA train, test weekly[:40], weekly[40:] model ARIMA(train, order(1, 1, 1)).fit() forecast model.forecast(stepslen(test)) rmse ((forecast - test) ** 2).mean() ** 0.5 print(fRMSE: {rmse:.2f} μg/m³)order(1,1,1)是最简单的一阶自回归加一阶差分组合不要贪高参数数据量不大时高阶复杂度只会增加过拟合风险。实际项目里我最后悔的一件事就是第一版把所有结论都建立在一套有重复时间戳的日均值上直到做预测时发现残差异常才回头重算了清洗逻辑。吃一堑长一智现在每出一个图之前我都会先打印一行df.shape和缺失值总数确认数据状态没问题再继续。这个习惯让我在后面的课设和实习项目里少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →