简介:这是一份基于Python的生物多样性与气候变化可视化分析项目的完整源代码与配套数据集,适合具备一定Python基础、从事生态学或环境科学相关数据分析和可视化的学习者。项目围绕生物多样性数据和气候因子展开,包含数据处理、图表绘制与分析流程,代码结构清晰。资源包共707个文件,压缩后约148MB,主要包含Python脚本与Jupyter Notebook(py/ipynb)、生物多样性数据集(csv)、可视化图片(png/jpg)以及交互式HTML报告等。各类文件按模块存放:原始数据置于DataGather目录,生成图表统一输出至ImageResource目录,每个分析单元有独立目录,便于逐项复现与对照学习。目前页面已有144人浏览学习,适合需要上手实战练习、参考完整项目流程的读者;通过该项目,可以掌握从数据读取、清洗到可视化展示的完整路径,并可基于已有数据和图表模板扩展自己的分析需求。
1. 从 WDI 到物种档案:这个可视化项目到底在分析什么
打开项目目录第一眼看到的不是代码,而是五个年份的SpeciesProfile.csv和一份WDIData.csv,这种文件排布方式说明它不是一个以算法为核心的工程,而是一个以"数据对齐"为核心的实证分析项目。WDIData是世界银行 WDI 数据库的截面快照,覆盖全球两百多个经济体的发展指标,而SpeciesProfile连续五年存放,意味着作者想做的事情是:把气候相关指标(气温、降水、CO₂ 排放、森林面积等)和生物多样性状态(物种丰度、濒危等级计数等)放在同一把时间尺度和同一套国家代码体系下做交叉观察。数据文件放在DataGather、图表输出到ImageResource,每一个分析单元一个目录,这套约定意味着项目可以被切成多个独立任务分别跑,而不是一个从头到尾的大脚本。适合阅读这份代码的人,是那些手里有截面数据、想把环境变量与生态变量拼在一起做趋势判断的分析师和研究者,因为项目真正值钱的是它处理两类异构数据时的关键取舍。
2. 数据管道:WDIData 与 SpeciesProfile 的预处理与关联
2.1 先看文件结构:宽表、长表与脚注表的分工
进入DataGather目录,WDIData.csv是典型的世界银行宽表:每一行是一个国家在某年的若干指标,列名里同时混着国家代码、指标名称以及 1960 到 2023 的年份列。WDIFootNote.csv则是脚注表,存放每个指标的统计口径、数据来源与特殊说明。而SpeciesProfile系列文件的结构通常是长表,每一行代表"某个分类单元在某年的评估状态",包含物种名、分类等级、濒危等级字段(比如 IUCN 的 LC/VU/EN/CR)、所属国家或地区。
这种结构差异决定了第一件要做的事是统一坐标系。宽表适合人读,但不适合matplotlib直接绘图,也不适合与物种表做关联。先写一个通用的读取与整形函数,把 WDI 数据从宽表转成长表,同时保留脚注信息作为元数据。
import pandas as pd def load_wdi_long(path: str, start_year: int = 2000) -> pd.DataFrame: df = pd.read_csv(path) id_vars = ["Country Name", "Country Code", "Indicator Name", "Indicator Code"] # 保留固定的标识列,把年份列全部转为长表行 long_df = df.melt( id_vars=id_vars, var_name="Year", value_name="Value", ) long_df["Year"] = long_df["Year"].astype(int) long_df = long_df[long_df["Year"] >= start_year] # 剔除空值占比过高的指标,按行保留可分析窗口 valid = long_df.dropna(subset=["Value"]) valid = valid[valid["Value"] != 0] return validmelt的作用是把宽度接近 60 列的年份字段折叠成两列Year和Value,这样后续按国家、按指标过滤时,只需要对Indicator Name做字符串匹配,不必记住每个指标在第几列。start_year参数用于裁剪早期数据稀疏的年份;WDI 在 1960 年代的覆盖度很低,如果直接全量保留,后面做时序对齐时会产生大量空值。这里的Value != 0过滤要谨慎,部分指标(如森林面积净变化率)本身就可能是 0,更稳妥的只过滤NaN,是否过滤 0 取决于具体指标口径。
2.2 物种数据的标准化与年份补齐
SpeciesProfile文件每年一份,但年份信息在文件名里而不是在表内,所以读取时要手动注入年份列。另一个关键问题是物种表的粒度:同一个物种可能在不同年份、不同国家下出现多条记录,如果直接用merge和气候数据拼接,会出现一对多万向爆炸。标准做法是先把物种表聚合到国家 + 年份 + 濒危等级计数的粒度,再和其他表做关联。
import glob def load_species_profiles(profile_dir: str) -> pd.DataFrame: frames = [] for fp in sorted(glob.glob(f"{profile_dir}/2*_SpeciesProfile.csv")): year = int(fp.split("/")[-1][:4]) df = pd.read_csv(fp) df["Year"] = year frames.append(df) species = pd.concat(frames, ignore_index=True) # 按国家、年份、濒危等级统计物种数 summary = ( species.groupby(["Country Code", "Year", "Red List Category"]) .size() .reset_index(name="Species Count") ) return species, summaryglob用文件名前缀2*直接匹配 2018 到 2022 这几个年份文件,好处是新增一年数据时不需要改代码。groupby后得到的summary是最小可用粒度,既保留物种多样性信息(不同濒危等级的数量分布),又压缩了数据量。注意这里Country Code是关联 WDI 的关键字段,如果物种表里没有国家代码而只有国家名,需要先用 ISO 3166-1 alpha-3 映射表做转换,这一步省略会导致后续 merge 失败。
2.3 关联策略:以国家代码为主键,以年份为内连接
WDI 和物种数据都有国家代码,但覆盖集合不同。WDI 覆盖全球,而物种数据往往集中在部分热点区域。直接取交集会丢掉样本量,但取并集又会出现大段空值。实际项目中推荐的做法是:以 WDI 的国家集合为基准,物种表为右表,做左连接,然后按指标维度做去空判断。
def build_analysis_frame(wdi_long: pd.DataFrame, species_summary: pd.DataFrame): merged = wdi_long.merge( species_summary, on=["Country Code", "Year"], how="inner", validate="one_to_many", ) return merged这里用inner而不是left,是因为绘图和趋势分析中残留空值会干扰回归和均值计算;宁可样本少一些,也要保证每个数据点都是完整的。validate="one_to_many"是防止 WDI 侧因为同一个国家年份出现重复指标行导致笛卡尔积,属于廉价但很有用的防御性编程。
3. 指标交叉:气候变量与生物多样性指标的时序对齐方法
3.1 先做时序完整性检查
跨年份分析(2018–2022)最怕的是某些国家只有部分年份有数据。可视化做出来是一条断折线,看不出是数据缺失还是真实下降。做法是先对Country Code + Indicator Name做透视,检查每个序列的年份覆盖度:
coverage = ( wdi_long.groupby(["Country Code", "Indicator Name"]) .agg( available_years=("Year", "nunique"), first_year=("Year", "min"), last_year=("Year", "max"), ) .reset_index() ) full_coverage = coverage[coverage["available_years"] >= 4]nunique统计的是年份种类数而不是行数,能防止同一年份多行记录导致的假性完整。first_year和last_year用来确认序列是否覆盖完整区间;如果某国家从 2021 年才开始有数据,那五年趋势图上就会出现前端缺失。过滤出available_years >= 4的国家和指标组合,是画趋势图前的最后一道门槛,低于门槛的数据做出来的线性回归和斜率比较没有统计意义。
3.2 宽表交叉与滚动窗口计算
时序对齐做完之后,把关键的几个指标从长表里抽出,做成宽表,每行是国家 + 年份,每列是一个指标值。这样既方便算相关性,也方便直接喂给seaborn画散点图矩阵。项目中常见的做法是同时计算气候变量的 3 年滚动均值,因为生物多样性对气候变化的响应有滞后性,用当年值匹配当年物种数据往往看不到明显规律。
def pivot_indicators(wdi_long: pd.DataFrame, indicators: list[str]) -> pd.DataFrame: subset = wdi_long[wdi_long["Indicator Name"].isin(indicators)] wide = subset.pivot_table( index=["Country Code", "Year"], columns="Indicator Name", values="Value", aggfunc="mean", ).reset_index() wide = wide.sort_values(["Country Code", "Year"]).groupby("Country Code").transform( lambda x: x.rolling(3, min_periods=1).mean() ) wide[["Country Code", "Year"]] = subset[["Country Code", "Year"]].drop_duplicates().values return wide这里pivot_table的aggfunc="mean"是为了防止同一国家年份下同一个指标出现多行(比如 WDI 有时会同时记录区域值和国家值)。rolling(3, min_periods=1)是处理前两年前值不足的窗口边缘情况,min_periods=1表示至少有一个窗口值就计算结果,这样不会在序列开头引入 NaN。滞后对齐是环境数据分析里最容易忽略的一步——气候对物种的影响通常要一到三年才能体现,直接用当年值拟合相关性,结果往往弱于预期。
3.3 缺失值的层次化处理
交叉表里可能出现三种缺失:气候数据有但物种数据无、物种数据有但气候数据无、两者都没有。第一种最常见,发生在某些小岛屿国家——WDI 有碳排放记录,但 IUCN 物种评估没覆盖。第二种出现在物种热点区域如马达加斯加,物种评估齐全但部分气候站点数据缺失。处理策略不是统一填充,而是分场景。
def fill_missing(df: pd.DataFrame, max_interp_gap: int = 2) -> pd.DataFrame: df = df.sort_values(["Country Code", "Year"]) df[["CO2_emissions", "Temperature", "Precipitation"]] = ( df.groupby("Country Code")[ ["CO2_emissions", "Temperature", "Precipitation"] ] .transform(lambda s: s.interpolate(limit=max_interp_gap, limit_direction="both")) ) return dfinterpolate只对相邻年份做线性插值,limit=2表示连续缺失超过两年就不插,保留 NaN,避免在数据稀疏区域用外推制造出平滑的假曲线。limit_direction="both"允许对序列开头和结尾的缺失值做填充,这在 WDI 数据里很关键——某些国家 2018 年才加入统计,但 2019、2020 的物种数据齐全,序列开头的缺失不做处理就会丢掉整个国家。对物种数据侧的缺失不做插值,因为物种评估有周期性,两轮评估之间的缺失不代表物种消失,插值会掩盖评估频率本身的信息。
4. 可视化呈现:多国多年份趋势图的绘制与参数调优
4.1 单指标时间序列:各国趋势叠放
项目把生成图表统一存到ImageResource目录,每个分析单元一个子目录,所以输出路径要事先约定好。画多国趋势时,直接叠画所有国家会得到一张杂乱无章的线网,实际处理中应该先做国家分组:把处于同一地理区域或同一发展水平组别的国家拆开画子图,趋势线表示该组均值,散点表示当年具体国家值。
import matplotlib.pyplot as plt import numpy as np def plot_group_trend(df, group_col, value_col, output_path): fig, ax = plt.subplots(figsize=(12, 6)) groups = df[group_col].unique() for g in groups: subset = df[df[group_col] == g] trend = subset.groupby("Year")[value_col].mean() ax.plot(trend.index, trend.values, label=g, linewidth=2) # 用半透明散点画出组内国家分布,避免折线掩盖波动 for _, row in subset.iterrows(): ax.scatter(row["Year"], row[value_col], s=12, alpha=0.25) ax.set_xlabel("Year") ax.set_ylabel(value_col) ax.legend(frameon=False) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) fig.tight_layout() fig.savefig(output_path, dpi=300, bbox_inches="tight") plt.close(fig)这里子图内先画组均值折线,再叠加散点,能同时展示中心趋势和离散度。alpha=0.25把单个国家的点压淡,避免一个国家的高值主导视觉。bbox_inches="tight"防止年份坐标轴标签(如 2018)被截图裁掉。dpi=300不是越高越好——网页展示用 150 足够,但论文配图需要 300,这里作为项目通用值折中。
4.2 相关性热力图:气候指标与物种状态
另一类高频图是气候指标和濒危物种数量之间的相关性矩阵。用seaborn的heatmap可以一次性看清楚温度、降水、CO₂、森林面积与 EN(濒危)和 CR(极危)物种数两两之间的线性相关方向。相关性计算之前先对非正态分布的物种计数做对数变换,否则个别国家的高物种基数会主导结果。
import seaborn as sns def plot_corr_matrix(df, save_path: str): cols = ["Temperature", "Precipitation", "CO2_emissions", "Forest_area"] species_cols = ["CR Count", "EN Count", "VU Count"] combined = df[cols + species_cols].copy() combined[species_cols] = np.log1p(combined[species_cols]) corr = combined.corr(method="spearman") fig, ax = plt.subplots(figsize=(10, 8)) sns.heatmap( corr, annot=True, fmt=".2f", cmap="RdBu_r", center=0, vmin=-1, vmax=1, square=True, linewidths=0.5, cbar_kws={"shrink": 0.8}, ax=ax, ) fig.savefig(save_path, dpi=300, bbox_inches="tight") plt.close(fig)method="spearman"选择秩相关而不是皮尔逊相关,因为物种计数分布呈长尾,皮尔逊相关容易被少数高值点带偏。log1p是log(x+1)的数值稳定写法,处理 0 值不报错。RdBu_r色带把负相关映射为蓝色、正相关映射为红色,center=0保证色带起点居中,避免全图色偏。
4.3 关键参数表:出图前逐项确认
| 图形类型 | 适用场景 | 关键参数 | 常见错误 |
|---|---|---|---|
| 多国折线图 | 趋势对比 | linewidth=2区分主线;alpha降噪 | 图例过多,超过15条直接弃用 |
| 相关性热力图 | 指标两两关系 | method="spearman";log1p变换目标列 | 不检查分布直接画皮尔逊相关 |
| 双轴柱线图 | 气候 vs. 物种数 | twinx();右侧轴加set_ylim | 左右轴量纲失衡,曲线趋势被扭曲 |
| 散点回归图 | 单指标与单一等级 | scatter_kws设透明度 | 不标注 R² 和 p 值,图不可独立阅读 |
双轴图是最容易被误读的图形:两个轴各自缩放,视觉上"趋势一致"可能是缩放后的假象。项目里如果画双轴图,建议在图上标注左右轴的相关系数,并注明scipy.stats.pearsonr计算出来的显著性水平。
5. 工程化落地:buildout 依赖管理与 ImageResource 目录约定
5.1 buildout.cfg 在项目中的作用
项目根目录里有buildout.cfg和setup.cfg,这不是常见的requirements.txt工作流。buildout是 zc.buildout 工具链的配置文件,它的核心价值不是替代 pip,而是把"环境变量、路径约定、启动入口"一起固化成可复现的构建过程。对于这类数据分析项目,buildout.cfg的典型作用是定义部分(parts),为DataGather和ImageResource这两个数据目录创建固定的文件系统结构。
[buildout] parts = datadir plotdir [datadir] recipe = plone.recipe.command command = mkdir -p ${buildout:directory}/DataGather ${buildout:directory}/ImageResource update-command = true [plotdir] recipe = plone.recipe.command command = python -m compileall ${buildout:directory}/src || trueplone.recipe.command会在buildout执行时运行指定的 shell 命令,这里用mkdir -p保证项目在全新 clone 之后也能自动创建数据输出目录。compileall做一个语法检查,|| true保证即使编译警告也不会中断流程。如果不想引入buildout,等效做法是写一个Makefile或init_dirs.sh,但buildout的优势在于多环境一致性——团队里有人用 Windows、有人用 Linux,buildout会把目录创建逻辑统一到一套配置里。
5.2 setup.cfg 做包级元数据管理
setup.cfg的存在意味着项目支持pip install -e .的开发模式安装。分析项目中常把工具函数(如数据加载、预处理、绘图封装)抽成包,测试和复现时直接 import 而不用到处复制脚本路径。
[metadata] name = biodiversity-climate-analysis version = 0.1.0 description = Biodiversity and climate change visualization from WDI and SpeciesProfile [options] packages = find: include_package_data = True python_requires = >=3.8 install_requires = pandas>=1.3 matplotlib>=3.5 seaborn>=0.11 numpy>=1.21python_requires = >=3.8设定了 Python 版本下限,seaborn在 0.11 以上的 API 才稳定支持heatmap的square参数。如果在新机器上执行pip install -e .失败,优先检查 Python 版本和seaborn是否装成了 0.10 老版本。
5.3 目录规范:把输出和输入隔离
项目要求图表数据统一存到ImageResource,这不仅仅是规整问题,更是可复现性问题。分析过程中会反复调整参数重跑实验,如果输出散落各处,旧图和新图混在一起,很容易在写报告时引用错误的版本。常见的改良是在ImageResource下按分析单元和时间戳再加一层子目录。
from pathlib import Path from datetime import datetime def make_output_dir(analysis_name: str) -> Path: base = Path("ImageResource") / analysis_name stamp = datetime.now().strftime("%Y%m%d_%H%M") out_dir = base / stamp out_dir.mkdir(parents=True, exist_ok=True) return out_dir时间戳方案显著增加生成的子目录数量,但每次重跑都会产生新的快照,不用担心覆盖之前的结果。parents=True会自动创建多级目录,exist_ok=True则允许同一分钟内重复运行时复用目录。对于路径包含中文的项目,Path对象比字符串拼接更安全,不会因为分隔符或转义问题在 Windows 上报错。
6. 扩展:把五年截面升级为趋势预警
五年的SpeciesProfile数据不只能画出逐年变化,还能用来构建简单的物种状态恶化预警。核心思路是:对每个国家计算EN(濒危)+ CR(极危)物种数在 2018–2022 年间的单调变化趋势,如果呈显著上升,标记为"生态压力上升"国家,再与同期的气候指标变动方向做联合判断。这个逻辑不复杂,但需要从五个独立的截面算出一个可跨年比较的指标,实现上要小心物种评估周期的影响——物种评级不是每年都更新,某个国家 2018 到 2020 年数值完全相同是正常的评估周期现象,不代表生物多样性稳定。
from scipy.stats import linregress def compute_trend_score(df, country_col="Country Code", year_col="Year", value_col="EN_CR Count"): results = [] for country, grp in df.groupby(country_col): grp = grp.sort_values(year_col) x = grp[year_col].values y = grp[value_col].values if len(x) < 3: continue slope, intercept, r_value, p_value, std_err = linregress(x, y) results.append({ "Country Code": country, "slope": slope, "r_squared": r_value**2, "p_value": p_value, "annual_rate": slope / max(np.mean(y), 1), }) return pd.DataFrame(results)linregress直接返回斜率、R²、p 值和标准误。实际筛选时用p_value < 0.1且slope > 0标记为"压力上升",p_value > 0.9且slope < 0标记为"改善",其他为"稳定"。annual_rate把绝对斜率归一化为相对于均值的变化率,解决物种基数大的国家天然斜率值更高的问题。值得注意:五年五个点做回归,自由度只有 3,统计功效很弱,p_value实际只适合做排序参考,不能当作严格显著性检验。更稳妥的做法是只取排名前 20 的国家做案例展示,而不是对所有国家下一个统计学结论。
把趋势得分与气温、降水变化做拼接后,用散点图绘制temperature_slope与biodiversity_slope的关系,右上象限的国家(气温上升的同时 EN+CR 数量也在上升)就是值得优先观察的对象。若某国家的物种数据在五年间没有评估更新,散点会落在横轴上,需要在图上标注"no reassessment",避免阅读者误读为稳定。这一步做完,项目的产出就从"展示了什么"升级为"下一轮数据来了之后该重点看哪里",这也是我建议在复盘时保留的一个模块。
本文还有配套的精品资源,点击获取