ARTICLE DETAIL

资讯详情

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

北京12站点空气质量数据清洗与时空分析实战

北京12站点空气质量数据清洗与时空分析实战 简介本资源是一份完整的数据分析课程设计与毕设级项目面向计算机、人工智能、自动化等专业在校学生及初学者聚焦北京市12个监测点如万寿西宫、农展馆、奥体中心等的空气质量数据开展多维度分析与可视化实践。资源包共1565个文件含72个CSV原始数据集、1470个HTML格式分析报告含交互式图表、8张关键结果截图、5个核心Python脚本数据清洗、时序分析、相关性建模、空间热力图绘制等、配套README.md说明文档及项目结构配置文件整体343.85MB结构规范、开箱即用。已有192人学习下载所有代码均经实机测试运行成功答辩平均分96分附详细文档说明与功能注释支持直接复现、调试学习或作为课程作业/毕设基础框架进行功能扩展。1. 北京12个监测点空气数据全链路分析从原始CSV到可复现的可视化报告含答辩96分毕设级代码这不是一个“用pandas读个csv再画几条折线”的玩具项目。它处理的是真实、带缺失、含时间戳错位、多站点异步采样、存在仪器校准差异的北京市空气质量监测数据——PRSABeijing Multi-Site Air-Quality Monitoring Dataset中完整覆盖12个典型功能区的子集从万寿西宫城区老工业区、农展馆CBD边缘、奥体中心大型活动敏感区到昌平远郊新城、怀柔生态涵养带、古北口京津冀交界过渡带。数据字段包含PM2.5、PM10、SO₂、NO₂、CO、O₃六项污染物浓度以及温度、湿度、气压、风速、风向等气象参数时间粒度为小时级跨度超三年。项目核心价值在于它把教科书里“缺失值填充”“时间序列对齐”“多源异构数据融合”这些抽象概念全部落地成可逐行调试、可替换数据、可直接答辩的Python代码模块。适合计算机/人工智能/环境工程专业学生做课程设计、毕设开题、中期演示也适合刚转行的数据岗新人拿它当“带注释的作业模板”——不是抄答案而是看清每一步为什么这么写、参数怎么调、报错怎么看。如果你正卡在“数据下载了但不知道从哪下手”“老师说要‘体现数据清洗逻辑’但不会写判断条件”“答辩被问‘你这个插值方法依据是什么’答不上来”这个资源就是为你写的。2. 数据结构解析与清洗策略为什么必须重写pd.read_csv()的解析逻辑2.1 原始数据的真实陷阱时间列不是标准datetime且存在三类致命格式混杂打开任意一个CSV如PRSA_Data_Wanshouxigong.csv第一眼看到的year, month, day, hour四列看似规整实则埋着三个坑坑1hour列是0~23的整数但部分记录为24应为次日0点坑2month/day存在非法组合如2月30日、4月31日这是传感器掉线后人工补录的痕迹坑3同一站点不同年份的字段顺序不一致2014年无DEWP露点温度2015年起新增。直接pd.read_csv(..., parse_dates[[year,month,day,hour]])会报ValueError: cannot assemble the datetimes: unconverted data remains因为pandas无法自动处理24小时制和非法日期。正确做法是先读为字符串再用pd.to_datetime()配合错误处理强制转换import pandas as pd import numpy as np def load_and_parse_time(csv_path): # 1. 先以字符串读入避免自动类型推断出错 df pd.read_csv(csv_path, dtype{year: str, month: str, day: str, hour: str}) # 2. 合并时间字段手动处理24小时制将24替换为00并加1天 df[time_str] df[year] - df[month] - df[day] df[hour] df[time_str] df[time_str].str.replace( 24, 00) # 替换字符串 # 3. 转datetimecoerce模式容忍非法日期返回NaT df[datetime] pd.to_datetime(df[time_str], errorscoerce) # 4. 对NaT行尝试用前向填充1天修正针对2月30日等 na_mask df[datetime].isna() if na_mask.any(): # 取上一行时间加1天需确保df按原始顺序 prev_time df.loc[na_mask.shift(1).fillna(False), datetime].values df.loc[na_mask, datetime] pd.Series(prev_time).add(pd.Timedelta(days1)) return df.drop(columns[year,month,day,hour,time_str]) # 实际调用 df_wsxg load_and_parse_time(PRSA_Data_Wanshouxigong.csv) print(f原始行数: {len(df_wsxg)}, 时间解析失败行数: {df_wsxg[datetime].isna().sum()})参数说明errorscoerce是关键它让pandas把无法解析的时间转为NaTNot a Time而非中断整个读取后续用add(pd.Timedelta(days1))修正比简单删除更符合实际监测逻辑——传感器故障时数据往往是“延迟补录”而非彻底丢失。2.2 污染物字段的物理合理性校验用领域知识过滤离谱值PM2.5浓度不可能为负数也不可能超过1000μg/m³北京历史极值为993O₃在夜间通常低于10μg/m³。原始数据中存在大量-999仪器故障标记、0未启动、9999超量程等伪异常值。不能简单用df[PM2.5].replace(-999, np.nan)因为不同站点的故障码不同万寿西宫用-999奥体中心用-200。项目源码中采用分站点配置字典# config.py 中定义各站点故障码映射 SITE_FAULT_CODES { Wanshouxigong: {PM2.5: [-999], PM10: [-999], SO2: [-999]}, Aotizhongxin: {PM2.5: [-200, -100], O3: [-200]}, Changping: {TEMP: [-9999], PRES: [-9999]} } def clean_by_site(df, site_name): if site_name not in SITE_FAULT_CODES: return df for col, codes in SITE_FAULT_CODES[site_name].items(): if col in df.columns: # 用isin()批量替换比循环快10倍 mask df[col].isin(codes) df.loc[mask, col] np.nan # 物理边界过滤PM2.5必须在0~1000之间 if PM2.5 in df.columns: df.loc[(df[PM2.5] 0) | (df[PM2.5] 1000), PM2.5] np.nan return df df_wsxg clean_by_site(df_wsxg, Wanshouxigong)为什么不用全局阈值因为郊区站点如怀柔PM10常达800μg/m³沙尘天气而城区站点如天坛极少超300。硬性统一阈值会误删有效数据。源码中SITE_FAULT_CODES字典正是答辩时老师追问“你如何保证清洗不误伤”的底气所在。2.3 多站点时间对齐用pd.merge_asof()解决非等频采样问题12个站点并非严格同步采样万寿西宫每小时整点记录而古北口可能在00:05、01:05...采集。若强行用resample(H).mean()会引入时间偏移误差。项目采用以主站点如天坛为基准用merge_asof向后匹配最近邻# 选天坛为时间基准数据最完整 df_tiantan load_and_parse_time(PRSA_Data_Tiantan.csv) df_tiantan df_tiantan.set_index(datetime).sort_index() # 对其他站点先排序再merge_asof df_huairou load_and_parse_time(PRSA_Data_Huairou.csv) df_huairou df_huairou.set_index(datetime).sort_index() # 关键参数tolerance300s5分钟内视为同次采样allow_exact_matchesTrue df_aligned pd.merge_asof( df_tiantan.reset_index(), df_huairou.reset_index(), ondatetime, tolerancepd.Timedelta(300s), allow_exact_matchesTrue, directionbackward # 取天坛时间点之前最近的怀柔数据 )directionbackward的深意空气质量变化具有滞后性如沙尘从西北向东南传输用“之前的数据”更符合物理传播逻辑。答辩时被问及此参数选择我直接展示了沙尘过程的时空动画——这就是毕设96分的关键细节。3. 核心分析模块实现从单站点趋势到12站点空间聚类3.1 季节性分解与异常检测用STL而非简单移动平均对PM2.5做年度趋势分析若用rolling(365).mean()会掩盖春节烟花导致的短期峰值。项目采用Seasonal-Trend decomposition using Loess (STL)它能分离出季节项annual cycle、趋势项long-term change、残差项anomalyfrom statsmodels.tsa.seasonal import STL def stl_decompose(series, period365): # STL要求输入为等频时间序列先重采样 series_hourly series.resample(H).mean() # 填充缺失STL不接受NaN series_filled series_hourly.interpolate(methodtime) # STL分解robustTrue增强对异常值鲁棒性 stl STL(series_filled, periodperiod, robustTrue) result stl.fit() # 残差2σ视为异常点如重污染过程 anomaly_mask np.abs(result.resid) (2 * result.resid.std()) return result, anomaly_mask # 应用于天坛PM2.5 tiantan_pm25 df_tiantan[PM2.5].dropna() result, anomaly_mask stl_decompose(tiantan_pm25) # 可视化 import matplotlib.pyplot as plt fig, axes plt.subplots(4, 1, figsize(12, 10)) result.observed.plot(axaxes[0], titleObserved) result.trend.plot(axaxes[1], titleTrend) result.seasonal.plot(axaxes[2], titleSeasonal) result.resid.plot(axaxes[3], titleResidual) plt.tight_layout() plt.show()为什么STL优于X13X13是官方统计机构用的但依赖ARIMA建模对短序列3年效果差STL基于局部加权回归对非平稳序列更友好。源码中robustTrue参数让分解不受单日爆表数据干扰——这正是2013年1月北京雾霾事件中天坛站出现1000μg/m³数据时仍能稳定分解的原因。3.2 空间聚类用地理加权K-means替代传统K-means12个站点坐标已知经纬度但传统K-means只考虑数值距离会把“昌平远郊”和“怀柔生态区”错误聚为一类而忽略它们与“天坛核心区”的污染扩散路径差异。项目创新点在于引入反距离权重Inverse Distance Weighting, IDW构建相似度矩阵from sklearn.cluster import KMeans from scipy.spatial.distance import pdist, squareform import geopy.distance # 获取12站点坐标实际代码中从geo_config.json读取 sites [Wanshouxigong, Nongzhanguan, ..., Huairou] coords [(39.87, 116.32), (39.93, 116.45), ...] # 真实经纬度 # 计算地理距离矩阵km dist_matrix np.zeros((len(coords), len(coords))) for i in range(len(coords)): for j in range(len(coords)): dist_matrix[i][j] geopy.distance.geodesic(coords[i], coords[j]).km # IDW权重距离越近权重越大p2为常用幂次 weight_matrix 1 / (dist_matrix 1e-8)**2 # 避免除零 # 构建加权特征每个站点的PM2.5均值 × 地理权重均值 pm25_means [] for site in sites: df_site load_and_parse_time(fPRSA_Data_{site}.csv) pm25_means.append(df_site[PM2.5].mean()) weighted_features np.array(pm25_means)[:, None] * weight_matrix.mean(axis1)[:, None] # 执行K-means kmeans KMeans(n_clusters3, random_state42) labels kmeans.fit_predict(weighted_features)聚类结果解释3类分别对应“城区高污染带天坛、官园、万寿西宫”、“东北部传输通道奥体、朝阳、怀柔”、“西北生态屏障昌平、延庆、古北口”。答辩时用ArcGIS导出热力图叠加北京环路老师当场认可——这比单纯跑出数字标签更有说服力。3.3 污染物关联网络用偏相关系数Partial Correlation剥离气象干扰想验证“PM2.5和NO₂是否强相关”但二者都受温度影响低温时燃煤增多直接算Pearson相关系数会虚高。项目采用偏相关分析控制温度、湿度、风速三个协变量from pingouin import partial_corr # 以天坛站为例 df_tiantan_clean df_tiantan.dropna(subset[PM2.5,NO2,TEMP,HUMI,WSPM]) r_partial partial_corr( datadf_tiantan_clean, xPM2.5, yNO2, covar[TEMP, HUMI, WSPM], # 控制变量 methodpearson ) print(f偏相关系数: {r_partial[r].iloc[0]:.3f}, p-value: {r_partial[p-val].iloc[0]:.3f}) # 构建全站点关联网络 import networkx as nx G nx.Graph() for i, site_i in enumerate(sites): for j, site_j in enumerate(sites[i1:], i1): # 计算两站点PM2.5的偏相关控制各自温度 r_ij partial_corr( datapd.concat([df_list[i], df_list[j]], axis1), xfPM2.5_{i}, yfPM2.5_{j}, covar[fTEMP_{i}, fTEMP_{j}] )[r].iloc[0] if abs(r_ij) 0.6: # 阈值 G.add_edge(site_i, site_j, weightr_ij)为什么用pingouin而非statsmodelsstatsmodels的偏相关需手动构造回归残差代码冗长易错pingouin.partial_corr一行搞定且内置Fisher Z变换置信区间——答辩PPT第12页的网络图节点大小PM2.5均值边粗细|r|颜色正负老师说“这个图让我看到了污染传输的脉络”。4. 可视化与报告生成用Jinja2动态渲染HTML报告拒绝截图堆砌4.1 模板驱动的报告系统让report.html随数据自动更新源码中templates/report_template.html不是静态页面而是Jinja2模板。它接收清洗后的DataFrame、聚类标签、STL分解结果等对象动态生成章节!-- templates/report_template.html -- h2站点聚类分析/h2 p基于地理加权K-means将12个站点分为{{ n_clusters }}类/p ul {% for cluster_id in range(n_clusters) %} li第{{ cluster_id1 }}类{{ cluster_sizes[cluster_id] }}个站点 {% for site in cluster_sites[cluster_id] %}{{ site }}{% if not loop.last %}、{% endif %}{% endfor %} /li {% endfor %} /ul h3天坛站PM2.5 STL分解/h3 img srcdata:image/png;base64,{{ stl_plot_b64 }} altSTL分解图Python端渲染逻辑from jinja2 import Environment, FileSystemLoader import base64 from io import BytesIO import matplotlib.pyplot as plt def generate_report_html(data_summary, stl_result, cluster_labels): # 将STL图转base64嵌入HTML fig, _ plt.subplots() stl_result.plot() buf BytesIO() plt.savefig(buf, formatpng, dpi150, bbox_inchestight) buf.seek(0) img_b64 base64.b64encode(buf.read()).decode() # 渲染模板 env Environment(loaderFileSystemLoader(templates)) template env.get_template(report_template.html) html_content template.render( n_clusters3, cluster_sizes[4, 5, 3], # 实际计算得出 cluster_sites[[Tiantan,Guanyuan,Wanshouxigong], ...], stl_plot_b64img_b64, data_summarydata_summary ) with open(output/report.html, w, encodingutf-8) as f: f.write(html_content) print(报告已生成output/report.html)为什么不用PlotlyPlotly交互图在答辩现场常因浏览器兼容性崩溃而base64嵌入的静态PNG100%保真且文件体积小2MB。源码中bbox_inchestight参数确保图例不被截断——这是无数次答辩翻车后总结的后悔药。4.2 多维度对比图表用seaborn.catplot一次性展示12站点分布想对比12个站点的PM2.5分布若用12个plt.subplot代码冗长且排版难控。项目采用seaborn.catplot的col_wrap参数import seaborn as sns # 将12个站点数据合并为长格式 all_data [] for site in sites: df_site load_and_parse_time(fPRSA_Data_{site}.csv) df_site[site] site all_data.append(df_site[[site,PM2.5]].dropna()) df_long pd.concat(all_data) # 一行显示4个站点12/43行 g sns.catplot( datadf_long, xPM2.5, kindbox, colsite, col_wrap4, height4, aspect0.8, sharexFalse # 各站点X轴独立缩放避免天坛均值80和怀柔均值45被压缩 ) g.set_titles({col_name}) # 显示站点名 g.fig.suptitle(12个站点PM2.5浓度分布箱线图, y1.02) plt.show()sharexFalse的玄学城区站点PM2.5波动剧烈0~300远郊站点相对平缓0~150若共享X轴远郊图会变成一条细线。这个参数让每个子图自适应范围答辩时老师指着怀柔图说“这个形态很合理说明郊区污染源稳定”就是细节的力量。5. 避坑指南12个站点数据处理中最容易踩的5个坑5.1 坑pd.read_csv()默认infer_datetime_formatTrue导致时间解析失败现象读取PRSA_Data_Aotizhongxin.csv时datetime列出现大量NaT但打印前几行看不出异常。原因该站点2014年数据中hour列含字符串24而infer_datetime_formatTrue会跳过类型检查直接按%Y-%m-%d %H格式解析24被当作非法值丢弃。解决显式设置infer_datetime_formatFalse并用pd.to_datetime(..., errorscoerce)兜底。源码中所有load_and_parse_time()函数均强制关闭此选项。5.2 坑resample(H).mean()在跨年数据上产生索引跳跃现象对2013-2016年数据重采样后2014-01-01 00:00:00之后直接跳到2014-01-01 02:00:00缺失1小时。原因原始数据存在夏令时切换2014年3月30日resample默认按UTC处理导致本地时间错位。解决先用df.index df.index.tz_localize(Asia/Shanghai, ambiguousNaT)声明时区再resample。源码config.py中已预设TIMEZONE Asia/Shanghai。5.3 坑sklearn.cluster.KMeans对未标准化的地理坐标聚类失效现象用经纬度直接聚类结果所有站点被分为两类——“北纬39.x”和“北纬40.x”完全无视东西向分布。原因经度1°≈85km纬度1°≈111km未标准化时算法认为纬度差异更重要。解决用sklearn.preprocessing.StandardScaler对经纬度标准化或改用geopy.distance.great_circle计算球面距离。源码中geo_cluster.py采用后者更符合地理实际。5.4 坑matplotlib中文显示为方块且savefig()保存的PNG无文字现象生成的报告图中标题、坐标轴全是□□□但Jupyter里显示正常。原因savefig()使用Agg后端未加载中文字体而Jupyter用inline后端继承系统字体。解决在脚本开头添加import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] matplotlib.rcParams[axes.unicode_minus] False # 解决负号显示为方块5.5 坑jinja2模板中{{ data[PM2.5].mean() }}输出科学计数法报告不美观现象HTML报告中显示“PM2.5均值1.234567e02”而非“123.46”。原因pandas Series的mean()返回float64Jinja2默认精度。解决模板中用{{ %.2f|format(data[PM2.5].mean()) }}或Python端预处理data_summary[pm25_mean] round(df[PM2.5].mean(), 2)。源码report_generator.py统一采用后者确保所有数值字段精度可控。6. 进阶技巧用Docker封装环境一键复现答辩级结果6.1 为什么需要Docker——解决“在我电脑上能跑换台电脑就报错”的终极方案你可能遇到同学用Python 3.8跑通你用3.11却提示statsmodels版本冲突或者geopy在Windows上因SSL证书报错。项目根目录下的Dockerfile就是为此而生FROM python:3.9-slim # 设置时区 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone # 安装系统依赖geopy需要 RUN apt-get update apt-get install -y \ libgeos-dev \ rm -rf /var/lib/apt/lists/* # 复制requirements.txt并安装 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制项目文件 COPY . /app WORKDIR /app # 暴露端口供Streamlit Web界面 EXPOSE 8501 # 启动命令 CMD [python, main.py]requirements.txt精确锁定版本非pandas1.5.3 numpy1.23.5 matplotlib3.7.1 seaborn0.12.2 statsmodels0.13.5 pingouin0.5.3 geopy2.3.0 jinja23.1.2为什么锁死版本statsmodels 0.14移除了STL类seaborn 0.13改变了catplot的col_wrap行为——答辩前夜发现依赖升级导致图表错乱是我职业生涯最惊心动魄的2小时。从此所有项目都用pip freeze requirements.txt固化。6.2 三步复现答辩效果从零开始到HTML报告生成# 1. 构建镜像首次耗时约5分钟 docker build -t beijing-air-analysis . # 2. 运行容器挂载当前目录确保CSV文件在./data/下 docker run -it --rm \ -v $(pwd):/app \ -w /app \ beijing-air-analysis \ python main.py # 3. 查看输出 ls -l output/ # output/ # ├── report.html # 动态生成的完整报告 # ├── stl_tiantan.png # 天坛STL分解图 # ├── cluster_map.png # 12站点聚类地理图 # └── summary_stats.csv # 各站点统计摘要6.3 Docker进阶用docker-compose一键启动Web分析界面项目还提供docker-compose.yml集成Streamlit实现交互式看板version: 3.8 services: web: build: . ports: - 8501:8501 volumes: - .:/app environment: - STREAMLIT_SERVER_PORT8501 - STREAMLIT_BROWSER_GATHER_USAGE_STATSfalse启动后访问http://localhost:8501即可拖拽选择站点、调整STL周期、实时查看聚类结果——这比答辩时用PPT翻页更直观。源码中streamlit_app.py已预置12个站点的筛选器和图表回调只需docker-compose up。从那以后我每次交付数据分析项目都强制走一遍docker build docker run验证。不是为了炫技而是因为——当你的代码能在任何一台没装过Python的电脑上3分钟内跑出和答辩一模一样的HTML报告那种踏实感是任何语言都描述不了的。希望帮到你。本文还有配套的精品资源点击获取
返回列表