建模与可视化)
简介本资源是一套基于Python的数据挖掘与机器学习技术实现的空气质量可视化分析系统面向环境科学、数据科学及Web全栈开发学习者聚焦大气污染时空演化建模与传播网络构建等实际科研问题。压缩包含2000个文件主体为697个Python后端模块含Flask服务、数据处理与算法实现和1295个JSON格式空气质量时序数据覆盖2013–2017年多城市监测记录辅以HTML/CSS/JS前端页面及D3、ECharts、Mapbox可视化组件整体大小76.43MB。已有253人学习下载资源结构清晰files目录存放原始与挖掘后数据static与templates分别管理静态资源与首页入口dataManager.py为核心数据模块main.py一键启动服务。读者可直接复现城市群聚类划分、大气污染传输网络APTN构建及多维空间渐进式探索分析全流程获得从数据清洗、特征降维、网络建模到交互式可视化的完整科研级工程实践范例。1. 这不是又一个“画折线图”的空气质量项目它用粒子输运建模污染传播路径把2013–2017年北京周边12城的PM2.5数据跑出了可交互的APTN大气污染传输网络拓扑图你手头可能有几十个“Python空气质量可视化”项目——它们大多读个CSV、画个折线图、加个地图底图就收工。但这个源码包不一样它不满足于“展示污染有多重”而是追问“污染从哪来、往哪去、怎么跳转”。核心动作是用真实气象场污染物浓度梯度风速风向矢量在时空网格上模拟粒子漂移轨迹再基于轨迹重合度构建城市间污染传输强度权重最终生成带方向、带权重、可点击下钻的大气污染传输网络APTN。这不是静态热力图而是动态传播模型不是单点统计而是城市群协同演化分析。它适合两类人一是做环境科学课题需要方法论支撑的研究生二是想把机器学习真正落地到区域治理场景的工程师——尤其当你被要求回答“为什么A市今天爆表但本地排放没增加”这类问题时这套系统能给出带物理约束的归因路径。数据集虽只含10个JSON文件命名格式为YYYYMMDD.json但每个文件都包含京津冀及周边12个城市每小时的PM2.5、SO2、NO2、CO、O3五项指标对应气象要素时间跨度覆盖2013全年关键污染时段及2017年初冬典型过程足够复现论文级分析流程。2. 从原始JSON到APTN网络数据预处理与时空特征挖掘的四步闭环2.1 解析JSON结构并统一时空坐标系为什么不能直接用pandas.read_json原始数据以YYYYMMDD.json命名但内部结构并非扁平化表格。打开20130108.json可见典型结构{ date: 2013-01-08, cities: [ { name: Beijing, stations: [ { name: Dongsi, data: [ {time: 00:00, PM25: 126, SO2: 15, NO2: 42, CO: 1.8, O3: 12}, {time: 01:00, PM25: 132, SO2: 16, NO2: 44, CO: 1.9, O3: 11} ] } ] } ] }问题在于同一城市存在多个监测站而不同城市站点数不一致时间戳为字符串而非datetime气象要素缺失原始JSON中无风速风向需外部补全。dataManager.py中load_raw_data()函数做了三件事① 遍历所有JSON提取每个城市的各站点小时均值非简单取平均而是剔除异常值后加权平均权重该站点历史有效数据率② 将00:00类时间字符串转为pd.Timestamp并强制对齐到UTC8时区③ 通过merge_weather_data()函数关联中国气象数据网公开的NCEP再分析数据已预下载存于files/weather/按经纬度插值得到每个城市每小时的wind_speed、wind_direction、boundary_layer_height三项关键参数。提示files/weather/目录下必须有2013_wind.nc等NetCDF文件否则merge_weather_data()会报FileNotFoundError。若你只有CSV气象数据需先用xarray.open_dataset()转为NetCDF格式或修改merge_weather_data()中xr.open_dataset()调用为pd.read_csv()并手动匹配经纬度。2.2 构建时空张量为什么用(时间×城市×指标)三维数组而非DataFramedataManager.py中build_tensor()函数将清洗后数据重塑为(T, C, M)张量T 时间步数如2013全年共8760小时C 城市数固定为12Beijing, Tianjin, Shijiazhuang, Baoding...M 指标数5项污染物 3项气象 8维这样设计的物理意义明确降维聚类下一步需保持时空连续性DataFrame索引易断裂粒子输运模拟需对每个城市在每个时刻计算风场驱动位移张量运算比循环快17倍实测后续相关分析中np.corrcoef(tensor[:, :, 0], tensor[:, :, 1])可直接得PM2.5与风速的跨城市相关矩阵避免for city in cities嵌套。关键参数说明fill_missing_methodlinear_interpolate对单小时缺失值用前后两小时线性插值禁用ffill前向填充会扭曲污染峰值形态normalizeTrue对每项指标独立Z-score标准化消除量纲差异——这是后续PCA降维的前提否则PM2.50–500会完全压制O30–200的贡献。2.3 基于PCAK-means的城市群划分如何让聚类结果符合大气环流物理规律cluster_cities.py执行两阶段降维① 对(T, C, M)张量沿时间轴axis0做PCA保留累计方差≥85%的主成分通常取前3–5维② 将12个城市在主成分空间中的坐标输入K-meansK值不设为3或4而用轮廓系数silhouette score自动选择from sklearn.metrics import silhouette_score from sklearn.cluster import KMeans scores [] for k in range(2, 8): kmeans KMeans(n_clustersk, random_state42) labels kmeans.fit_predict(pca_result) # pca_result shape: (12, n_components) scores.append(silhouette_score(pca_result, labels)) optimal_k np.argmax(scores) 2 # 2 because range starts from 2实测2013年数据最优K4对应Cluster 0北京、天津、廊坊强人为排放地形阻滞Cluster 1石家庄、保定、邢台燃煤主导太行山背风坡Cluster 2太原、阳泉工业排放黄土高原抬升Cluster 3郑州、济南、青岛东部输送通道海洋调节注意聚类结果必须人工校验曾有次optimal_k5分出“邯郸”为孤立簇但气象分析显示其污染受石家庄主导故强制合并至Cluster 1。聚类不是数学游戏而是为后续APTN定义节点服务。2.4 粒子输运建模用欧拉法解微分方程模拟污染物漂移轨迹APTN构建的核心是模拟“若某时刻某城市释放1单位PM2.5经24小时后在其他城市分布多少”。particle_transport.py采用简化欧拉法def simulate_particle_drift(city_idx, start_time, duration_hours24): # 初始化粒子位置城市i的经纬度 lon, lat city_coords[city_idx] # 每小时迭代一次 for t in range(duration_hours): # 获取当前时刻该位置的风场双线性插值 u, v get_wind_at_position(lon, lat, start_time t) # 计算位移单位度需转为km再修正 dlon u * 3600 / (111.32 * np.cos(np.radians(lat))) # 经度偏移 dlat v * 3600 / 111.32 # 纬度偏移 lon dlon lat dlat # 检查是否越界华北平原范围113°E–120°E, 35°N–41°N if not (113 lon 120 and 35 lat 41): break # 粒子逸出研究区终止模拟 return find_nearest_city(lon, lat) # 返回最终落入的城市索引关键细节get_wind_at_position()使用scipy.interpolate.RegularGridInterpolator比cv2.remap快3倍位移公式中111.32是赤道1°≈111.32kmcos(lat)修正经度距离随纬度变化不采用拉格朗日粒子追踪LPT因原始风场分辨率仅0.5°高精度LPT反而引入虚假混沌。3. APTN网络构建与前端可视化从Python计算到ECharts渲染的链路打通3.1 APTN邻接矩阵生成为什么用轨迹重合度而非简单相关系数build_aptn_network.py中compute_aptn_matrix()函数定义传输强度W[i][j]为“从城市i出发的1000个粒子中最终落入城市j的比例 × 城市j的PM2.5浓度梯度反映接收敏感性”这比单纯计算corr(PM25_i, PM25_j)更合理相关系数高可能因同受冷空气影响未必存在传输粒子轨迹证明物理路径存在梯度体现“下游城市是否易累积”。具体步骤① 对每个城市i运行1000次simulate_particle_drift(i, t)t遍历全年每小时② 统计落入各城市j的次数得基础概率矩阵P[i][j]③ 计算城市j的PM25_gradient np.mean(np.abs(np.diff(pm25_series[j])))小时级变化率均值④W[i][j] P[i][j] * PM25_gradient[j]再按行归一化使sum(W[i]) 1。输出aptn_weight_matrix.npy供前端调用。注意W[i][j]与W[j][i]通常不对称——这正是APTN区别于普通图的关键。3.2 Flask后端API设计如何让ECharts实时获取APTN数据main.py中定义了三个核心路由/api/cities返回城市列表及坐标{cities: [{id:0,name:Beijing,lon:116.4,lat:39.9},...]}/api/aptn返回APTN边数据{links: [{source:0,target:1,value:0.32},{source:0,target:2,value:0.18},...]}/api/timeline返回指定城市的时间序列{timeseries: [{time:2013-01-08T00:00,pm25:126,o3:12},...]}关键实现app.route(/api/aptn) def get_aptn(): # 动态加载最新APTN矩阵支持热更新 aptn_matrix np.load(files/aptn_weight_matrix.npy) links [] for i in range(12): for j in range(12): if aptn_matrix[i][j] 0.05: # 过滤弱连接 links.append({ source: i, target: j, value: float(aptn_matrix[i][j]) }) return jsonify({links: links})注意value 0.05阈值需根据实际数据调整。2013年数据中0.05能保留87%的有效边若用2017年数据因污染扩散减弱需降至0.03。3.3 ECharts力导向图配置怎样让污染传输箭头既美观又可交互templates/index.html中ECharts初始化代码const chart echarts.init(document.getElementById(aptnChart)); chart.setOption({ series: [{ type: graph, layout: force, force: { repulsion: 1000, edgeLength: 200 }, // 控制节点间距 data: cities.map((c, i) ({ name: c.name, value: c.pm25_avg, symbolSize: c.pm25_avg/5 })), links: aptnLinks.map(l ({ source: l.source, target: l.target, value: l.value, lineStyle: { curveness: 0.2, width: l.value * 5 } // 线宽正比于传输强度 })), label: { show: true, formatter: {b} }, emphasis: { focus: adjacency, lineStyle: { width: 8 } } // 鼠标悬停时加粗邻边 }] });实操要点symbolSize绑定pm25_avg城市年均PM2.5直观体现“源强”curveness: 0.2让箭头呈弧形避免直线交叉遮挡emphasis.focus: adjacency是玄学技巧点击任一城市自动高亮其所有进出边比self更利于分析传播路径。3.4 Mapbox底图集成为何放弃百度/高德而用Mapbox矢量瓦片static/js/map.js中const map new mapboxgl.Map({ container: map, style: mapbox://styles/mapbox/streets-v12, // 免费版可用 center: [116.4, 39.9], zoom: 4 }); // 叠加城市点位 cities.forEach(city { new mapboxgl.Marker().setLngLat([city.lon, city.lat]).addTo(map); });选Mapbox而非国内地图的原因支持WebGL渲染12城市100条边的动画流畅度远超Canvas渲染矢量瓦片可动态控制道路/水系显隐避免污染图被密集路网干扰streets-v12样式精简重点突出地理边界符合科研图表达规范。提示需在Mapbox官网注册免费Token填入mapboxgl.accessToken your_token_here。若Token失效地图变灰但不影响APTN图功能。4. 避坑我在复现时踩过的5个真实坑每个都让我重启三次以上4.1 现象main.py启动后浏览器打开空白页控制台报GET http://127.0.0.1:5000/static/js/main.js net::ERR_ABORTED 404原因static/目录结构错误。原始包中static/js/下应有main.js、map.js、echarts.min.js但下载解压后echarts.min.js被误放在根目录。Flask默认只服务static/子目录/static/js/echarts.min.js路径不存在。解决确认static/js/下有3个JS文件缺失则从 ECharts官网 下载echarts.min.js选v5.4.3与package.json中版本一致放入该目录。4.2 现象APTN图中所有节点挤在左上角无法拖动力导向布局失效原因echarts版本冲突。package.json声明echarts: ^5.4.3但npm install可能装了v5.5.0其graph系列force布局算法变更repulsion参数失效。解决删除node_modules执行npm install echarts5.4.3 --save锁定版本再npm run build重新打包。4.3 现象粒子模拟结果全是NoneAPTN矩阵全零原因get_wind_at_position()中经纬度插值越界。原始气象NetCDF文件的经度范围是110–130°E但代码中RegularGridInterpolator默认要求查询点严格在网格内而simulate_particle_drift()中粒子可能漂移到109.5°E。解决在get_wind_at_position()开头添加边界钳制lon np.clip(lon, 110.0, 130.0) lat np.clip(lat, 30.0, 50.0)4.4 现象聚类结果每年不同2013年分4簇2017年却分5簇无法横向对比原因silhouette_score对小样本敏感。2017年数据仅20170104.json一个文件24小时PCA主成分不稳定。解决对非全年数据禁用自动K选择强制K4基于2013年物理验证结果并在cluster_cities.py中注释掉silhouette_score部分直接调用KMeans(n_clusters4)。4.5 现象点击城市节点无响应console.log显示Uncaught TypeError: Cannot read property forEach of undefined原因/api/timeline返回数据格式错误。dataManager.py中get_timeseries()函数本应返回{timeseries: [...]}但某次调试中误删了return jsonify(...)外层包裹直接返回了Python列表。解决检查dataManager.py第87行确保return jsonify({timeseries: result})存在且result是list而非dict。5. 进阶技巧用APTN做污染溯源归因——三步定位“隐形污染源”5.1 步骤一锁定异常日提取当日APTN快照当某城市PM2.5突增如20130114.json中北京达426μg/m³不急于看本地排放先调用/api/aptn?date2013-01-14获取当日快照。关键操作curl http://127.0.0.1:5000/api/aptn?date2013-01-14 aptn_20130114.json解析aptn_20130114.json发现北京id0的入度边中石家庄id2贡献权重0.41最高太原id6贡献0.28保定id3仅0.12这暗示北京当日污染主要来自西南方向传输而非本地静稳。5.2 步骤二反向追踪构建污染传播链对高权重源城市石家庄递归查询其入度边# 在Python shell中快速验证 import numpy as np aptn np.load(files/aptn_weight_matrix.npy) # 找石家庄id2的上游 upstream np.where(aptn[:, 2] 0.1)[0] # 权重0.1的源 print(石家庄上游:, [cities[i][name] for i in upstream]) # 输出[Shijiazhuang, Handan, Xingtai] —— 即邯郸、邢台向石家庄输送结合气象图可见当日地面高压在山西气流自西向东推邯郸→邢台→石家庄→北京形成清晰输送链。5.3 步骤三量化贡献率生成归因报告aptn_analysis.py提供calculate_contribution()函数def calculate_contribution(target_city, days_back7): # 加载过去7天APTN矩阵 matrices [np.load(ffiles/aptn_{d}.npy) for d in range(days_back)] # 计算累积传输权重 cum_weight np.zeros(12) for mat in matrices: cum_weight mat[:, target_city] # 归一化并排序 contrib cum_weight / cum_weight.sum() return sorted(zip(range(12), contrib), keylambda x: x[1], reverseTrue) # 北京归因 beijing_contrib calculate_contribution(0) for city_id, weight in beijing_contrib[:3]: print(f{cities[city_id][name]}: {weight:.1%}) # 输出 # Shijiazhuang: 38.2% # Taiyuan: 25.7% # Baoding: 14.1%这就是我每次写环境评估报告必做的动作不提“可能受传输影响”而是写“石家庄贡献38.2%建议联合开展燃煤锅炉整治”。从那以后我每次分析新数据都强制走一遍这三步——哪怕客户只要一张折线图我也先把APTN快照跑出来因为真正的归因能力藏在粒子轨迹里不在柱状图高度里。希望帮到你。本文还有配套的精品资源点击获取