简介这份资源面向具备一定数据分析基础、从事可持续发展、环境政策或公共管理领域的研究人员与高校师生围绕联合国17个可持续发展目标构建了SDRPI、SDGI与SDCI三个指数用于量化评估2000至2020年全球115个国家的综合绩效、目标间不平衡性与协调水平。资源包为单个PDF文件约674KB内含完整的Python复现代码及逐段解释涵盖模糊逻辑隶属度函数、基尼系数计算与滑动窗口相关性分析等核心环节并附有论文理论背景与结果讨论。读者可据此理解指数计算逻辑掌握跨国比较与区域发展模式识别的方法框架为学术复现或差异化政策研究提供可参考的代码基础。目前已有41人学习适合希望深入全球可持续发展格局动态认知的进阶读者。1. 从三个指数到一张全球绩效地图这套评估到底在算什么2000 到 2020 年全球可持续发展目标推进了二十年但如果你手里只有各国 GDP、碳排放、教育投入这些零散指标很难回答一个具体问题某个国家这二十年的“可持续绩效”到底是稳步改善还是靠某一年的资源透支换来的短期好看SDRPI、SDGI、SDCI 这三个指数就是为回答这类问题而设计的一套组合拳——它们分别从资源压力、发展增益、协调程度三个维度刻画一个国家或地区的可持续表现再叠加时间与空间两个轴看不平衡性和协调性怎么演化。这套评估适合谁做区域发展规划的研究者、需要给政策做量化背书的分析师、以及想用 Python 把多源面板数据跑成一张可解释地图的数据从业者。它不要求你会写复杂的计量模型但要求你能把 pandas 的数据对齐、numpy 的矩阵运算、scipy 的统计检验和 networkx 的协调网络串起来。下面我按“指数怎么算 → 数据怎么整 → 时空怎么拆 → 协调怎么建网 → 坑在哪 → 怎么验证”的顺序把这条链路走一遍。2. SDRPI-SDGI-SDCI 的构造逻辑与最小可跑实现2.1 三个指数分别代表什么为什么不能只用一个SDRPISustainable Development Resource Pressure Index衡量的是资源消耗与生态承载之间的张力数值越高说明单位发展成果背后的资源压力越大。SDGISustainable Development Gain Index看的是发展增益也就是教育、健康、收入这些维度的综合改善幅度。SDCISustainable Development Coordination Index则是前两者之间的协调度它不关心绝对高低只关心“压力”和“增益”是不是同向变化、比例是否失衡。只用一个指数的翻车点在于一个国家可能 SDGI 很高但 SDRPI 同样高说明增长是靠高消耗堆出来的单看增益会得出过于乐观的结论。三个指数一起看才能把“高增益高压力”“低增益低压力”“高增益低压力”这几类国家区分开。常见做法是先把每个维度的原始指标做极差标准化再用熵权法或等权法合成最后对 SDCI 用耦合协调度模型计算。2.2 用 pandas 做面板数据对齐与标准化假设你手里有三张表resource.csv、gain.csv、meta.csv分别存资源压力原始指标、发展增益原始指标和国家元数据。第一步不是急着算指数而是把索引对齐到“国家-年份”这个二维面板上。import pandas as pd import numpy as np # 读取三张原始表统一列名 res pd.read_csv(resource.csv) gain pd.read_csv(gain.csv) meta pd.read_csv(meta.csv) # 确认关键列存在避免后续 merge 时静默丢行 for df, cols in [(res, [country, year, res_raw]), (gain, [country, year, gain_raw])]: missing set(cols) - set(df.columns) if missing: raise ValueError(f缺少列: {missing}) # 按国家-年份做外连接保留所有出现过的组合 panel pd.merge(res, gain, on[country, year], howouter) panel panel.sort_values([country, year]).reset_index(dropTrue) # 极差标准化对每个指标列单独做避免量纲干扰 def minmax(series): lo, hi series.min(), series.max() if hi lo: return pd.Series(np.zeros(len(series)), indexseries.index) return (series - lo) / (hi - lo) panel[res_norm] panel.groupby(year)[res_raw].transform(minmax) panel[gain_norm] panel.groupby(year)[gain_raw].transform(minmax) # 检查缺失比例超过 30% 的年份要考虑插值还是剔除 na_ratio panel[[res_norm, gain_norm]].isna().mean() print(na_ratio)这段代码的关键点有三个。第一merge 用 outer 而不是 inner是为了暴露数据缺口而不是悄悄丢掉国家。第二标准化按 year 分组做是因为不同年份的指标分布可能漂移全局标准化会把时间趋势混进截面比较里。第三标准化前先判断 hi lo否则遇到某年所有国家取值相同会直接除零。参数上如果你用的是熵权法而不是等权把 minmax 换成熵值计算即可但注意熵权法对零值敏感标准化后要加一个极小偏移。2.3 合成 SDRPI、SDGI 并计算 SDCI标准化完成后SDRPI 和 SDGI 可以直接加权合成。SDCI 用耦合协调度公式# 等权合成权重可按研究设计调整 w_res, w_gain 0.5, 0.5 panel[SDRPI] w_res * panel[res_norm] panel[SDGI] w_gain * panel[gain_norm] # 耦合协调度C 为耦合度T 为综合协调指数D 为协调度 def coupling_coordination(a, b, alpha0.5, beta0.5): # 防止零值导致 log 或除零 eps 1e-9 a_safe np.clip(a, eps, None) b_safe np.clip(b, eps, None) C 2 * np.sqrt(a_safe * b_safe) / (a_safe b_safe) T alpha * a_safe beta * b_safe D np.sqrt(C * T) return C, T, D panel[C], panel[T], panel[SDCI] coupling_coordination( panel[SDRPI].values, panel[SDGI].values ) # 按国家看二十年 SDCI 的均值和趋势 summary panel.groupby(country).agg( sdci_mean(SDCI, mean), sdci_std(SDCI, std), years(year, nunique) ).sort_values(sdci_mean, ascendingFalse) print(summary.head(10))耦合协调度的逻辑是C 衡量两个系统同步程度T 衡量整体水平D 把两者开方后综合。alpha 和 beta 控制你更看重压力还是增益默认 0.5 是中性。注意 np.clip 那一步不是可选项——只要有一个国家的 res_norm 或 gain_norm 为 0C 就会变成 0 或 nan后面整条时间序列都会断。我一般会在标准化后统一加 1e-6 再进公式比在函数里 clip 更省事。3. 时空不平衡性把面板拆成时间趋势和空间截面3.1 时间维度用 scipy 做趋势检验而不是只看均值很多分析到 groupby 均值就停了但“2000-2020 年不平衡性”这个说法要求你判断趋势是否显著。Mann-Kendall 检验是非参数趋势检验里最常用的一种scipy 本身没有直接实现但可以用 scipy.stats.kendalltau 近似或者手写一个。from scipy import stats import numpy as np def mann_kendall(series): 返回 MK 统计量 S、标准化 Z 和双侧 p 值 x np.asarray(series, dtypefloat) n len(x) if n 8: return np.nan, np.nan, np.nan S 0 for i in range(n - 1): S np.sign(x[i 1:] - x[i]).sum() # 方差修正处理结值ties _, counts np.unique(x, return_countsTrue) tie_term sum(c * (c - 1) * (2 * c 5) for c in counts if c 1) var_s (n * (n - 1) * (2 * n 5) - tie_term) / 18 if S 0: Z (S - 1) / np.sqrt(var_s) elif S 0: Z (S 1) / np.sqrt(var_s) else: Z 0 p 2 * (1 - stats.norm.cdf(abs(Z))) return S, Z, p # 对每个国家的 SDCI 序列做趋势检验 trend_results [] for country, grp in panel.groupby(country): grp grp.sort_values(year) S, Z, p mann_kendall(grp[SDCI].values) trend_results.append({country: country, S: S, Z: Z, p: p}) trend_df pd.DataFrame(trend_results) trend_df[trend] np.where(trend_df[p] 0.05, np.where(trend_df[Z] 0, 上升, 下降), 不显著) print(trend_df[trend].value_counts())MK 检验的坑集中在结值修正上。如果你的 SDCI 保留两位小数很多国家会出现相同值不修正 tie_term 会让方差偏大、p 值偏保守。另一个坑是样本量n 小于 8 时 MK 检验基本没有统计效力我一般直接标 nan 而不是硬算。参数上显著性水平 0.05 是惯例但做多国比较时建议同时报告 Z 值大小因为 p 值受样本量影响很大。3.2 空间维度用基尼系数和泰尔指数拆解不平衡空间不平衡性不能只看最高最低两个国家常见做法是算截面基尼系数和泰尔指数。基尼系数对中间分布敏感泰尔指数可以把总差异拆成“组间”和“组内”适合按洲或收入组分层。def gini(values): x np.sort(np.asarray(values, dtypefloat)) n len(x) if n 0 or x.sum() 0: return np.nan index np.arange(1, n 1) return (2 * (index * x).sum() - (n 1) * x.sum()) / (n * x.sum()) def theil_t(values, groups): groups 为与 values 等长的分组标签 df pd.DataFrame({v: values, g: groups}).dropna() df df[df[v] 0] total_mean df[v].mean() df[share] df[v] / df[v].sum() df[log_term] np.log(df[v] / total_mean) T_total (df[share] * df[log_term]).sum() # 组间 grp df.groupby(g).agg(v_sum(v, sum), n(v, size)) grp[mu] grp[v_sum] / grp[n] grp[share] grp[v_sum] / df[v].sum() T_between (grp[share] * np.log(grp[mu] / total_mean)).sum() return T_total, T_between, T_total - T_between # 按年份算空间不平衡 spatial [] for year, grp in panel.groupby(year): g gini(grp[SDCI].dropna().values) T_total, T_between, T_within theil_t( grp[SDCI].values, grp[country].map(meta.set_index(country)[region]).values ) spatial.append({year: year, gini: g, theil_total: T_total, theil_between: T_between, theil_within: T_within}) spatial_df pd.DataFrame(spatial) print(spatial_df.head())基尼系数这里用的是排序加权公式比直接套积分公式更稳。泰尔指数要求所有值为正SDCI 本身在 0 到 1 之间但如果有 0 值要先剔除或加偏移。组间泰尔占比越高说明不平衡主要来自区域之间而不是区域内部这个结论直接决定你后续政策建议是“跨区域协调”还是“区域内补短板”。4. 协调性分析用 networkx 构建国家协调网络4.1 协调性为什么适合用网络来表达SDCI 衡量的是单个国家内部压力与增益的协调但“协调性分析”在时空框架下还有一层含义国家之间在可持续绩效上是否同步变化。如果两个国家的 SDCI 时间序列高度相关可以认为它们在可持续路径上存在联动。把所有国家的两两相关关系建成网络节点是国家边是显著相关就能用 networkx 看社区结构、中心性和连通性。常见做法是先用皮尔逊或斯皮尔曼相关算相关矩阵再做显著性过滤最后用阈值或最小生成树建图。斯皮尔曼对非线性单调关系更稳我一般优先用它。4.2 从相关矩阵到邻接矩阵的构建步骤import networkx as nx from scipy.stats import spearmanr from itertools import combinations # 把面板转成 国家 x 年份 的宽表 wide panel.pivot_table(indexcountry, columnsyear, valuesSDCI, aggfuncmean) # 至少要有 10 个年份才纳入网络否则相关不可靠 wide wide.dropna(thresh10) countries wide.index.tolist() corr_matrix wide.T.corr(methodspearman) # 构建邻接矩阵只保留显著且相关系数超过阈值的边 threshold 0.6 adj pd.DataFrame(0, indexcountries, columnscountries) pairs list(combinations(countries, 2)) for a, b in pairs: r, p spearmanr(wide.loc[a], wide.loc[b], nan_policyomit) if p 0.05 and abs(r) threshold: adj.loc[a, b] r adj.loc[b, a] r G nx.from_pandas_adjacency(adj) # 去掉孤立节点避免后续中心性全为 0 G.remove_nodes_from(list(nx.isolates(G))) print(f节点数: {G.number_of_nodes()}, 边数: {G.number_of_edges()}) print(f连通分量数: {nx.number_connected_components(G)}) # 社区检测与中心性 from networkx.algorithms.community import greedy_modularity_communities communities list(greedy_modularity_communities(G, weightweight)) betweenness nx.betweenness_centrality(G, weightNone) top_hub sorted(betweenness.items(), keylambda x: -x[1])[:5] print(社区划分:, [len(c) for c in communities]) print(中介中心性 Top5:, top_hub)这段代码有几个必须注意的参数。threshold 设 0.6 是经验值设太低网络会变成一团设太高会只剩孤立点建议在 0.5 到 0.7 之间试。p 0.05 是显著性门槛但做几十个国家两两比较时多重检验问题严重严格做法是用 Benjamini-Hochberg 做 FDR 校正。greedy_modularity_communities 的 weight 参数要和你邻接矩阵里存的值一致如果存的是相关系数weightweight 会让强相关边权重更大社区划分更合理。betweenness_centrality 默认不认权重如果你想让强相关边影响中心性要显式传 weight 并确保边属性名对得上。4.3 协调网络的解读边界网络建出来之后最容易犯的错是把“相关”当“因果”。两个国家 SDCI 同步上升可能是因为它们有相似的产业结构也可能只是全球大趋势下的共同漂移。我一般会补一个偏相关或去掉全球均值后再算相关看边是否还稳。另一个边界是时间窗口2000-2020 整体相关可能掩盖前十年和后十年的结构变化建议按五年或十年分段建网对比社区划分是否漂移。5. 避坑与排查这套评估里最容易翻车的五个地方5.1 现象merge 之后国家数变少原因键值不一致解决先做键标准化最常见的翻车是 resource.csv 里写 United Statesgain.csv 里写 USAmerge 之后直接丢一半行。解决方式是在 merge 前统一做键标准化去空格、转小写、维护一张别名映射表。别指望 fuzzy merge那会引入更难查的错配。5.2 现象SDCI 出现 nan 或 0原因标准化后存在零值解决加偏移或改用非零下限耦合协调度公式里只要有一个输入为 0C 就退化成 0D 也跟着为 0。如果你的数据里确实有国家在某年资源压力为 0要么加 1e-6 偏移要么改用其他协调度定义。我一般会在标准化后统一panel[[res_norm,gain_norm]] 1e-6并在文档里注明。5.3 现象MK 检验 p 值大面积不显著原因序列太短或结值太多解决降精度或改用 Sen 斜率2000-2020 只有 21 个点如果 SDCI 保留两位小数结值会很多方差被高估p 值偏大。解决方式一是保留更多小数位二是改用 Sen 斜率配合置信区间判断趋势方向和幅度不单纯依赖 p 值。5.4 现象networkx 建图后全是孤立点原因阈值过高或样本太短解决降阈值并检查缺失如果 wide 表里很多国家只有几年数据spearmanr 的 nan_policyomit 会让有效样本更少相关不稳定。先检查wide.notna().sum(axis1)把年份不足的国家剔掉再建网比事后调阈值更靠谱。5.5 现象不同年份标准化后不可比原因按年标准化改变了分布解决明确比较目标按年做 minmax 适合截面比较但如果你要比较同一个国家 2000 年和 2020 年的绝对进步按年标准化会把时间趋势抹掉。这时候应该用全局标准化或固定基准年。选哪种取决于你的问题问“哪年国家间差距大”用按年问“哪个国家进步快”用全局。6. 进阶验证用置换检验和分段网络确认结论不是巧合前面所有结论都建立在“观测到的模式有意义”这个假设上。进阶做法是用置换检验给 SDCI 趋势和网络边做零模型验证。具体操作把每个国家的年份标签随机打乱重算 MK 统计量和相关矩阵重复 1000 次看真实值在零分布里的分位。如果真实 Z 值超过零分布 95 分位趋势才站得住。def permutation_test_mk(series, n_perm1000, seed42): rng np.random.default_rng(seed) x np.asarray(series, dtypefloat) _, Z_real, _ mann_kendall(x) null_Z [] for _ in range(n_perm): shuffled rng.permutation(x) _, Z_null, _ mann_kendall(shuffled) if not np.isnan(Z_null): null_Z.append(Z_null) null_Z np.array(null_Z) p_perm (np.abs(null_Z) abs(Z_real)).mean() return Z_real, p_perm # 对趋势最明显的五个国家做置换检验 top5 trend_df.dropna().reindex( trend_df[Z].abs().sort_values(ascendingFalse).index ).head(5)[country].tolist() for c in top5: s panel[panel[country] c].sort_values(year)[SDCI].values Z_real, p_perm permutation_test_mk(s) print(f{c}: Z{Z_real:.2f}, 置换 p{p_perm:.3f})置换检验的参数里n_perm 建议至少 1000seed 固定保证可复现。注意 MK 统计量在置换下不是标准正态所以直接比绝对值分位不要套正态 p 值。分段网络验证则是把 2000-2020 切成 2000-2010 和 2011-2020 两段分别建网看社区划分的调整兰德指数。如果两段社区几乎不重叠说明协调结构在十年间发生了实质变化整体网络的平均结论会掩盖这个转折。我自己在这套流程里最深的教训是不要等所有指数算完才检查数据质量。我一般会在 merge 之后、标准化之前先跑一遍缺失热力图和键值匹配率把问题拦在最前面。另一个习惯是每个中间结果都落盘成 parquet方便后面分段重算时不用从头跑。这套评估的价值不在于三个指数的公式多精巧而在于它逼你把时间、空间、协调三个维度都显式地拆开验证而不是用一个综合得分糊弄过去。希望帮到你。本文还有配套的精品资源点击获取