十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

北京空气监测数据分析:基于pandas的清洗与可视化实践

北京空气监测数据分析:基于pandas的清洗与可视化实践 简介这是一份围绕北京市12个地点空气监测数据展开的数据分析大作业项目面向计算机、数据分析相关专业的在校学生和毕设人员可作为课程设计、大作业或毕业设计初稿使用。压缩包内共1565个文件整体约343.85MB核心内容包括72个CSV监测数据文件、5个Python分析脚本、1470个HTML可视化结果页面以及文档说明和效果截图方便对照学习与二次修改。项目对多站点空气质量数据进行了读取、清洗、统计与可视化展示源码经过运行验证作者还提供远程教学和答疑支持。目前已有191人学习下载若需快速完成相似主题的空气数据分析任务这份资料提供了较完整的可复用方案。1. 北京12个地点空气监测数据分析项目在解决什么问题数据分析大作业最怕的不是题目太大而是拿到数据之后不知道每一步该验证什么。北京 12 个地点对应监测站点的空气监测数据包含时间、地点、AQI 和六项污染物浓度是典型的二维面板数据。这个项目要做的是三件事把分散的监测记录整理成标准表格把统计规律算出来再用图表把规律讲清楚。整个过程围绕 pandas 清洗、分组聚合和可视化展开覆盖代码实现、源代码与文档交付的完整链路。这个题目适合正在选 Python 数据分析大作业题目的学生也适合想把真实监测数据做成作品集的求职者。前者照着完整流程复现即可后者可以把方法迁移到其他城市或更长周期的监测数据上。字段按通用空气质量格式设计替换成本地数据后代码可以直接套用分析结论也更容易在面试时讲成一段完整的故事。2. 数据准备阶段pandas 合并 12 个监测点数据并完成清洗2.1 先对齐监测数据字段与单位再写合并代码拿到监测数据的第一步不是写代码而是把字段含义和单位梳理清楚。北京各监测站点的记录通常包含以下字段字段名含义单位date监测时间yyyy-mm-dd hh:mm:ssstation监测点名称文本AQI空气质量指数无量纲PM2.5 / PM10颗粒物浓度µg/m³SO2 / NO2 / O3气态污染物浓度µg/m³CO一氧化碳浓度mg/m³这里最容易出错的是 CO 的单位。PM2.5、PM10、SO2、NO2、O3 习惯用 µg/m³而 CO 用 mg/m³两者相差 1000 倍。如果在清洗阶段不统一单位后面计算相关性、画热力图时会出现量纲错配结论完全不可信。我一般会在读取后把 CO 统一换算成 µg/m³再进入后续流程。换算不复杂但在代码里留一行注释复查时就能知道这个口径是谁定的。另一个容易忽略的点是 12 个监测点是否各自一个文件。如果是按监测点拆分的 CSV文件名里通常带有城区名称这个信息本身就是一条维度要显式提取出来加到数据里而不是在合并后手动补。丢了文件名信息后面按地点分组统计时就会看到一个空表或者全部堆在同一个标签下。2.2 glob 批量读取 CSV 并用 concat 合并监测文件常见做法是把所有监测文件放进一个 data 目录用 glob 匹配全部 CSV逐个读取并提取监测点名称最后 concat 拼接。参考代码如下import glob import pandas as pd all_files glob.glob(data/*.csv) df_list [] for path in all_files: # 文件名格式: data/北京_东城_2023.csv - 取 东城 作为监测点名称 station path.split(/)[-1].split(_)[1] df pd.read_csv(path, encodingutf-8) df[station] station df_list.append(df) # 纵向合并所有监测点的数据 df pd.concat(df_list, ignore_indexTrue) print(df.shape) # (总行数, 总列数) print(df[station].value_counts()) # 检查 12 个监测点是否齐全这段代码做了三件事用 glob 拿到目录下所有 CSV 路径循环读取每个文件并为每一行打上监测点标签最后用 concat 把 12 个文件纵向堆叠。ignore_indexTrue 让合并后的行号重新从 0 编号避免不同文件各自从 0 开始造成索引重复。打印 value_counts 是为了看各监测点的样本量是否均衡如果某个站点缺了一大段数据统计分析时它会明显拖后腿需要回到数据源头确认。read_csv 的参数需要按实际数据调整。国产监测数据很多是 GBK 编码直接读会抛 UnicodeDecodeError把 encoding 改成 gbk 即可。时间列如果被读成字符串要第一时间用 pd.to_datetime 转换否则后续 set_index、resample 全部会出问题。文件很大时可以用 usecols 只读取需要的列再配合 dtype 指定浓度列的类型能省下不少内存。2.3 缺失值插值与 IQR 异常值剔除空气监测设备经常因为维护、断电等原因产生整段缺失传感器异常还会产生负值或离谱的大数。动手清洗前先做一次体检# 统计各列缺失值数量 print(df.isna().sum()) # 检查浓度列是否存在非法负值 pollutants [PM2.5, PM10, SO2, NO2, CO, O3] for col in pollutants: neg_count (df[col] 0).sum() print(f{col}: {neg_count} 个负值)缺失值处理分两种场景。零星缺失用线性插值但要按监测点分组后分别插值避免把不同地点的数值混在一起估计for col in pollutants: df[col] ( df.groupby(station)[col] .transform(lambda s: s.interpolate(limit_directionboth)) )transform 返回与原 df 等长的结果lambda 里的 s 是每个监测点单独的一列序列interpolate 用前后值线性补齐空缺。limit_directionboth 允许序列开头和结尾的缺失也参与填充。对于连续缺失超过该站点总记录 5% 的站点直接插值会制造出人为平滑的数据这时候改成用该站点当月的观测中位数填充并在文档说明里记录填充规则。提示插值后对比一下填充前后的均值如果变化超过 10%说明缺失不是随机的需要检查原始数据来源。异常值用 IQR 方法剔除对每个监测点、每项污染物分别计算 Q1 和 Q3把低于 Q1-1.5IQR、高于 Q31.5IQR 的点标记为异常。空气浓度分布通常是右偏的异常点大多偏大去掉之后统计均值会更贴近典型水平。清洗结束时记录原始行数和清洗后行数这两个数字要写进文档说明是评分和复查时最常被追问的细节。3. 核心统计分析AQI 与六项污染物的分组聚合和相关性计算3.1 groupby 和 agg 计算各监测点统计指标清洗完成后的第一层分析是按监测点分组算描述统计量快速排出 12 个地点的污染水平高下stats df.groupby(station).agg( AQI_mean(AQI, mean), AQI_max(AQI, max), PM25_mean(PM2.5, mean), PM25_p90(PM2.5, lambda x: x.quantile(0.9)), O3_mean(O3, mean), ).round(2) print(stats.sort_values(AQI_mean, ascendingFalse))agg 里用 (列名, 函数) 元组的写法是 pandas 2.x 推荐风格比旧式字典 {AQI: mean} 在列重命名上更直接。PM25_p90 用 lambda 取 90 分位数比最大值更稳能反映高污染日的水平而不会被单次极端值带偏。排序用 ascendingFalse污染最重的站点出现在第一行横向对比的结论一眼可见。如果需要全量统计指标df.groupby(station).describe() 一次给出 count、mean、std、min、四分位数和 max缺点是多级索引处理起来麻烦。我的习惯是探索阶段用 describe最终写入报告的统计表用 agg 自定义只保留和结论相关的列避免文档里出现一堆用不上的数字。3.2 时间重采样与 AQI 等级占比统计小时级数据直接分析噪声很大需要先重采样到日或月粒度。分析 AQI 等级占比时要先建立等级划分标准这也是报告中一张非常标准的参照表AQI 范围等级含义0-50一级优51-100二级良101-150三级轻度污染151-200四级中度污染201-300五级重度污染300六级严重污染计算每个监测点各等级天数的占比是报告中很有说服力的一个统计结果df[date] pd.to_datetime(df[date]) df df.set_index(date) # 用 cut 把 AQI 切成等级区间 bins [0, 50, 100, 150, 200, 300, float(inf)] labels [优, 良, 轻度, 中度, 重度, 严重] df[aqi_level] pd.cut(df[AQI], binsbins, labelslabels, rightTrue) # 各监测点等级天数占比 level_ratio ( df.groupby([station, aqi_level]) .size() .unstack(fill_value0) ) level_ratio level_ratio.div(level_ratio.sum(axis1), axis0) print(level_ratio.round(4))pd.cut 的 bins 和 labels 要一一对应rightTrue 表示区间右闭即 50 归入良而不是优。先按两个维度 groupby 再 size得到每个监测点在每个等级上的天数unstack 把等级变成列fill_value0 保证没有出现过的等级显示 0 而不是 NaN。最后用 div 按行归一化成占比。这张表比单一的平均 AQI 更能说明污染天数的分布结构放在报告结论部分比一张大图更直观。按月重采样的代码也很常见df.resample(ME).agg({AQI: mean, PM2.5: mean})。注意 pandas 2.2 之后建议用 ME 表示日历月末旧写法 M 会触发 FutureWarning。如果想看小时规律先 df[hour] df.index.hour再按 hour 和 station 分组取均值能看出早晚高峰各站点 NO2 抬升的时段差异。3.3 污染物相关性矩阵与 AQI 贡献度解读六项污染物和 AQI 之间谁强谁弱相关系数矩阵一次算清import seaborn as sns corr df[[AQI, PM2.5, PM10, SO2, NO2, CO, O3]].corr() sns.heatmap(corr, annotTrue, fmt.2f, cmapRdYlBu_r, vmin-1, vmax1, xticklabelsTrue, yticklabelsTrue)corr 默认计算 Pearson 相关系数。空气质量数据右偏明显改用 methodspearman 计算秩相关会更抗离群点。vmin 和 vmax 固定色标范围防止某对变量相关性特别高时压缩其他格子的颜色差异。解读时重点看 AQI 所在行PM2.5 与 AQI 的相关性通常最高O3 在夏季会显著上升CO 与 SO2 的相关性则反映燃煤排放的共同来源。解释结论时要注意AQI 是分指数的函数而非独立测量值PM2.5 和 AQI 相关高有一部分原因是指标体系自身的设计不能简单说成PM2.5 导致了 AQI 升高。把这一层写进报告能直接体现对指标体系的理解深度比单纯贴一张热力图更有说服力。4. 可视化呈现12 个监测点污染水平的横向对比4.1 箱线图对比 PM2.5 分布并解决中文乱码箱线图是横向对比多个地点浓度分布的首选中位数、四分位距、离群点一张图全部呈现。下面是 12 个监测点 PM2.5 的对比代码import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False plt.figure(figsize(14, 6)) sns.boxplot(datadf, xstation, yPM2.5) plt.xticks(rotation45) plt.title(12 个监测点 PM2.5 浓度分布对比) plt.tight_layout() plt.savefig(output/pm25_boxplot.png, dpi200)这里集中解决两个高频问题。中文乱码rcParams 把全局字体改成 SimHei同时关闭 Unicode 负号显示。Windows 下 SimHei 开箱即用Linux 服务器没有这个字体需要先执行 fc-list 查看可用的 CJK 字体再改成对应的字体名称。图像输出dpi200 适合插入 Word 或 PDF 报告再高只增加文件体积tight_layout 防止 x 轴标签旋转 45 度后被画布边缘截断。提示如果保存的图片标题或坐标轴出现方框说明系统缺中文字体换个路径安装字体比在代码里反复试字体名更省时间。读箱线图时关注三个特征中位数高的站点说明日常污染水平高箱体长的站点说明日间波动大上方的离群点要回查日期确认是否对应某次重污染过程。把观察到的异常站点与第三章的统计表互相印证结论才站得住。4.2 热力图展示 12 个监测点与污染物的对应关系第二张核心图是监测点乘污染物的热力图一行一个地点一列一种污染物一眼看出每个站点的污染画像pivot df.pivot_table( values[PM2.5, PM10, SO2, NO2, O3, CO], indexstation, aggfuncmean ) # 逐列归一化到 0-1消除量纲影响 pivot_scaled (pivot - pivot.min()) / (pivot.max() - pivot.min()) sns.heatmap(pivot_scaled, annotTrue, fmt.2f, cmapYlOrRd, linewidths0.5) plt.tight_layout() plt.savefig(output/station_pollutant_heatmap.png, dpi200)pivot_table 里 values 传列表、index 传 station生成 12 行乘 6 列的均值表。直接画会有个问题CO 是 mg/m³ 量级其他污染物是 µg/m³ 量级数值范围差出千倍热力图的颜色全被 CO 支配。这里用 min-max 归一化逐列压缩到 0-1颜色表示的是该污染物在不同监测点之间的相对高低同一列内部的比较关系保持不变跨列的绝对值则不再具备可比性。如果需要可比先做标准化再画图语义会变成偏离平均水平的程度。如果 12 个监测点的热点呈现明显的城区-郊区梯度或南北差异可以在文档说明里结合地形和产业布局讨论原因。这一层不需要额外代码但能把数据可视化项目从画图提升到解释数据的层面答辩时是非常加分的叙述线。4.3 时间序列折线图叠加全市均值与单站点最后一张必出的图是时间趋势。建议双线叠加全市均值一条线污染最重的监测点一条线直观拉开平均线和最差点的差距daily_all df.resample(D)[AQI].mean() daily_worst df[df[station] 东城].resample(D)[AQI].mean() plt.figure(figsize(14, 5)) plt.plot(daily_all.index, daily_all.values, label全市均值, linewidth1.5) plt.plot(daily_worst.index, daily_worst.values, label东城, linewidth0.9, alpha0.8) plt.legend() plt.ylabel(AQI) plt.tight_layout() plt.savefig(output/aqi_trend.png, dpi200)resample(D) 分别对全市和单站点取日均值。两条线用不同参数拉开层次全市均值线粗而实单站点线细且半透明视觉上自然形成主体参照的关系。数据跨度超过一年时 x 轴日期会非常密用 matplotlib.dates 的 MonthLocator 和 DateFormatter 控制刻度间隔只显示月份标签。配色上避免红绿对比蓝橙搭配对色弱读者更友好。到这一步报告的三张主图齐了对应关系如下图表回答的问题关键参数箱线图12 个监测点谁的污染高、波动大xstation, yPM2.5热力图每个站点以哪种污染物为主pivot_table min-max 归一化折线图污染随时间怎么变化resample(D) 与双线叠加三张图加上第三章的统计表正好构成地点、污染物、时间三个维度的完整证据链这是 Python 数据分析与可视化项目里最标准的呈现结构。5. 结果导出与验证源代码、截图和文档说明的收尾技巧5.1 ExcelWriter 一次导出多张统计表报告写作时把统计结果导出成 Excel 是最省事的交付方式。ExcelWriter 可以把多张表写进同一个工作簿的不同 sheetwith pd.ExcelWriter(output/analysis_result.xlsx, engineopenpyxl) as writer: stats.to_excel(writer, sheet_name地点统计, indexTrue) monthly.to_excel(writer, sheet_name月度趋势, indexTrue) corr.to_excel(writer, sheet_name相关性矩阵, indexTrue) level_ratio.to_excel(writer, sheet_name等级占比, indexTrue) pivot.to_excel(writer, sheet_name污染物均值, indexTrue)engineopenpyxl 是 xlsx 的默认引擎。导出后重新打开文件检查数字格式保留两位小数即可不要出现 -0.00 这类浮点噪音写表前统一 round(2) 处理。带 MultiIndex 的表在导出前先 reset_index避免 Excel 里出现层级列。5.2 验证代码可复现的三个动作交付源代码时验证比代码本身更常被忽视。三个动作能拦截大部分问题第一把 data 目录复制到另一个路径重新执行一遍确认没有写死绝对路径第二用官方发布的月报数字交叉核对12 个监测点的 AQI 均值应该落在合理区间对不上就优先怀疑单位换算和缺失值填充策略第三检查输出文件的时间戳和内容是否同步避免文档里的截图和代码跑出的结果是两套数字。更进一步的验证是做一次层次聚类把 12 个监测点按污染物均值特征聚类看结果是否和行政区划、功能区划吻合from scipy.cluster.hierarchy import linkage, dendrogram Z linkage(pivot_scaled, methodward) plt.figure(figsize(8, 6)) dendrogram(Z, labelspivot_scaled.index, leaf_rotation90) plt.tight_layout() plt.savefig(output/station_cluster.png, dpi200)methodward 按方差最小化原则合并类别适合连续型浓度数据。pivot_scaled 已经归一化过可以直接作为输入。如果聚类分组和直觉上的城区组、郊区组差异很大值得深挖往往能发现某个监测点附近有固定污染源这反而是报告里最有价值的发现。5.3 文档说明与截图的组织规范文档说明建议按这个骨架写数据来源与字段说明、清洗规则、分析方法、结果结论、文件清单。截图放到对应的分析章节里不要集中贴在最后。截图命名用 图1_监测点箱线图.png 这种格式和文档里的图编号一一对应终端运行截图要截全包含命令和输出结果不要只截一半。最后检查代码里是否有调试残留比如临时 print 的中间变量、写死的测试路径这些细节在答辩时比代码逻辑更容易被挑出来。本文还有配套的精品资源点击获取
返回列表