多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

城市空气质量数据可视化:Flask + ECharts 完整实战指南

城市空气质量数据可视化:Flask + ECharts 完整实战指南 简介这是一份基于Python、Flask与ECharts构建的城市空气质量数据分析及可视化项目适合Python期末大作业或数据可视化入门学习。项目包含空气质量预报与天气数据可视化两大模块覆盖全国375个城市实时AQI指数、首要污染物及最高/最低气温、紫外线、湿度、能见度、风速等时序变化并通过地图、表格、散点图、折线图、饼图实现多维度交互展示。资源包共10个文件以Python脚本、CSV数据文件、HTML模板、演示录屏和说明文档为主总体积4.83MB。其中3个py文件分别实现数据抓取、预测与Flask视图3个csv提供城市列表与空气质量历史数据webm录屏直观展示页面跳转与交互效果。代码结构清晰包含完整源码和可直接运行的数据文件便于二次开发或改造为课程设计。目前已有1148人学习浏览适合具备基础Python语法、希望快速掌握FlaskECharts集成开发的学习者。1. 城市空气质量大作业Flask ECharts 的完整闭环期末拿到一个“城市空气质量数据分析及可视化”的 Python 大作业最常见的翻车方式是花了一周调图表样式最后却发现数据管道是断的——要么 CSV 里全是脏数据要么 Flask 接口返回不了 JSON要么 ECharts 拿到了数据却渲染不出来。这份资源的价值在于它不是零散的代码片段而是从数据文件、清洗脚本、Flask 后端到 ECharts 前端的一条完整链路能直接跑起来当大作业交也能拆开当脚手架用。适合刚学完 Python 基础、正被 Flask 和 ECharts 联动折磨的本科学生也适合想快速搭一个数据可视化大屏原型的从业者。先说明页面好看是结果数据管道扎实才是核心。2. 先理数据再写网页从 CSV 到 DataFrame 的清洗管道2.1 数据字段怎么定AQI、六项污染物与时间粒度空气质量数据集的字段设计直接决定了后面图表能不能画出来。项目数据文件里的字段一般包含城市名称、监测时间、AQI 指数以及六项污染物浓度PM2.5、PM10、SO2、NO2、CO、O3。其中 AQI 是综合指数用于展示总体趋势六项污染物用于雷达图、饼图和对比柱状图。时间粒度是一个关键取舍。有的数据源是按小时记录的有的按天记录这份资源里的数据文件既有小时粒度也有日粒度处理时我用 pandas 做降采样resample把小时数据聚合成日均值。这里要注意resample 之后索引会变成 DatetimeIndex如果直接存回 CSV 再读出来日期类型会变成字符串接口层处理时要重新解析。字段映射建议做成一个常量字典方便后面接口层引用也方便老师查代码时一眼看懂字段含义FIELD_MAP { city: 城市, time: 监测时间, aqi: AQI指数, pm25: PM2.5浓度, pm10: PM10浓度, so2: SO2浓度, no2: NO2浓度, co: CO浓度, o3: O3浓度 }这段映射表把英文字段名和中文表头对应起来核心作用是让你在清洗、接口、前端三个阶段使用同一套命名不至于在 Flask 返回 JSON 时突然冒出中文 key导致前端取不到数据。我一般还会在末尾加一行assert len(FIELD_MAP) 9防止后续加字段时漏改。2.2 pandas 清洗的几个关键动作原始数据文件不会是干净的我拆这份资源时发现典型的脏数据有三种空值NaN、字符串混入数值列、监测时间格式不统一。清洗时先读文件再逐列处理每一步操作都要有输出检查。import pandas as pd df pd.read_csv(data/air_quality.csv, encodingutf-8) df.columns [FIELD_MAP.get(col, col) for col in df.columns] df[监测时间] pd.to_datetime(df[监测时间], errorscoerce) df df.dropna(subset[监测时间, AQI指数]) num_cols [PM2.5浓度, PM10浓度, SO2浓度, NO2浓度, CO浓度, O3浓度] for col in num_cols: df[col] pd.to_numeric(df[col], errorscoerce) df df[(df[AQI指数] 0) (df[AQI指数] 500)] df df.sort_values(监测时间).reset_index(dropTrue) df.to_csv(data/air_quality_clean.csv, indexFalse, encodingutf-8-sig)这段清洗里比较容易被忽略的是errorscoerce参数。pd.to_datetime遇到无法解析的日期会变成 NaTpd.to_numeric遇到非数值字符串会变成 NaN这样才不会中断主流程但后续dropna会把这些行剔除。我用utf-8-sig写回 CSV是为了让 Excel 打开时中文不乱码实际给 Flask 读取时用普通utf-8就行。AQI 指数范围我做了 0 到 500 的过滤这是国标范围。如果数据里有超过 500 的值说明源数据可能混入了仪器异常记录直接剔除比留着更安全。清洗完成后最好打印一下df.info()确认没有空值和异常类型再继续。2.3 存成接口层要用的结构清洗完的数据不能直接给 Flask 用因为接口层需要的是“按城市聚合”“按时间聚合”的统计结果而不是原始明细表。常见的做法是提前算好几张宽表按城市聚合的均值表、按日期聚合的 AQI 趋势表、六项污染物的平均浓度表。city_avg df.groupby(城市)[AQI指数].mean().sort_values() trend df.groupby(df[监测时间].dt.date)[AQI指数].mean() pollutant_avg df[num_cols].mean().sort_values(ascendingFalse)这三行是后续所有图表的数据基础。注意groupby之后得到的是 Series 而不是 DataFrame接口层序列化时要转回 Python 原生类型。城市聚合结果用于柱状图排名时间趋势用于折线图污染物均值用于饼图或雷达图。我习惯把这三张结果表在启动 Flask 时一次性加载进模块级变量而不是每次请求都重新聚合因为数据文件不大重复计算纯属浪费。这一步做好了后端接口会简单到只剩取值和返回。3. Flask 后端把查询结果变成 JSON 接口3.1 路由设计一个大屏页面 四个数据接口这个项目的后端职责非常明确渲染一个首页模板然后提供四个 JSON 接口给前端图表用。用 Flask 做这件事比 FastAPI 顺手的地方在于——Flask 内建 Jinja2 模板渲染页面接口和数据接口可以在同一个进程里跑不用额外配静态文件服务对大作业体量的项目来说是零学习成本。FastAPI 的异步性能和自动文档很强但这里没有用武之地杀鸡不用牛刀。接口设计遵循“一次请求专供一个图表”的原则这样前端每个图表只关心自己的数据源单独调试时也能在浏览器直接访问接口看 JSON 结构。from flask import Flask, jsonify, render_template app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/api/city_rank) def city_rank(): data {categories: city_avg.index.tolist(), values: city_avg.round(1).tolist()} return jsonify(data) app.route(/api/aqi_trend) def aqi_trend(): data {dates: trend.index.astype(str).tolist(), values: trend.round(1).tolist()} return jsonify(data)/api/city_rank返回城市排名/api/aqi_trend返回时间趋势。这里有一个细节city_avg.index是 pandas Index 对象tolist()之后会转成 Python 列表但round(1)返回的仍是 Series必须再套一次tolist()。很多初学翻车就翻在少了一层转换Flask 的jsonify底层的 JSON 编码器不认识 pandas 的 Series 和 numpy 类型。3.2 从 DataFrame 到 JSON序列化与日期处理最容易被坑的是日期序列化。trend.index是date对象直接jsonify会报Object of type date is not JSON serializable。常见做法是在接口内部统一做字符串转换而不是去改 Flask 全局的 JSON 编码器——全局改动影响范围太大排查问题时不方便。app.route(/api/pollutant_pie) def pollutant_pie(): data [ {name: name, value: float(pollutant_avg[name])} for name in pollutant_avg.index ] return jsonify(data)这里用float()把 numpy.float64 强转成 Python float是防TypeError: Object of type float64 is not JSON serializable的标准动作。另一个隐藏细节是pollutant_avg已经排过序所以饼图数据顺序天然是从高到低。如果你想让前端自己排序也可以不排序直接返回但我一般倾向于后端把数据处理完前端只负责渲染——这样前端代码可以短很多老师检查代码时也更直观。3.3 模板与静态资源的组织方式Flask 项目目录如果不规划清楚接口写好也会乱成一团。推荐结构是app.py放路由templates/放 HTMLstatic/css/放样式static/js/放页面脚本和数据请求。数据文件放data/目录清洗脚本独立成preprocess.py这样大作业答辩时能说清楚“数据处理和后端分离”的设计思路。air-quality-project/ ├── app.py # Flask 主程序含所有路由 ├── preprocess.py # 数据清洗脚本 ├── requirements.txt # 依赖清单 ├── data/ │ ├── air_quality.csv # 原始数据 │ └── air_quality_clean.csv # 清洗后数据 ├── templates/ │ └── index.html # 大屏页面 └── static/ ├── css/style.css └── js/main.js # ECharts 相关逻辑这个目录结构是 Flask 官方推荐的默认约定templates和static这两个目录名是框架写死的。把数据文件和代码分开的最大好处是提交大作业时可以直接说明数据来源和清洗流程不用在代码里翻半天找数据在哪。requirements.txt里我会固定 pandas、Flask 两个核心依赖版本号用而不是避免换机器装不上。4. ECharts 前端把 JSON 变成大屏图表4.1 图表选型折线、柱状、饼图、雷达分到哪个位置大屏页面不是几个图表的简单堆叠每个图表要对应一个分析问题。我在这份资源里看到的图表布局逻辑是这样的左侧放城市 AQI 排名柱状图中间主区域放 AQI 时间趋势折线图右侧放六项污染物占比饼图下方或侧边再放六项污染物的雷达图。柱状图适合排名对比折线图适合趋势观察饼图适合占比结构雷达图适合多维指标对比——选型逻辑要能在答辩时讲出来。ECharts 的图表初始化都是同一个套路echarts.init(dom)setOption(option)。以柱状图为例const chartDom document.getElementById(rankChart); const rankChart echarts.init(chartDom); fetch(/api/city_rank) .then(res res.json()) .then(data { rankChart.setOption({ title: { text: 城市 AQI 年均值排名 }, tooltip: {}, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: data.categories }, yAxis: { type: value, name: AQI }, series: [{ type: bar, data: data.values, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #83bff6 }, { offset: 1, color: #2f8cf7 } ]) } }] }); });这段代码里fetch返回的是 Promise所以setOption必须在.then()里面调用这是一个时序问题——如果写在fetch外面图表会拿到空数据然后一直空白。柱状图我加了渐变色LinearGradient这是让大屏看起来不廉价的常用技巧成本只有几行配置。grid.containLabel: true能防止左侧城市名被截断属于画完必调的参数。4.2 请求与渲染时序Ajax 拿到数据再 setOption饼图、折线图、雷达图的写法都遵循同一套模式。但有一个高频翻车点多个图表同时发请求时后端的响应速度不同页面会出现“图表一个一个蹦出来”的效果。我一般会在页面加载时统一用一个initAllCharts()函数管理并且在每个图表初始化前先清空 DOM 宽度。async function initAllCharts() { const [rankRes, trendRes, pieRes, radarRes] await Promise.all([ fetch(/api/city_rank).then(r r.json()), fetch(/api/aqi_trend).then(r r.json()), fetch(/api/pollutant_pie).then(r r.json()), fetch(/api/radar).then(r r.json()) ]); renderRank(rankRes); renderTrend(trendRes); renderPie(pieRes); renderRadar(radarRes); }用Promise.all并发请求四个接口的等待时间是最大值而不是总和页面整体加载更快。前端渲染函数各自独立接口出错时也只在控制台看到单个请求失败不会整个页面白屏。注意每个渲染函数内部都要做if (data.categories.length 0) return;的空数据保护否则后端返回空列表时 ECharts 会报There is no chart to display。4.3 与 Flask 联调时的前后端约定前后端联调最容易出问题的不是代码逻辑而是字段名对不上。我见过太多案例后端返回categories前端写成了category调试半小时才发现多了一个 s。解决办法是在项目初期就定好接口返回格式的统一约定列表字段统一叫categories和values对象数组统一用{ name, value }结构。饼图用的是{ name, value }对象数组柱状图和折线图用{ categories, values }两个平行数组雷达图用{ indicators, values }。这样约定之后前端每个图表的渲染函数只需要关心三种数据结构不用为每个接口单独写解析逻辑。另一个联调技巧是直接在浏览器地址栏访问/api/city_rank看返回的 JSON 是不是预期的字段和类型这一步能把绝大多数前后端问题定位到具体层。5. 运行与部署避坑从本地起服务到局域网可见5.1 环境依赖与版本问题先把环境跑通再谈功能。我拆资源时会先建一个干净的虚拟环境避免和系统 Python 混淆包版本。pandas 2.x 和 Flask 3.x 之间没有兼容性问题但如果机器上装的是 pandas 1.xgroupby.mean()和resample的 API 基本一致不用担心。容易踩的坑是 numpy 版本过老导致pd.to_numeric在特定字符串格式下解析失败——解决方法就是升级 numpy不要改代码去兼容。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install flask pandas python preprocess.py python app.pypreprocess.py先执行可以确认数据清洗没问题再启动 Flask。如果跳过清洗直接跑app.py接口返回的数据很可能全是 NaN 或者空列表到时候排查起来要同时怀疑数据和代码两个环节非常被动。我一般会先看清洗脚本的输出目录里有没有生成air_quality_clean.csv再决定要不要进下一步。5.2 坑 1图表区域空白控制台报 “There is no chart to display”现象页面能打开、接口能返回 JSON但 ECharts 图表区域一片空白。原因最常见的有两个一个是 ECharts 的 JS 文件没加载成功CDN 被墙或网络慢另一个是放置图表的 div 没有设置高度。ECharts 初始化时如果容器高度为 0图表会静默失败不报错但也不渲染。解决先把 ECharts 换成本地静态文件引入再给每个图表容器显式设置styleheight: 400px。index.html里如果引入的是 CDN 地址要把script srchttps://cdn.../echarts.min.js/script下载到本地static/js/目录然后改成相对路径引用。这样答辩时即使现场网络不好页面也能正常展示。给 div 设最小高度是前端基本功这里单独提是因为 ECharts 对容器高度异常敏感它的 canvas 会直接继承一个 0 高度。5.3 坑 2Flask 报 “Object of type int64 is not JSON serializable”现象浏览器访问接口直接 500终端里报这个错。原因pandas 计算出的均值、聚合结果在转 JSON 时元素类型是 numpy.int64 或 numpy.float64Flask 自带的 JSON 编码器不认这些类型。解决在接口返回值前统一做类型转换int()、float()、str()三件套。# 错误的做法 return jsonify({value: city_avg[0]}) # 正确的做法 return jsonify({value: float(city_avg[0])})这个坑我只遇到一次就记住了因为报错信息特别直白。更隐蔽的情况是city_avg.index.tolist()返回的城市名是 numpy.str_ 类型同样会触发序列化错误。所以我现在写接口的习惯是所有从 pandas 出来的值一律过一遍原生类型转换不抱侥幸心理。5.4 坑 3host0.0.0.0起了服务但室友访问不了现象本地127.0.0.1:5000能访问局域网内另一台电脑输入192.168.x.x:5000打不开。原因Windows 防火墙拦住了 Python 进程的入站请求或者两台设备不在同一个网段。解决先app.run(host0.0.0.0, port5000, debugTrue)确认服务监听所有网卡然后检查防火墙入站规则。Windows 下第一次运行时会弹出防火墙授权窗口如果不小心点了取消之后就得手动去“高级安全 Windows Defender 防火墙”里放行 Python。macOS 和 Linux 一般没有这个问题。另外检查局域网连通最直接的办法是在另一台机器上ping 192.168.x.xping 通再怀疑端口问题ping 不通就先查网段和防火墙。5.5 坑 4数据读出来中文乱码现象Flask 接口返回的中文城市名变成乱码或者前端显示“寮哄害”之类的乱码字符。原因CSV 文件编码不一致read_csv用了错误编码或者响应头缺少charsetutf-8。解决读取时强制指定编码df pd.read_csv(..., encodingutf-8)如果源文件是 GBK 就改encodinggbkFlask 侧用jsonify已经自动处理了 UTF-8所以主要检查数据源。# 读取时尝试两种编码自动降级 try: df pd.read_csv(data/air_quality.csv, encodingutf-8) except UnicodeDecodeError: df pd.read_csv(data/air_quality.csv, encodinggbk)这段try/except是我处理来源不明数据文件时的标准做法。很多公开数据集是 GBK 编码在 Windows 上导出 CSV 尤其常见。如果两种编码都失败就用errorsignore先看一下哪里断了避免直接让整个程序崩溃。6. 扩建与加分技巧把大作业做出彩6.1 加一张累计均值卡片大作业要拿高分光有四个图表还不够加一个数据总览卡片能明显提升完整度。卡片显示“全部城市平均 AQI”“最干净城市”“首要污染物”三块信息前端用一个fetch接口拉回来填进 HTML。app.route(/api/overview) def overview(): clean_city city_avg.idxmin() data { avg_aqi: float(city_avg.mean().round(1)), best_city: str(clean_city), best_aqi: float(city_avg.min().round(1)) } return jsonify(data)idxmin()能直接拿到均值最小的城市名这一步比先排序再取第一个更简洁。前端拿到数据后往三个span里填文本不需要任何图表库。这张卡片放在页面顶部老师打开第一眼就能看到分析结论比把结论藏在图表里更直观。6.2 用 Jinja2 变量动态切换城市如果项目要求分析多个城市而不是只看整体可以用 Jinja2 模板变量实现城市切换。在 Flask 路由里接收查询参数再传给模板app.route(/) def index(): selected request.args.get(city, 北京) return render_template(index.html, selected_cityselected)index.html里用{{ selected_city }}渲染到页面标题前端 JS 在初始化时读这个值拼进接口查询参数。但要注意接口路由也要支持city参数否则前端切换城市后图表数据不会变。这个功能单独做有点繁琐我一般是改一个接口支持城市过滤其他接口保持全量数据减少重复工作量。6.3 验收检查清单答辩前我会按这张清单过一遍几乎能堵住所有翻车点检查项操作预期结果数据清洗运行preprocess.py生成air_quality_clean.csv无报错接口自测浏览器访问/api/city_rank返回合法 JSON中文正常页面加载刷新http://127.0.0.1:5000/四个图表全部渲染无空白边界处理访问/api/aqi_trend日期按升序排列无空值局域网访问手机或另一台电脑访问能打开页面图表可交互这张表其实是我自己的“后悔药”机制因为以前吃过亏答辩当天突然换了一台电脑环境没配好页面白屏只能尬聊。从那以后我每次在交付或者展示前都会强制走一遍这五步验收确认环境和代码都没问题才敢松一口气。至于那些序列化、CDN、防火墙的坑多踩几次就长记性了网上搜来的经验总不如自己亲手修一遍来得实在。希望这份拆解能帮你少走一段弯路大作业顺利过关。本文还有配套的精品资源点击获取
返回列表