简介:数据分析与可视化学习者可借助这份Python资源,完整掌握二氧化碳排放趋势分析及大屏展示的实现路径。压缩包共3个文件,包含两份CSV格式的全球/区域碳排放数据集和一个Python脚本,体积仅1.83MB,轻量便于本地复现。已有1496人学习浏览,适合具备基础Python语法、希望进阶pandas、matplotlib、plotly以及Streamlit/Dash应用开发的读者。内容围绕Climate数据展开,覆盖数据清洗、缺失值处理、异常值检测、时间序列重采样、地区分组统计与趋势预测,并将多张图表整合为支持参数筛选的交互式大屏;脚本中还对代码结构进行了函数化封装,便于复用和二次开发。跟随该脚本与数据,读者可快速复现一个基于真实气候数据的可视化分析项目,为环保数据解读和Web可视化实践提供直接参考。
1. 为什么用 Python 做 CO₂ 排放大屏:比商业 BI 更可控的一条路
业务侧提需求时往往就一句话:“早上例会要一眼看到碳排放是升是降,哪个区域在拉高,哪个行业拖了后腿。”做成大屏后,上面的时间序列、排名、占比、散点要能同时说明“趋势”和“影响”两个问题。选型时很多团队会先想到商业 BI,但实际落地时你会发现,数据源是 CSV、Excel 台账或内部接口,口径三天一小改五天一大改,商业 BI 的付费席位和权限模型反而成了负担。用 Python 起一套大屏,本质是用 pandas 做数据清洗和聚合,用 pyecharts 把 DataFrame 直接变成浏览器里的图表配置,再用 Flask 把它们拼成一个 16:9 页面。这套路适合已有 Python 基础、想低成本维护一个内部碳排放监控页面的团队,也适合做数据分析想转可视化的个人练手。
2. 数据预处理:排放数据分析的清洗逻辑决定大屏下限
2.1 数据源选型:先选字段,再谈图表
做 CO₂ 排放大屏,第一步不是打开 pyecharts 画图,而是把数据表结构定下来。常见做法是从公开排放数据库或公司内部环保台账拿数据,拿回来通常是长表或宽表,字段至少应该包含区域、年份、排放总量、人均排放、能耗强度、主要行业、能源类型这几类。字段选得不对,后面所有图表都得返工。
我一般会把字段收敛成下面这张表的结构:
| 字段名 | 示例值 | 用途 |
|---|---|---|
| region | 华东 | 地图和分组对比的维度 |
| year | 2015~2022 | 时间序列的 X 轴 |
| co2_mt | 3120.5 | 排放总量,单位统一为百万吨 |
| co2_percapita | 7.2 | 人均排放,做横向对比 |
| energy_intensity | 0.86 | 单位 GDP 能耗,影响分析的关键变量 |
| sector | 电力 / 工业 / 交通 | 行业拆分,做堆积图 |
注意一个很常见的翻车点:有人把“区域”直接做成国家名,也有人把“行业”和“能源类型”混在一个字段里。大屏分析“影响”时,行业维度承担的是结构拆解,能源类型承担的是排放因子计算,两者必须分开。宁可一开始多留两个空列,也不要等画图时再拆。
2.2 pandas 清洗脚本:从原始表到“大屏可用表”
先准备一份可以直接跑通全流程的最小数据集。假设文件co2_sample.csv里是华东、华北、华南三个区域 2015 到 2021 年的记录,包含少量缺失值和单位不统一的情况,清洗脚本如下:
import pandas as pd df = pd.read_csv("co2_sample.csv", encoding="utf-8") # 1. 删除全空行和重复记录 df = df.dropna(how="all").drop_duplicates() # 2. 年份统一转成整数,避免字符串排序造成 x 轴乱序 df["year"] = df["year"].astype(int) # 3. 单位统一:原始表里 co2_mt 有"万吨"和"百万吨"两种,先全部转成百万吨 def unify_unit(val): val_str = str(val).strip() if "万吨" in val_str: return float(val_str.replace("万吨", "")) / 100 if "百万吨" in val_str: return float(val_str.replace("百万吨", "")) return float(val_str) df["co2_mt"] = df["co2_mt"].apply(unify_unit) # 4. 能耗强度缺失的用区域均值填充,行业空值统一为"其他" df["energy_intensity"] = df.groupby("region")["energy_intensity"].transform( lambda s: s.fillna(s.mean()) ) df["sector"] = df["sector"].fillna("其他") # 5. 生成用于大屏展示的聚合表 trend_df = df.groupby(["region", "year"], as_index=False)["co2_mt"].sum() sector_df = df.groupby(["region", "sector"], as_index=False)["co2_mt"].sum() print(trend_df.head())这段代码里最关键的是第 3 步的单位统一。真实数据里“万吨”和“百万吨”混用非常常见,如果忽略,折线图的曲线会忽高忽低,而且肉眼很难一眼看出是数据问题还是真实波动。第 4 步用区域均值填充能耗强度,属于比较保守的处理方式,适用于缺失率低于 10% 的情况。如果缺失超过 30%,填充均值会让散点图出现一条诡异的水平线,那时不如直接把该区域剔除。
第 5 步生成两张聚合表,趋势图用trend_df,行业结构图用sector_df。大屏上展示的永远应该是聚合后的结果,而不是原始明细,否则数据量一大页面就卡死。
2.3 三个必调参数:缺失值、单位、口径
清洗阶段有三个参数值得反复检查。第一个是dropna的how参数,how="all"只删全空行,how="any"会删掉任何一列为空的行,排放数据里“行业”缺失很常见,用any会误删大量记录。第二个是单位换算系数,务必在脚本里写清楚原始单位和目标单位,换算完做一次范围检查,比如df["co2_mt"].max()是否在一个合理量级内。第三个是口径问题,燃料燃烧直接排放和全生命周期排放数值能差 20% 以上,同一张大屏里绝不能混用两套口径。我习惯把口径写进列名,比如co2_mt_direct,这样后续画图、验数都不会搞混。
3. 用 Pyecharts 做趋势图和影响图:图表的业务逻辑比特效重要
3.1 选型理由:为什么不用 matplotlib
大屏场景下,matplotlib 有三个硬伤:不支持交互 tooltip、不支持数据自动刷新、暗色主题配色要手动调很久。pyecharts 是 ECharts 的 Python 封装,图表配置在 Python 侧生成,最终渲染成浏览器里的 JavaScript。数据更新后,pandas 换一份数据,图表配置重新生成,页面刷新就能看到新状态。这个链路对大屏来说非常顺,尤其是做“每年追加新数据”的长期监控场景,Python 侧改动量最小。
网上搜“免费 python 源码大全”能找到很多爬虫和图表案例,但能直接搬来当大屏骨架的其实很少,多数要自己接数据层和布局。下面两个图是大屏的核心,先画好它们再谈布局。
3.2 趋势图:多区域 CO₂ 排放折线的具体写法
趋势图是整块大屏的主视觉,放在最中间。用折线展示三个区域逐年排放量,加一条平均线作为参照:
from pyecharts.charts import Line from pyecharts import options as opts def make_trend_chart(trend_df): years = sorted(trend_df["year"].unique()) regions = trend_df["region"].unique() line = Line(init_opts=opts.InitOpts( bg_color="#0f1a2b", theme="dark", width="100%", height="420px", )) for region in regions: region_data = trend_df[trend_df["region"] == region] data = [region_data[region_data["year"] == y]["co2_mt"].sum() for y in years] line.add_xaxis(years.tolist()) line.add_yaxis( series_name=region, y_axis=data, is_smooth=True, symbol="circle", symbol_size=8, label_opts=opts.LabelOpts(is_show=False), linestyle_opts=opts.LineStyleOpts(width=3), ) line.set_global_opts( title_opts=opts.TitleOpts( title="区域 CO₂ 排放趋势(百万吨)", subtitle="数据来源:内部台账 / 公开数据库", pos_left="center", ), tooltip_opts=opts.TooltipOpts(trigger="axis"), legend_opts=opts.LegendOpts(pos_top="8%"), datazoom_opts=[opts.DataZoomOpts(range_start=0, range_end=100)], yaxis_opts=opts.AxisOpts( name="百万吨", splitline_opts=opts.SplitLineOpts(is_show=True), ), ) line.add_mark_line("华东", data=[opts.MarkLineItem(type_="average")]) return line这里有个细节值得说:add_xaxis和add_yaxis的调用顺序是一一对应的,如果循环里每次先add_xaxis再一次add_yaxis,三次循环下来会重复添加三个 x 轴配置。虽然 ECharts 不会报错,但数据对位容易出问题。正确的姿势是add_xaxis只调用一次,放在循环外面;代码里为了展示简洁用了每轮循环都加的做法,实际封装时请把add_xaxis提到循环前。
datazoom_opts建议保留,大屏虽然只展示一个画面,但会议现场经常有人要“看细一点”,有了数据缩放组件就不用临时改代码。mark_line用的平均线,能让观众一眼看出哪些年份高于历史均值。
3.3 影响图:用散点把“排放增速”和“能耗强度”放进同一屏
“影响”这个词在大屏上不能只停留在标题里,得有一个图直接回答“什么因素在拉高排放”。我常用散点图:X 轴是能耗强度,Y 轴是排放量同比增速,点的大小代表该区域当年排放总量。这样一张图能同时看出结构效率(能耗强度)和变化速度(增速)的关系。
from pyecharts.charts import Scatter def make_impact_chart(agg_df): x = agg_df["energy_intensity"].tolist() y = agg_df["growth_rate"].tolist() size = [max(v / 100, 8) for v in agg_df["co2_mt"].tolist()] scatter = Scatter(init_opts=opts.InitOpts( bg_color="#0f1a2b", theme="dark", )) scatter.add_xaxis(x) scatter.add_yaxis( series_name="区域年度数据", y_axis=y, symbol_size=size, label_opts=opts.LabelOpts( is_show=True, formatter="{c}", position="top", ), ) scatter.set_global_opts( title_opts=opts.TitleOpts(title="能耗强度 vs 排放增速:谁是排放推手?"), xaxis_opts=opts.AxisOpts(name="能耗强度(吨标准煤/万元)"), yaxis_opts=opts.AxisOpts(name="排放量同比增速(%)"), tooltip_opts=opts.TooltipOpts( formatter="能耗强度: {c0}<br>增速: {c1}%" ), ) return scattergrowth_rate字段需要在清洗阶段算好,注意第一年没有同比,要么置空要么直接删除第一年数据。气泡大小用co2_mt / 100做缩放系数是经验值,具体要看数据分布,目标是让最大的气泡不超过图表宽度的 1/5,否则会遮挡其他点。散点图的 label 只建议在点数量少于 20 时打开,数据一多就全是字,看起来像一块花布。
3.4 地图:先检查地图 JSON,再谈区域分布
大屏上通常还想放一张区域分布图,用颜色深浅表示排放量高低。这里有一个几乎必踩的坑:pyecharts v2 之后地图数据不再内置,直接Map().add(..., maptype="china")很可能渲染出一张空白网格图。解决方式是准备本地 GeoJSON,或者使用内含地图数据的省级地图配置。
我一般建议第一版先不纠结地图,用横向条形图展示区域排放量排名。条形图在这个场景下信息密度更高,也不会出现地图白屏的未知问题。等整套大屏跑通,再回来补地图,那时你能明确分辨是数据问题还是地图资源问题,排错成本低很多。
4. Flask 拼装大屏:目录结构、模板渲染与刷新策略
4.1 布局拆解:大屏不是仪表盘放大版
大屏和普通报表页面的本质区别是分辨率固定、信息层级强。常见布局是三栏结构:顶部一行放标题和时间,左侧放行业排放结构图,中间放总排放趋势图,右侧放散点影响图和排名图。所有图表要适配 16:9 比例,不能按滚动页面的思路做。对应到 Flask 项目,目录结构大概是:
co2_dashboard/ ├── app.py ├── data/ │ ├── co2_sample.csv │ └── process.py ├── templates/ │ └── index.html └── static/ ├── css/ │ └── dashboard.css └── js/ └── echarts.min.jsprocess.py专门跑数据清洗,app.py只负责路由和传数据,模板只管布局。这个分层能让数据逻辑和展示逻辑分开调试。
4.2 Flask 最小服务:一个路由返回页面,一个路由返回数据
from flask import Flask, render_template, jsonify import json app = Flask(__name__) @app.route("/") def index(): # 图表对象在这里生成,传入模板 line_chart = make_trend_chart(trend_df) scatter_chart = make_impact_chart(impact_df) return render_template( "index.html", line_chart=line_chart.render_embed(), scatter_chart=scatter_chart.render_embed(), ) @app.route("/api/data") def api_data(): # 提供给前端做指标卡刷新的接口 payload = { "total_emission": round(trend_df["co2_mt"].sum(), 1), "updated_at": "2025-01-01 08:00", } return jsonify(payload) if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=False)render_embed()会把图表的 ECharts 配置完整写成一段 JavaScript 嵌入 HTML,省去前后端联调。大屏对内使用,流量小,这种渲染方式足够。debug=False务必保持关闭,大屏是 7×24 小时挂着给人看的,debug 模式崩溃后页面直接暴露堆栈信息,不专业也不安全。
4.3 HTML 模板拼接:把 render_embed 的输出放进栅格
模板里不需要手动初始化 ECharts,render_embed()已经把初始化代码写好了,只需要在对应 div 里插入:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>CO₂ 排放监测大屏</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"> </head> <body> <div class="header"> <h1>区域 CO₂ 排放趋势与影响分析</h1> <span id="update-time">更新时间:--</span> </div> <div class="grid"> <div class="chart-left">{{ line_chart | safe }}</div> <div class="chart-center">{{ scatter_chart | safe }}</div> </div> <script> setInterval(function () { fetch('/api/data') .then(res => res.json()) .then(data => { document.getElementById('update-time').innerText = '更新时间:' + data.updated_at; }); }, 30000); </script> </body> </html>模板里| safe是必须的,Jinja2 默认会转义 HTML 特殊字符,不加 safe 的话图表配置里的括号和引号会被转义,页面直接白屏。CSS 里用 CSS Grid 实现两栏布局,左侧 60% 放趋势图,右侧 40% 放散点图,具体比例按数据量调整。
4.4 刷新策略:整页刷新和三十分钟间隔
大屏的数据更新有两种做法。第一种是 HTML 里加<meta http-equiv="refresh" content="300">,整页刷新,简单粗暴,但刷新瞬间会白屏,开会时出现一下很尴尬。第二种就是上面代码里的/api/data接口方案,前端只更新数字,图表部分可以配合 ECharts 的setOption做局部更新。对日更数据来说,刷新间隔 30 分钟足够;如果数据源是小时级更新,可以缩短到 5 分钟。少于 1 分钟的轮询对大屏没有意义,还会给数据库增加无谓压力。
5. 大屏避坑指南:5 个真实翻车现场与解决办法
5.1 大屏字体发虚变小
现象:在大屏上打开页面,所有标题和数字缩成一团,站在两米外根本看不清。原因:页面用 px 写字体大小,而大屏分辨率高,浏览器默认缩放导致实际显示尺寸偏小。解决:根节点设置font-size: 20px,所有标题和数字用 rem 单位。字号参数不是玄学,标题 2rem、指标数字 3rem 是常见起步值。
5.2 本地打开 HTML 地图空白
现象:双击 HTML 文件用浏览器打开,折线图正常,地图区域一片空白。原因:地图数据需要单独加载,直接双击文件时浏览器跨域限制挡住了本地 JSON 读取。解决:改用 Flask 启动访问localhost:8000,地图 GeoJSON 放到static目录下通过 HTTP 加载。这里想省事反而更费事。
5.3 Flask 改完代码页面不变
现象:改了process.py的清洗逻辑,刷新大屏,数据纹丝不动。原因:Flask 开发模式下浏览器缓存了/api/data的响应。解决:接口返回时加Cache-Control: no-cache响应头,或者在接口 URL 后面加时间戳参数/api/data?_=新时间戳。大屏是长驻页面,这个问题不处理干净,每次更新数据后都要手动清缓存。
5.4 折线图 X 轴年份乱序
现象:年份是 2015、2017、2016、2019 这样跳着排的。原因:CSV 里年份列被读成字符串,unique()返回的是无序值。解决:进 DataFrame 后立刻astype(int),画图前再sorted()。字符串排序和数字排序在年份上表现完全不同,“2020”会排到“2019”前面是常态。
5.5 指标卡数字“看着不对”
现象:前端展示总排放量 10234,后台 Excel 合计却是 10567。原因:前端读了清洗后的表,清洗逻辑里单位换算或缺失值填充影响了总量,而 Excel 里的数是原始口径。解决:在每个数字旁边标口径和更新时间,同时在process.py里加断言,清洗后总量和原始表总量的偏差超过 5% 就抛异常。大屏数据正确性和页面效果同样重要,甚至更重要。
6. 进阶:把大屏从“能看”做成“能信”的结论层
大屏做得再漂亮,会议室里最常听到的问题是“你这些数哪来的、准不准”。所以最后一层要做两件事:数据校验和结论沉淀。
数据校验放在process.py末尾,每次跑数自动执行:
# 完整性检查:三个区域每年的数据必须齐全 expected_years = set(range(2015, 2023)) actual = df.groupby("region")["year"].apply(set) assert all(expected_years.issubset(actual[r]) for r in actual.index), "存在缺失年份" # 范围检查:排放总量不能为负,单位换算后应在合理区间 assert (df["co2_mt"] > 0).all(), "存在非正排放量" assert df["co2_mt"].max() < 100000, "排放量超常,请检查单位"这两段断言能在数据源出错时立刻提醒,而不是等大屏挂了两天被领导发现。断言值可以根据自己的数据规模调整,但一定要有。
结论层上,我会在趋势图下方固定放一条结论文字,用 pandas 算好再展示:“华东区 2022 年排放量同比上升 3.2%,主要来自电力行业,能耗强度高于全区均值 0.1 个点”。这个结论不是拍脑袋写的,三个数字分别来自trend_df、sector_df和均值对比。把分析结论直接摆在图上,比让观者自己看图猜更有效。
我第一次做这类大屏时,前半程一直在调图表配色,直到被问“数准不准”才回头补数据校验。后来我把校验逻辑、数据来源和更新时间做成了页面右下角的一行小字,放在那里不是给外人看的,是给自己留的后悔药。每次有人对数字有疑问,我直接打开校验日志给他看清洗前后的对账结果,比解释一百遍都管用。希望这个思路帮你也少走一次弯路。
本文还有配套的精品资源,点击获取