
简介本资源面向环境科学、数据挖掘与机器学习方向的学习者与研究人员提供一套基于Python的空气质量数据可视化分析系统源码及配套数据可用于城市群划分、污染传输网络构建与污染传播过程探索等课题实践。项目采用BS架构前端整合HTML、CSS、JavaScript及D3、ECharts、Mapbox等可视化库后端基于Python与Flask搭建main.py为服务入口dataManager.py负责数据模块files目录存放原始数据与挖掘结果templates中的index.html为前端入口。压缩包共2000个文件以1295个json数据文件和697个py脚本为主另含少量txt、xml与md说明文件整体约76.43MB结构清晰便于按模块查阅。目前已有252人学习下载适合希望掌握降维聚类、粒子输运与相关分析、多维空间变换渐进式探索等方法并需要完整可运行项目参考的读者。1. 从一堆 2013 年的 JSON 说起这套空气质量分析系统到底能跑出什么手里拿到这份源码的时候我先看的不是代码是files文件夹里那串文件名20130108.json、20131225.json、20131223.json、20130114.json、20130117.json、20131220.json、20131226.json、20170104.json、20131222.json、20130106.json。清一色按日期命名跨度从 2013 年初到 2017 年初典型的逐日空气质量监测快照。这套 Python 基于数据挖掘与机器学习的空气质量数据可视化分析系统干的事就是把这些散落的日度 JSON 吃进去做降维聚类划城市群、建大气污染传输网络、再用渐进式探索分析把污染传播过程摊开给人看。前端 BS 架构HTML/CSS/JavaScript 打底D3、ECharts、Mapbox 三件套负责图形后端 Flask 扛服务main.py一跑就能起。适合谁做环境数据方向的数据分析、想找一个能直接改的 Flask 可视化全栈练手项目、或者课程设计要交空气质量主题的人。它不是一个玩具 demodataManager.py是真正的数据模块其余 py 文件是课题过程的测试脚本读一遍能看清一条从原始 JSON 到可视化结论的完整链路。2. 数据层拆解JSON 怎么进、降维聚类怎么划城市群2.1 先认清 files 里的数据形态与字段files文件夹分两块原始数据和数据挖掘后的相关数据。原始那批就是上面那串日期 JSON按天存每条记录对应一个监测点或城市的空气质量指标。常见做法是每个 JSON 里放一个数组元素含城市名、AQI、PM2.5、PM10、SO2、NO2、CO、O3 以及经纬度。你要做的第一件事不是写模型是把这些字段对齐——不同来源的 JSON 字段名可能一个叫pm25一个叫PM2.5不统一后面全乱。我一般先写个探查脚本把结构和缺失率打出来import json, glob, os from collections import Counter files glob.glob(files/*.json) field_counter Counter() record_count 0 for fp in files: with open(fp, r, encodingutf-8) as f: data json.load(f) # 兼容两种常见结构顶层是 list或包在 data 键里 rows data if isinstance(data, list) else data.get(data, []) record_count len(rows) for row in rows: field_counter.update(row.keys()) print(文件数:, len(files), 总记录:, record_count) for k, v in field_counter.most_common(): print(f{k}: 出现 {v} 次)逻辑说明glob把files下所有 JSON 捞出来逐个读isinstance判断顶层结构避免有的文件是{data: [...]}有的直接是[...]导致for row in data报错。参数上encodingutf-8必须显式写Windows 默认 GBK 读中文城市名会炸。field_counter统计字段出现频次频次明显低于总记录数的字段就是缺失重灾区后面要么补要么丢。2.2 降维聚类划城市群的实现路径摘要里说的「基于降维聚类的城市群划分」落到代码就是先标准化再降维再聚类。空气质量各指标量纲差得远PM2.5 动辄上百CO 常在个位数不标准化聚类会被大数值指标带偏。常见做法是StandardScaler标准化后用 PCA 降到 2~3 维再上 KMeans。为什么先降维一是高维下距离度量会失效维度灾难二是降到 2 维正好能画散点图给前端。下面这段是可直接抄的骨架import numpy as np from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.cluster import KMeans # X: shape (n_samples, n_features)每行一个城市某时段的指标均值 scaler StandardScaler() X_std scaler.fit_transform(X) pca PCA(n_components0.9) # 保留 90% 方差自动定维数 X_pca pca.fit_transform(X_std) print(保留主成分数:, X_pca.shape[1], 累计方差:, pca.explained_variance_ratio_.sum()) kmeans KMeans(n_clusters4, random_state42, n_init10) labels kmeans.fit_predict(X_pca)逻辑说明n_components0.9让 PCA 自己决定留几个主成分比硬写 2 更稳因为不同时段数据方差分布不一样。n_init10是 KMeans 多次初始化取最优sklearn 新版默认值变过显式写死避免版本差异导致结果飘。n_clusters4不是拍脑袋得用肘部法或轮廓系数验证下面给个快速验证from sklearn.metrics import silhouette_score for k in range(2, 8): km KMeans(n_clustersk, random_state42, n_init10).fit(X_pca) print(k, round(silhouette_score(X_pca, km.labels_), 3))轮廓系数越接近 1 越好一般挑拐点。参数说明random_state固定住保证复现silhouette_score在样本量大时计算慢可以先抽样。这一步跑完每个城市有了群标签前端 Mapbox 上按群着色城市群划分就出来了。2.3 把聚类结果落回 files 供前端消费聚类完不能只在内存里得写回文件让 Flask 读。我一般存成 JSON键是城市值是群号和降维坐标import json result {} for city, label, coord in zip(cities, labels, X_pca): result[city] {cluster: int(label), x: float(coord[0]), y: float(coord[1])} with open(files/cluster_result.json, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2)ensure_asciiFalse保证中文城市名不被转义成\uXXXX前端直接可读。indent2方便你肉眼查。这一步做完dataManager.py里加个读取函数就能供接口调用。3. Flask 后端与可视化前端怎么接起来3.1 main.py 起服务与 dataManager.py 的职责边界main.py是入口跑起来 Flask 服务。dataManager.py是后端数据模块负责读files里的原始和挖掘后数据、做必要的聚合再吐给路由。这个分工要守住数据加工全在dataManager.py路由只做参数解析和返回别把聚类逻辑塞进路由函数否则接口一多就成黑匣子。典型路由长这样from flask import Flask, jsonify, request import dataManager app Flask(__name__) app.route(/api/cluster) def cluster(): return jsonify(dataManager.get_cluster_result()) app.route(/api/series) def series(): city request.args.get(city) if not city: return jsonify({error: city required}), 400 return jsonify(dataManager.get_city_series(city)) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)逻辑说明jsonify自动设Content-Type: application/json前端fetch直接.json()。request.args.get取查询参数缺参数返回 400 而不是空数据前端好判断。host0.0.0.0让同局域网其他机器能访问只本机调试可改127.0.0.1。debugTrue开发期热重载上线务必关掉否则有安全风险。3.2 ECharts 与 Mapbox 各管什么图前端三件套分工明确ECharts 画折线、柱状、散点这类统计图比如某城市 PM2.5 随时间变化Mapbox 画地理分布城市按经纬度打点、按群着色D3 一般用在需要高度自定义的图形比如污染传输网络那种带连线的力导向图。ECharts 接后端数据的最小写法fetch(/api/series?city北京) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(trend)); chart.setOption({ xAxis: { type: category, data: data.dates }, yAxis: { type: value, name: PM2.5 }, series: [{ type: line, data: data.values, smooth: true }] }); });逻辑说明echarts.init绑定容器容器必须有明确宽高否则图不显示——这是新手最常翻的车。smooth: true让折线平滑数据点少时更耐看。Mapbox 那边需要 tokenindex.html里初始化地图时填自己的别用示例里过期的。3.3 大气污染传输网络APTN的数据组织摘要提到的粒子输运和相关分析构建 APTN本质是把城市当节点、城市间污染的相关性或传输强度当边。常见做法是算两两城市 PM2.5 序列的皮尔逊相关系数超过阈值就连边import pandas as pd df pd.DataFrame(city_series) # 列是城市行是时间 corr df.corr(methodpearson) edges [] threshold 0.6 for i in corr.columns: for j in corr.columns: if i j and abs(corr.loc[i, j]) threshold: edges.append({source: i, target: j, value: round(corr.loc[i, j], 3)})参数说明threshold0.6是经验值太低网络糊成一团太高只剩孤点得按数据调。i j避免重复边和无向图自环。结果edges直接喂给 D3 力导向图或 ECharts graph 系列。注意相关系数高不等于因果传输这里只是统计关联别在结论里写死「A 污染传给了 B」。4. 避坑与排查这套源码跑不起来时先看这几条4.1 现象main.py 一跑就报 ModuleNotFoundError原因Flask、sklearn、pandas、numpy 这些依赖没装或者装到了别的 Python 环境。解决先确认当前解释器再按需装。python -c import sys; print(sys.executable)看路径然后pip install flask scikit-learn pandas numpy。如果用了虚拟环境确保 IDE 里选的解释器和命令行一致vscode 右下角切换解释器这一步很多人漏。4.2 现象前端页面空白控制台报 ECharts is not defined原因index.html里 ECharts 的 script 标签路径错或者 CDN 被拦。解决检查static下有没有本地 echarts.min.js有就引本地别依赖外网。容器没宽高也会白屏给#trend加width:100%;height:400px。4.3 现象JSON 读取报 UnicodeDecodeError原因文件不是 UTF-8或含 BOM。解决读的时候试encodingutf-8-sig能吃掉 BOM。批量读时加 try 捕获把出错文件名打出来单独处理别让一个坏文件卡死整批。4.4 现象聚类结果每次跑都不一样原因KMeans 随机初始化没固定。解决random_state写死n_init显式设。PCA 本身确定性问题基本都在 KMeans。另外数据顺序变了结果也可能微变排序后再喂。4.5 现象Mapbox 地图不显示、报 401原因token 无效或没配。解决去 Mapbox 账号拿自己的 public token 填进初始化代码别用仓库里可能过期的。国内访问 Mapbox 瓦片偶尔慢可换成本地静态底图先跑通逻辑。5. 进阶把渐进式探索分析做成可交互的筛选链路前面跑通的是静态链路真正让这套系统有价值的是「渐进式探索」——用户点一个城市群图跟着变拖时间轴污染传输网络随时间重算。落地做法是把后端接口参数化前端用状态驱动。比如给/api/series加时间范围参数app.route(/api/series) def series(): city request.args.get(city) start request.args.get(start, 2013-01-01) end request.args.get(end, 2017-12-31) return jsonify(dataManager.get_city_series(city, start, end))dataManager里按日期过滤再返回前端时间轴change事件里重新fetch并setOption。这里有个性能坑每次拖动都请求会卡常见做法是加 200ms 防抖或者后端把常用区间预聚合缓存起来。验证方法很直接——打开浏览器 Network 面板看拖动时请求频率和响应时间超过 300ms 就得优化。再进一步是污染传输网络随时间的演化。把每个时间窗口的edges算好存成files/aptn_2013.json这种按年或按月的文件前端切换时间就换数据源避免实时算相关系数拖慢响应。我一般会写个批处理脚本一次性生成所有窗口for year in range(2013, 2018): sub df[df.index.year year] corr sub.corr() edges build_edges(corr, threshold0.6) with open(ffiles/aptn_{year}.json, w, encodingutf-8) as f: json.dump(edges, f, ensure_asciiFalse)参数上threshold可以按年份微调因为不同年份数据完整度不同固定阈值可能某年边特别少。验证时把每年的边数打出来数量级差太多就说明阈值要动。最后说个我踩过的坑这套系统里dataManager.py和那些测试 py 文件容易职责混淆改着改着数据逻辑散得到处都是。从那以后我每次接手这类项目都强制先把dataManager.py的对外函数列一张表——输入什么、输出什么、被哪些路由调用——再动任何一行代码。这张表不用写进文档自己心里有数就行能省掉大量「改了这里崩那里」的后悔药。希望帮到你。本文还有配套的精品资源点击获取