简介:面向想用Python实现碳排放数据可视化大屏的学习者与开发者,这份资源覆盖数据分析与Web可视化从入门到进阶的常见路径。资源围绕二氧化碳排放趋势与影响分析,整合了pandas数据处理、matplotlib/seaborn静态图表、plotly交互图表以及Streamlit大屏搭建等关键环节,可帮助读者理解从数据清洗、时间序列分析到多图联动的完整流程。压缩包共3个文件,包含两个CSV数据文件和一个Python脚本,体积约1.83MB,数据文件可支撑按年份、地区等维度探索全球CO2排放情况,脚本则直接演示大屏应用的组织方式。已有1494人学习/下载,说明该项目有较好的参考价值。通过拆解这份资源,读者可以掌握面向真实数据集的可视化大屏开发思路,并学到如何用少量代码快速呈现数据分析结论,适合用于课程设计、研究报告或个人作品集。
1. 从一行 CSV 到全屏可视化大屏,中间只隔了这几个 Python 库
很多人拿到owid-co2-data.csv这类全球碳排放数据集,第一步就是read_csv之后plot()画条折线,然后发现图丑、没法交互、也没法跟同事演示。真正的工业级做法,是把数据处理、趋势分析和可视化大屏拆成一条流水线:先用 pandas 把数据清洗到可分析的状态,再用 matplotlib/seaborn 做静态探索,最后用 Streamlit 把多个图表组装成一个支持下拉筛选、日期范围选择的大屏页面。这不是套 UI 模板,而是每一层都有明确的职责边界。这篇文章要拆解的,就是Climate.zip里那份典型的 Carbon Dioxide 数据集配合 Streamlit 构建大屏的完整流程,适合正在做数据分析可视化项目、以及准备把分析结果交付成可演示产品的 Python 开发者。你会看到我实际写过的数据清洗逻辑、趋势回归代码、大屏组件布局和那些文档里没写清楚的坑。
2. 先别急着画图:二氧化碳数据集的清洗与重采样策略
任何可视化大屏的可信度都建立在数据质量上,而owid-co2-data.csv这种来自 Our World in Data 的原始文件,通常包含 200 多个国家、从 1750 年到 2024 年的逐行记录,列里有 CO2 排放总量、人均排放、GDP、人口、能源消费等多个维度。直接读进 pandas 就开始画图,你会被缺失值、负数和不同粒度的时间戳折磨到怀疑人生。我建议的第一步,是明确一个「分析基线」:你关注的是全球趋势、国家对比、还是人均维度?这决定了后续groupby()的键和重采样的频率。
2.1 数据加载与列裁剪的实用模式
import pandas as pd import numpy as np df = pd.read_csv('owid-co2-data.csv') print(df.shape) print(df.columns.tolist()) print(df['year'].min(), df['year'].max())这份数据的特点是宽表结构,每行是一个国家在某年的观测记录。直接全量载入没问题,但为了减少内存和后续操作的干扰,我一般只保留跟分析相关的列:country、year、co2、co2_per_capita、population、gdp。其他诸如cement_co2、flaring_co2这类细分列,除非你要做排放源拆解,否则可以先不加载。
cols = ['country', 'year', 'co2', 'co2_per_capita', 'population', 'gdp'] df = df[cols].copy()参数说明:这里用copy()是为了避免后续SettingWithCopyWarning,这是 pandas 里最常见的坑之一。如果你不 copy,后面做df['co2'] = df['co2'].fillna(0)可能会直接写进原始 DataFrame 的视图里,导致告警甚至数据污染。
2.2 缺失值处理不能一刀切
二氧化碳数据里,co2列对很多早期年份或小国是 NaN,但 NaN 的含义分两种:一种是「没有数据」,一种是「数值为 0」。在碳排放语境下,1750 年的国家排放量大概率是 0,而不是缺失。所以处理策略应该区分:
# 对于co2列,pre-1850年缺失填充为0(当时工业排放几乎为0) mask = (df['year'] < 1850) & (df['co2'].isna()) df.loc[mask, 'co2'] = 0 # 对于人均列,如果co2为0则人均也为0 mask_per = (df['co2'] == 0) & (df['co2_per_capita'].isna()) df.loc[mask_per, 'co2_per_capita'] = 0 # 其余仍缺失的co2填充为前向观测值(按国家分组) df['co2'] = df.groupby('country')['co2'].ffill()逻辑说明:ffill()是按国家分组后,用该国最近一年的有效值填充后续缺失,这比全局均值填充更符合时间序列特征——一个国家的排放量不会突然从 100 变成 0 再跳回 100。注意顺序:先填充历史零值,再做前向填充。如果反过来,1850 年前的缺失也会被后来的工业排放值污染。
处理完后,建议做一次数据健康度检查:
print("剩余缺失值:", df[['co2', 'co2_per_capita']].isna().sum()) print("存在负值:", (df[['co2', 'co2_per_capita']] < 0).sum())存在负值的话,通常是因为数据源里有调整后的负排放(土地利用变化导致),但在大屏上展示「碳排放总量」时负值会破坏巴图表的可读性,我一般直接置为 0 并标注「数据已修正」。
2.3 按年和按国家重采样,构造大屏的三大数据集
可视化大屏一般至少需要三层视图:全球总趋势、主要国家对比、国家详情页。这三个视图对应三个不同粒度的 DataFrame:
# 全球年度排放总量 global_yearly = df.groupby('year')['co2'].sum().reset_index() # 各国最新年度排放量(用于柱状图排名) latest_year = df['year'].max() countries_latest = df[df['year'] == latest_year].sort_values('co2', ascending=False).head(15) # 重点国家每年排放(用于多线对比) key_countries = ['China', 'United States', 'India', 'Russia', 'Germany'] selected_countries = df[df['country'].isin(key_countries) & (df['year'] >= 1960)]参数说明:groupby('year')['co2'].sum()会把所有国家同一年数据加总成全球值,注意此时你不需要担心国家名重复,因为sum()天然处理了多行累加。如果你想做区域聚合,比如把欧盟若干国合成一个区域,就需要在 groupby 键里增加「区域映射」列。常见做法是提前建一个 country -> region 的字典,用df['region'].map(region_map)生成新列再聚合。
这里重采样的关键点在于:数据集原始粒度是「国家-年」,如果你要做 1960—2024 年的趋势分析,不需要再按月重采样,年度粒度已经足够;但如果你遇到的是月度数据或日度数据,就务必用pd.to_datetime先转换,然后set_index('date').resample('YE').sum(),其中'YE'是 pandas 2.x 的新写法,旧版本用'Y'或'A'。
3. 趋势分析不只是画折线:用滑动平均和线性回归量化排放增速
大屏的核心信息是「排放趋势在变好还是变坏」,但原始折线图噪声很大,尤其在 1950 年前的区间,数值低、波动不明显。仅仅把df.plot()的结果丢到大屏上是不够的,你需要叠加滑动平均、增长率曲线,甚至一段简单的回归预测来支撑结论。这一部分我通常用 pandas + numpy 完成计算,再用 matplotlib 渲染成静态图,最后嵌入 Streamlit。
3.1 滑动平均消除短期波动
global_yearly['co2_ma'] = global_yearly['co2'].rolling(window=10, center=True).mean() global_yearly['co2_pct_change'] = global_yearly['co2'].pct_change() * 100rolling(window=10, center=True)的含义是取前后各 10 年的平均,center=True让滑动窗口对齐到当前年份,这样曲线不会出现滞后偏移。pct_change()计算的是同比增长率,单位是百分比,注意第一行结果是 NaN,需要dropna()再展示。
用这个数据,你可以回答「近十年全球排放增速是上升还是下降」这类问题。比如 2000 年附近增长率接近 3%,而 2015 年后维持在 1% 左右,这些具体数字放到大屏的指标卡片上,比一张曲线图更有说服力。
3.2 分段线性回归寻找趋势拐点
全球排放趋势并不是一条直线,1950 年以前和 2000 年以后斜率完全不同。如果你想给大屏加一个「趋势是否放缓」的结论,我建议用分段线性回归,而不是全局回归:
from scipy import stats def segment_slope(df, start, end): seg = df[(df['year'] >= start) & (df['year'] <= end)].copy() slope, intercept, r_value, p_value, std_err = stats.linregress(seg['year'], seg['co2']) return slope, p_value slope_1950_2000 = segment_slope(global_yearly, 1950, 2000) slope_2000_2024 = segment_slope(global_yearly, 2000, 2024) print(f"1950-2000年斜率: {slope_1950_2000[0]:.2f} 百万吨/年") print(f"2000-2024年斜率: {slope_2000_2024[0]:.2f} 百万吨/年")这段代码输出的斜率对比,可以直接写成大屏上的结论性文字:「2000 年后年均增长约为 1950—2000 年的 0.8 倍」。这里stats.linregress返回的p_value可以用来做显著性检验,如果p_value > 0.05,说明该时间段内的线性趋势不显著,展示时就要谨慎下结论。
用 numpy 做同样计算的替代写法是:
import numpy as np x = seg['year'].values y = seg['co2'].values slope = np.polyfit(x, y, 1)[0]np.polyfit返回的是多项式的系数,[0]取的就是斜率。和linregress的差距在于没有直接给出 p 值,但如果你熟悉统计细节,也可以用np.corrcoef计算相关系数。大屏上需要展示「斜率」这种单一数值时,我倾向于用linregress,因为它连置信区间都给了。
3.3 用热力图和回归结果选择可视化维度
做完趋势判断后,下一步就要决定大屏上放哪些图表。我的默认配置是:
- 全球排放总量面积图,叠加 10 年滑动平均线
- 主要国家碳排放柱状图(最新年份 Top 15)
- 重点国家历年排放多折线图(1960 年以后)
- 人均排放 vs GDP 的散点图(用于展示排放效率)
其中散点图最容易暴露数据问题。co2_per_capita和gdp里如果有异常值,比如某个小岛国 GDP 数据缺失导致人均值被错误放大,散点就会飞出去。所以做散点前,建议把gdp列为空的记录直接过滤掉,或者用df.dropna(subset=['co2_per_capita', 'gdp']),不要用fillna(0),否则 0 值会堆积在原点附近,干扰视觉。
scatter_df = df[df['year'] == latest_year].dropna(subset=['co2_per_capita', 'gdp']) scatter_df = scatter_df[scatter_df['population'] > 1_000_000] # 过滤人口小于100万的国家过滤条件写成population > 1_000_000是为了避免微型国家的人均数值波动过大。这个参数可以调整,如果你想看全球所有国家和地区,就不加这个条件,但大屏上的样本点会多出几十个噪音。
4. Streamlit 布局实战:把静态图组装成可筛选的大屏页面
数据分析部分产出的是干净的 DataFrame 和 matplotlib 图,接下来要用 Streamlit 把它们组装成 Web 大屏。Streamlit 的优势在于它的执行模型简单:每次页面交互都会从上到下重新执行整个脚本,因此你只需要用st.sidebar定义筛选组件,然后在主区域根据筛选结果重新绘图即可。相比 Dash,Streamlit 不需要显式注册回调,代码量少一半,非常适合快速交付。
4.1 页面配置与多列布局
import streamlit as st import matplotlib.pyplot as plt import matplotlib matplotlib.use('Agg') matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans'] matplotlib.rcParams['axes.unicode_minus'] = False st.set_page_config(page_title="全球二氧化碳排放趋势大屏", layout="wide", initial_sidebar_state="expanded") st.title("🌍 全球二氧化碳排放趋势与影响分析") # 顶部指标卡片 col1, col2, col3, col4 = st.columns(4) with col1: st.metric("全球累计排放量(亿吨)", f"{global_yearly['co2'].sum() / 1e4:.0f}") with col2: st.metric("最新年份", latest_year) with col3: st.metric("最大排放国", countries_latest.iloc[0]['country']) with col4: st.metric("全球同比增速", f"{global_yearly['co2_pct_change'].iloc[-1]:.1f}%")layout="wide"是关键配置,它让页面占满整个浏览器宽度,否则默认窄屏下大屏效果大打折扣。st.metric是 Streamlit 内置的指标卡组件,直接显示数值和可选的前置 delta 变化量。如果你想把「同比增速」做成红色/绿色箭头,可以用delta=参数,但要注意delta是绝对值差还是百分比差,容易搞混。
字体配置里matplotlib.rcParams['font.sans-serif']设了SimHei,这是 Windows 下常见黑体。如果你部署到 Linux 服务器,就需要改成系统里实际存在的中文字体,比如'Noto Sans CJK SC'。否则 matplotlib 会抛警告,中文标签显示为方块。axes.unicode_minus=False是为了让负号正常显示。
4.2 侧边栏筛选组件与数据联动
大屏不能只是静态图,用户需要能切换年份区间和关注国家。Streamlit 的st.sidebar天然适合放这类控件:
st.sidebar.header("筛选条件") # 年份范围滑块 year_range = st.sidebar.slider("分析年份范围", min_value=int(df['year'].min()), max_value=int(df['year'].max()), value=(1960, int(df['year'].max()))) # 国家多选 all_countries = sorted(df['country'].unique()) selected_countries = st.sidebar.multiselect("选择对比国家", all_countries, default=key_countries) # 图表类型 chart_type = st.sidebar.radio("图表类型", ("面积图", "折线图", "柱状图"))silder返回的是元组(start_year, end_year),你在绘图前必须用它过滤 DataFrame:
filtered_yearly = global_yearly[(global_yearly['year'] >= year_range[0]) & (global_yearly['year'] <= year_range[1])]注意坑点:year_range组件里的值是 Python int,而 DataFrame 的year列可能是 int64,两者比较没问题。但如果你筛选国家,multiselect返回的是 list,st.sidebar的默认值必须是 list,不能是字符串,否则页面加载会报错。
4.3 生成并嵌入 matplotlib 图表
Streamlit 嵌入 matplotlib 图的推荐方式是用st.pyplot(fig),在脚本里先创建plt.subplots()或plt.figure(),绘制完直接传入。我先写一个函数封装绘图逻辑:
def plot_global_trend(df, chart_type='area'): fig, ax = plt.subplots(figsize=(12, 5)) if chart_type == 'area': ax.fill_between(df['year'], df['co2'] / 1e3, alpha=0.4, label='CO2排放总量') ax.plot(df['year'], df['co2_ma'] / 1e3, color='red', linewidth=2, label='10年滑动平均') elif chart_type == 'line': ax.plot(df['year'], df['co2'] / 1e3, label='CO2排放总量') else: ax.bar(df['year'], df['co2'] / 1e3, width=0.8) ax.set_xlabel("年份") ax.set_ylabel("排放量(十亿吨 co2)") ax.legend() ax.grid(True, alpha=0.3) return fig st.subheader("全球二氧化碳排放趋势") fig1 = plot_global_trend(filtered_yearly, chart_type) st.pyplot(fig1)fill_between的面积图比普通折线更醒目,适合大屏顶部的总览区域。/ 1e3是把百万吨换算成十亿吨的单位转换,这里只是展示层的单位处理,不应该改变原始 DataFrame 的数据。如果你用 plotly 生成交互式图表,可以直接传给st.plotly_chart,但 Streamlit 社区版对交互次数有限制,所以我个人在大屏上偏好 matplotlib 静态图 + 侧边栏重绘,性能和稳定性更可控。
4.4 多图表网格排布
大屏要显得信息密度高,需要用st.columns做网格排布。我习惯两行两列的布局:
row1_col1, row1_col2 = st.columns(2) with row1_col1: st.subheader("最新年份排放 Top 10 国家") fig2, ax = plt.subplots(figsize=(8, 4)) top10 = countries_latest.head(10) ax.barh(top10['country'][::-1], top10['co2'][::-1] / 1e3) ax.set_xlabel("十亿吨 CO2") st.pyplot(fig2) with row1_col2: st.subheader("重点国家时间序列对比") if selected_countries: comp_df = df[df['country'].isin(selected_countries)] fig3, ax = plt.subplots(figsize=(8, 4)) for c in selected_countries: cdf = comp_df[comp_df['country'] == c] ax.plot(cdf['year'], cdf['co2'], label=c) ax.legend(loc='upper left') st.pyplot(fig3)条形图用barh即水平条形图,[::-1]反转顺序是为了让最大的国家显示在最上方,符合阅读习惯。国家多选如果选了超过 8 个,折线图会非常拥挤,我一般会在代码里加限制:
if len(selected_countries) > 8: st.warning("最多选择 8 个国家,当前已截断") selected_countries = selected_countries[:8]注意st.warning只是提示,不会中断执行,这样用户看到的是截断后的图形,同时知道原因,体验比直接报错好得多。
5. 从本地大屏到可交付:性能优化、缓存与部署避坑
这节解决最后一个实际问题:当数据量变大、筛选变多时,Streamlit 每次交互都重新执行整个脚本,导致大屏卡顿,以及部署到 Linux 后中文乱码、字体缺失等环境问题。我总结三个最实用的技巧。
5.1 用@st.cache_data缓存读数和计算
Streamlit 默认每次交互重跑全部代码,而pd.read_csv一个几百 MB 的 CSV 会耗时数秒。解决方案就是加缓存装饰器:
import streamlit as st @st.cache_data(show_spinner=False) def load_data(path): df = pd.read_csv(path) # 这里放预处理逻辑 return df df = load_data('owid-co2-data.csv')@st.cache_data会基于函数名和参数自动缓存结果。注意它只适合返回可哈希参数的数据,如果你的load_data参数里有 DataFrame,就会报错。我将数据预处理逻辑全放进load_data里,这样只要 CSV 文件不变,后续交互都不会重新读取和清洗。show_spinner=False是为了避免每次重跑都显示加载转圈动画。
这里有个记忆点:@st.cache_data是 Streamlit 1.18 之后的写法,旧版是@st.cache,后者在新版本已经废弃。如果你看到旧代码,直接替换成前者即可。
5.2 过滤条件变化时才重绘
除了缓存数据,还可以用st.session_state记录上次的筛选值,避免重复绘图。但更简洁的方式是把筛选组件放在st.form里,用户点击「应用」按钮才触发重绘:
with st.sidebar.form("filter_form"): year_range = st.slider("年份", 1960, 2024, (1960, 2024)) selected_countries = st.multiselect("国家", all_countries, default=key_countries) submitted = st.form_submit_button("应用筛选")这种模式下,只有点击按钮,代码才往下执行,避免了滑块拖动时页面每动一格就刷新一次的卡顿。大屏演示时这个交互模式更可控。缺点是用户需要多一次点击,但换来的是流畅度。
5.3 部署后的中文字体与资源路径问题
本地运行streamlit run streamlit_co2_vis.py没问题,但部署到云服务器时常见两个坑:
第一,matplotlib 无法显示中文。你需要手动安装中文字体并注册:
apt-get install -y fonts-noto-cjk然后在代码里指定字体路径:
from matplotlib import font_manager font_manager.fontManager.addfont('/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc') matplotlib.rcParams['font.sans-serif'] = ['Noto Sans CJK JP']注意字体名称要用「Noto Sans CJK JP」,因为 Linux 上这个字体家族通常以 JP 命名,直接用 SC 可能找不到。
第二,相对路径失效。你在本地跑pd.read_csv('owid-co2-data.csv')没问题,但部署后工作目录可能不是项目根目录。推荐在使用文件前切换到脚本所在目录:
import os os.chdir(os.path.dirname(os.path.abspath(__file__)))这行代码把当前工作目录设置为.py文件所在目录,这样 CSV 的路径始终稳定。生产环境里也可以用pathlib.Path(__file__).parent拼接绝对路径,但os.chdir更粗暴有效。
最后一个小技巧:如果你希望大屏自动刷新数据,比如接入实时的碳排放监测接口,可以在load_data里加上pd.read_csv的cache参数配置,或者用st_autorefresh组件定时调用rerun。但对于历史数据分析大屏,数据是静态的,没必要频繁刷新。我个人更倾向于把 CSV 数据用「日期 + 数据集版本」命名,例如owid-co2-data-20250101.csv,更新数据时改文件名并更新代码里的路径,这样既保留历史版本,又便于回溯分析结论。
最后,部署到 Streamlit Community Cloud 时,记得在requirements.txt里固定版本号,尤其要写pandas>=2.0。因为 pandas 2.x 和 1.x 在resample别名上有差异,社区云默认安装最新版,如果你的代码里用了旧版'Y'别名,新版会报错。把这条路走通,你的二氧化碳排放大屏就不仅是一份作业,而是一个能随时打开给业务方看的交互式分析工具。
本文还有配套的精品资源,点击获取