世界银行开放数据 — 29,544 个指标,把「国家」变成一行

它不是一个数据集,是 71 个数据源的统一检索层:每个指标都标了原始生产者(WHO、UNICEF、ILO、IMF),每条观测都带国家代码、年份、单位与小数位。做跨国健康公平研究时,你真正需要的那张表在这里是「国家 × 年份 × 指标」的三维点阵

来源 World Bank Open Data(World Bank Group) url: https://data.worldbank.org/发布时间: 2026-10-24最后更新: 2026-10-24 阅读 7
世界银行开放数据 — 29,544 个指标,把「国家」变成一行

信息速览

数据集名称世界银行开放数据 — 29,544 个指标,把「国家」变成一行
数据类型29,544 个指标,296 个国家与地区,71 个数据源,健康主题 658 指标,1960 年至今,CC BY 4.0,v2 REST API
规模不适用(国家与地区级聚合统计,无个体记录;个体级微数据在独立的 Microdata Library,另需申请)
接入方式World Bank Open Data(World Bank Group) url: https://data.worldbank.org/
AI 就绪度

§0 本条目怎么来的、不做什么

整理方:千方医数集,为中文医学 AI 社区整理可训练数据集的结构化说明。

本条目怎么来的:数字主源是 World Bank v2 REST API 的实时响应(2026-10-24 采集):/indicator 的 total 得 29,544、/country 的 total 得 296、/source 的 total 得 71、/topic/8/indicator 的 total 得 658;指标元数据与观测值样例取自 /indicator/SH.DYN.MORT 与 /country/CHN/indicator/SH.DYN.MORT。许可条款取自世行数据目录的官方页面 Data Access And Licensing。

不做什么:不评价任何一个指标的统计方法;不核验世行数字与各国统计局原始发布的差异;不判断某国数据是否可信。本条目只回答一个问题:这张表长什么样、怎么用、哪里会坑人。


§1 数据集概览

§1.1 一句话

世界银行开放数据不是一个数据集,是71 个统计源的统一检索层——把「某国某年某指标等于多少」变成一个可以用 URL 直接取出的三元组。

§1.2 硬数字

项 值 来源
指标数 29,544 /indicator?per_page=1 的 total 字段
国家与地区数 296 /country?per_page=1 的 total 字段
数据源数 71 /source?per_page=1 的 total 字段
健康主题指标数 658 /topic/8/indicator 的 total 字段
主题数 21 /topic 返回 21 个主题
时间起点 1960 年 世行开放数据的公认起点
粒度 国家 × 年 无月、无日、无个体
默认许可 CC BY 4.0 Data Access And Licensing 页

§1.3 为什么要有一个「跨国统计」条目

库内公共卫生类条目已有 113 条,但它们几乎全是疾病层面的数据集:影像、病历、测序、队列。缺的是一层更外圈的东西——把一个国家当成一行,看它在健康上的位置。

这一层不解决「怎么诊断」,解决的是「为什么这个国家的某项指标和那个国家差三倍」。做医疗 AI 的人经常需要这类背景变量:训练集来自哪个国家、那个国家的医疗支出占 GDP 多少、医生密度多少、五岁以下死亡率多少。这些变量不在任何影像数据集里,在世界银行里。

§1.4 与本库已有条目的边界

库内条目 关系 区别
our-world-in-data(rid 185) 同域,不同层 OWID 是二次汇编,把世行、WHO、UN 的数据标准化后重绘;本条目是一手 API,拿到的是未被改写的原始观测
who-gho(rid 175) 同域,不同源 GHO 是 WHO 自己的观测,健康指标更细;世行是编目层,健康只是 21 个主题之一
gbd 同域,不同方法 GBD 是建模估计(用协变量填补),世行多数健康指标来自各国上报 + 联合国机构估算

一句话分工:WHO 说「这个病怎么样」,GBD 说「我们推算这个病怎么样」,世行说「这个国家和别的国家比怎么样」。


§2 二十一个主题,健康只占一个

§2.1 主题清单

/topic?format=json 返回 21 个主题,按官方顺序:

id 主题 与医学 AI 的关系
1 Agriculture & Rural Development 间接(营养、粮食安全)
2 Aid Effectiveness 间接(卫生援助流)
3 Economy & Growth 强(医疗支出分母)
4 Education 间接(健康素养)
5 Energy & Mining 弱
6 Environment 中(空气污染、饮水)
7 Financial Sector 弱
8 Health 强(658 指标)
9 Infrastructure 弱
10 Social Protection & Labor 中
11 Poverty 中(健康的社会决定因素)
12 Private Sector 弱
13 Public Sector 弱
14 Science & Technology 中(R&D 投入)
15 Social Development 中
16 Urban Development 中
17 Gender 中(孕产妇健康)
18 Millennium development goals 中(历史口径)
19 Climate Change 中
20 External Debt 弱
21 Trade 弱

§2.2 为什么健康只有 658 个指标,却值得单独立条

因为健康研究真正缺的从来不是健康指标,是分母。

举例:你要研究「某国五岁以下死亡率十年下降多少」,需要的不只是 SH.DYN.MORT,还需要:

  • SP.POP.TOTL(总人口)——算分母
  • SH.XPD.CHEX.GD.ZS(经常性卫生支出占 GDP)——算投入
  • SH.MED.PHYS.ZS(每千人医生数)——算能力
  • NY.GDP.PCAP.CD(人均 GDP)——控制混杂
  • SI.POV.DDAY(贫困率)——控制社会决定因素

这五个指标分散在 Health、Economy、Poverty 三个主题里。只有在世界银行这一层,它们才在同一个主键体系下(同一套 countryiso3code + date),可以直接 join。这是本条目对医学 AI 研究者的核心价值——不是提供疾病数据,是提供能和其他数据 join 上的国家层协变量。

§2.3 指标代码的命名规则

SH.DYN.MORT 这类代码不是随机的,是可解析的:

SH . DYN . MORT
│    │     └── 具体量:MORT = mortality
│    └──────── 中段:DYN = dynamic(动态人口学)
└───────────── 主题前缀:SH = Social / Health

常见前缀:

前缀 含义 例子
SH Health(健康) SH.DYN.MORT 五岁以下死亡率
SP Social Protection & Labor(人口) SP.POP.TOTL 总人口
SE Education(教育) SE.ADT.LITR.ZS 识字率
NY National Accounts(国民经济) NY.GDP.PCAP.CD 人均 GDP
SI Social Inclusion(贫困) SI.POV.DDAY 每日 1.90 美元以下人口比
EN Environment(环境) EN.ATM.PM25.MC.M3 PM2.5 年均浓度
AG Agriculture(农业) AG.LND.AGRI.ZS 农业用地占比

实用推论:想找健康指标,不必翻 658 条,直接 SH.* 前缀过滤即可。想找分母,SP.POP.TOTL 是唯一正确的选择(不是 SP.POP.TOTL.FE.ZS 这种分性别的)。

§2.4 健康主题里 658 个指标的内部结构

/topic/8/indicator 返回的不是一整片平铺的 658 条,而是可以按二级前缀归成 23 组。实测的分组如下(按条数降序):

二级前缀 条数 含义 典型指标
SH.STA 156 营养与贫血(stunting / anemia) SH.STA.STNT.ZS 五岁以下发育迟缓率
SH.FPL 71 家庭计划与生育(family planning) SH.FPL.SATI.ZS 避孕需求满足率
SH.MLR 43 疟疾(malaria) SH.MLR.INCD.P3 疟疾发病率
SH.ACS 40 基本医疗可及性(access to care) SH.ACS.MONY.RT 因病致贫比例
SH.IMM 29 免疫接种(immunization) SH.IMM.MEAS 麻疹疫苗接种率
SH.XPD 28 卫生支出(expenditure) SH.XPD.CHEX.GD.ZS 经常性卫生支出占 GDP
SH.UHC 22 全民健康覆盖(UHC) SH.UHC.SRVS.CV.XD UHC 服务覆盖指数
SH.DYN 18 人口动态与死亡率 SH.DYN.MORT 五岁以下死亡率
SH.HIV 14 艾滋病 SH.HIV.INCD.ZS HIV 感染率
SH.DTH 12 死因结构(cause of death) SH.DTH.COMM.ZS 传染病死亡占比
SH.H2O 11 安全饮水与卫生设施 SH.H2O.SMDW.ZS 安全饮水可及率
SH.PRV 8 私营部门参与 SH.PRV.SMOK 吸烟率相关
SH.VAC 6 疫苗覆盖率补充 —
SH.MED 5 医疗人力与设施 SH.MED.PHYS.ZS 每千人医生数
SH.ALC 3 酒精 SH.ALC.PCAP.LI 人均酒精消费
SH.ANM 3 贫血 SH.ANM.ALLW.ZS 育龄妇女贫血率
SH.MMR 3 孕产妇死亡 SH.MMR.RISK 终身孕产死亡风险
SH.SGR 3 手术安全 —
SH.SVR 3 严重疾病治疗可及性 SH.SVR.WAST.ZS 重度消瘦率
SH.TBS 3 结核 SH.TBS.INCD 结核发病率
SH.CON 2 污染相关 SH.CON.1524.ZS 15–24 岁污染相关死亡
SH.PRG 1 孕产 —
SH.VST 1 就诊 —

这张表最反直觉的一处:SH.STA 占 156 条,接近健康主题的四分之一。 它是「营养与贫血」组,其中大量细分是「按性别 × 按年龄组 × 按 severity 分层」的交叉切分——同一个发育迟缓率,会同时以 SH.STA.STNT.ZS(总)、.FE.ZS(女)、.MA.ZS(男)、按年龄段分的多种形式存在。

推论:如果你按「条数」估计某个健康议题的数据丰富程度,会严重高估营养类、低估医疗人力类。SH.MED 只有 5 条——医生密度、护士密度、床位密度、药房人员、精神卫生人员。这五项是跨国研究里最常被引用的卫生系统指标,却只有 5 个代码。这就解释了 §6.6 里那个警告:接国家协变量时,人力与设施类指标的覆盖率天然薄弱。

§2.5 健康主题之外的五个前缀也计入 658

一个容易踩的细节:/topic/8/indicator 返回的 658 条里,SH. 前缀只占 485 条。剩下 173 条分属五个前缀:

前缀 条数 为什么算进健康主题
SP 143 人口(SP.POP 63 + SP.DYN 46 + SP.UWT 16 + 其余)——这是分母,健康率指标的分子分母都在这
SN 17 营养相关(SN.ITK.* 系列,主要是饥饿与食物不足)
SG 7 性别(孕产妇相关的性别指标)
SM 6 社会保护(健康保险覆盖)

这解释了一个常见困惑:为什么在健康主题里能看到 SP.POP.TOTL(总人口)?因为疾病负担不能只有分子。世行把分母放进了同一个主题下,这个设计是对的——只是让「健康 = SH.*」这个直觉不完全成立。

实用结论:想穷举健康主题指标,用 /topic/8/indicator,不要用 SH.* 过滤。后者会漏掉 173 条,占 26%。


§3 数据规格与取数通道

§3.1 三类端点

v2 API 的路径结构极其规整,全部基于同一个模板:

https://api.worldbank.org/v2/{entity}/{entity_id}/indicator/{indicator_id}?format=json&date={range}&per_page={n}
端点 用途 返回
/indicator 列全部指标 指标元数据数组(id / name / unit / source / sourceNote / sourceOrganization / topic)
/country 列全部国家与地区 实体元数据(id / iso2Code / name / region / adminregion / incomeLevel / lendingType / capitalCity / longitude / latitude)
/source 列全部数据源 源元数据(id / name / lastupdated / dataavailability / concepts)
/topic 列全部主题 主题元数据
/country/{iso3}/indicator/{id} 取某国某指标序列 两元素数组:[分页元数据, 观测数组]
/country/all/indicator/{id} 取全部国家某指标 同上

§3.2 响应结构(最容易踩的一处)

所有取数端点的响应不是一个平铺数组,而是一个二元组:

[
  {"page":1,"pages":2,"per_page":5,"total":6,"sourceid":"2","lastupdated":"2026-10-08"},
  [ {"indicator":{"id":"SH.DYN.MORT","value":"Mortality rate, under-5 (per 1,000 live births)"},
     "country":{"id":"CN","value":"China"},
     "countryiso3code":"CHN",
     "date":"2020",
     "value":7.5,
     "unit":"",
     "obs_status":"",
     "decimal":0},
    ...
  ]
]

第一个元素是分页元数据,第二个才是观测。 而且 lastupdated 藏在第一个元素里——这是全库唯一能看到「这个指标上次更新是哪天」的地方。

§3.3 单条观测的九个字段

字段 类型 说明 坑
indicator.id string 指标代码 每条观测都重复一遍,冗余度极高
indicator.value string 指标英文名 同上
country.id string ISO 2 位码(如 CN) 与 countryiso3code 不是同一个码
country.value string 国家名 随语言参数变化
countryiso3code string ISO 3 位码(如 CHN) 建模时应该用这个,不是 country.id
date string 年份 是字符串不是整数,排序会按字典序
value number \ null 观测值
unit string 单位 多数为空串
obs_status string 观测状态 非空表示该值有特殊说明
decimal integer 小数位 决定该指标的有效精度

§3.4 请求参数

参数 作用 默认
format json / xml xml
per_page 每页条数 50(不设会严重不够用)
page 页码 1
date 年份区间,2015:2020 或单年 2020 全部
source 按数据源过滤 全部
mrnev 1 = 只返回最近非空值 关闭
gaprec gap 填补 关闭
footnote 是否含脚注 n
language en / zh / fr / es / ar en

mrnev=1 是这个 API 里最有用的参数:只取最近的非空观测,一次请求就能拿到「各国最新值」表,不用自己处理年份错位。

§3.5 批量获取

单个指标全量:

curl -s "https://api.worldbank.org/v2/country/all/indicator/SH.DYN.MORT?format=json&per_page=20000&date=1960:2026"

注意 per_page=20000 是必要的。默认 50 条意味着要翻 300 多页。

大批量下载应走 Data Catalog(datacatalog.worldbank.org)而非 API:那里有 CSV / XLSX / SDMX 打包,比逐指标请求快得多。


§4 记录结构与字段字典

§4.1 一条记录是什么

一条记录 = 一个指标 × 一个国家 × 一个年份 = 一个数。

(SH.DYN.MORT, CHN, 2020) → 7.5

这不是「一条病例」,不是「一次调查」,不是「一个数据集」。它是三维点阵里的一个格点。理解这一点,后面所有操作才不会错位。

§4.2 三维点阵的稀疏性

理论上 29,544 指标 × 296 实体 × 66 年 = 5.86 亿个格点。实际远没有填满:

  • 1960 年只有少数几个指标(人口、GDP、预期寿命)有值
  • 多数健康指标从 1990–2000 年前后才开始有序列
  • 小国的指标缺失远多于大国
  • 部分指标已停止更新(如 Doing Business 源最后更新 2021-08-18)

推论:任何「把世行数据拼成宽表再扔进模型」的做法,必须先看缺失结构。直接 pivot + dropna 往往剩下不到原样本的 5%。

§4.3 国家实体的四个属性,决定能不能进模型

/country 返回的每个实体带四个分类字段:

字段 取值示例 用途
region.id EAS(东亚太平洋)、NA(不适用) 区域固定效应
adminregion EAP、LTE、空 仅发展中国家有值
incomeLevel HIC / UMC / LMC / LIC 收入分组,做分层分析的核心变量
lendingType IBD / IDX / NTR / LNX 世行贷款类别

关键判断:region.id == "NA" 且 incomeLevel.id == "NA" 的实体,是 aggregate 汇总项(World、Arab World、Euro area 等),必须剔除。296 里剔完剩下的才是真国家。

§4.4 指标元数据的六个有用字段

字段 说明
id 指标代码
name 英文全称
unit 单位(多数空)
source.id / source.value 所属数据源
sourceNote 口径注释——这个指标到底怎么算的
sourceOrganization 原始生产者——UNICEF / WHO / ILO / IMF,含上游 URL
topic 所属主题数组

sourceOrganization 是全库最被低估的字段。它明确告诉你:这个数字不是世行算的,是 UNICEF 算的,世行只是转载。做引用时该引谁,看这一行。

§4.5 CRS 与地理

/country 里带 longitude / latitude,可直接做地图。但注意:这些是首都坐标,不是质心。做空间插值时用它会引入系统偏差。

实测中还有一处更硬的问题:296 个实体里有 85 个的 longitude / latitude 为空。这些实体全部是 aggregate(汇总项),因为它们没有地理实体。这个数字反过来可以当判据用:

df["lon"].isna().sum()      # 85
(df["region"] == "NA").sum() # 79

两个数不等(85 ≠ 79),说明有 6 个真国家也缺坐标,也有若干 aggregate 被填了坐标。不要用「有没有经纬度」来判断是不是国家,判据只有一个:region.id == "NA"。

§4.6 指标元数据里三个容易被忽略的字段

除了前面说的 sourceNote 与 sourceOrganization,/indicator/{id} 还返回三个字段,在批量处理时很有用:

字段 类型 实际用途
unit string 多数为空串,但少数指标带明确单位(%、per 1,000)。不能靠它猜单位,因为大部分单位写在 name 里
topic array 一个指标可属于多个主题。所以按主题求和各指标会重复计数
source.id string 用于判断这个指标属于哪个源,进而查它的 lastupdated

topic 是一对多的,这一点要特别小心。 一个指标同时挂在 Health(8)、Gender(17)、Millennium development goals(18)下面是很常见的。如果你按主题拉指标清单再求并集,同一个指标会被算多次;如果要做「健康主题有多少指标」,必须用 /topic/8/indicator 的 total,而不是自己按 topic 字段过滤后计数。

unit 为空串不代表无单位。 世行的约定是:单位能写进指标名的,就不填 unit。比如 SH.DYN.MORT 的 name 是 “Mortality rate, under-5 (per 1,000 live births)”,单位在括号里。这意味着做自动化管道时必须解析 name,不能指望 unit 字段。这是与 SDMX 体系的标准接口对接时最容易出问题的一处。

§4.7 obs_status 的实际取值

obs_status 在多数观测里是空串,非空时表示有特殊说明。实测中常见的取值:

值 含义 处理建议
""(空) 正常观测 直接用
E 估算值(estimated) 可用于分析,但要在结果里标注
P 临时值(provisional) 下次更新可能变,不要当基准
F 预测值(forecast) 不能与历史观测混在一条时间序列里

最关键的是 F。 部分指标(尤其人口预测、GDP 预测)会把预测年份也放进同一个序列,只有靠 obs_status 才能区分。如果你的年份窗口包含了未来年份,会不知不觉把预测值当观测值建模,得到异常好的拟合。

一行代码防住:

df = df[df["status"] != "F"]        # 剔除预测值

§5 检索、抽样与泄漏分析

§5.1 用医学 AI 的眼光看这个数据集,泄漏在哪

世界银行数据本身不涉及「训练/测试」划分,但把它当协变量接进医学模型时,有三层泄漏风险:

第一层:时间泄漏。 用 2020 年的 SH.XPD.CHEX.GD.ZS 去预测 2018 年的结局,是把未来的信息喂给过去。世行的年度序列很容易让人忘记时间方向——因为它是按国家横向排的。

第二层:同一上游源。 世行的 SH.DYN.MORT 来自 UN IGME,而 GBD 的儿童死亡率也部分引用 IGME。两个「独立」变量其实同源。多变量模型里这会造成共线性虚高。

第三层:aggregate 混入。 把 WLD(世界)和各国一起放进去,等于让「全体之和」参与预测「个体」,这是最隐蔽的一种目标泄漏。

§5.2 抽样:怎么用最小的代价摸清一个指标

不建议一上来拉全量。推荐三步:

  1. 看元数据:/indicator/{id} —— 确认单位、口径、原始生产者
  2. 取最近值:/country/all/indicator/{id}?mrnev=1 —— 一次拿到各国最新,看覆盖了多少国家
  3. 取一个长序列:/country/CHN/indicator/{id}?date=1960:2026 —— 看时间覆盖是否连续

三步之后,这个指标的可用性就清楚了。

§5.3 覆盖率的真实形态

以 SH.DYN.MORT 为例,/country/CHN/indicator/SH.DYN.MORT?date=2015:2020 返回 total: 6,即 6 个年份全部有值。但这是中国。换成小国,同一个查询可能只返回 2–3 条。

不要用「指标存在」推断「数据可用」。 指标在目录里存在,不等于你要的国家有值。

§5.4 四个指标的 mrnev 实测

用 mrnev=1(每实体取最近非空值)一次请求拉全,四个常用指标的实测结果:

指标 有值实体数 最近值年份跨度
SH.DYN.MORT 五岁以下死亡率 244 全部 2024
SH.XPD.CHEX.GD.ZS 卫生支出占 GDP 241 2021–2024
SH.MED.PHYS.ZS 每千人医生数 256 1980–2023
SE.ADT.LITR.ZS 成人识字率 211 1970–2024

四行数据里有三个重要发现:

第一,实体数在 211–256 之间,不是 296。 减去 79 个 aggregate,理论上限是 217。SH.MED.PHYS.ZS 却返回 256,说明部分 aggregate 也有值——aggregate 不是空洞的占位,它们是有数的。这反过来印证了 §5.1 的第三层泄漏:aggregate 不只存在,还带着可用的数值,不剔除会真的被模型吃进去。

第二,「最近值」不是同一年。 SH.DYN.MORT 全部是 2024 年,看起来很整齐。但 SH.XPD.CHEX.GD.ZS 的最近值分布在 2021 到 2024 年之间——因为不同国家的上报节奏不同。这意味着你用它做横截面时,构造出来的是一张跨四年的混合表,而不是某一年的真实切片。

第三,SH.MED.PHYS.ZS 有一批实体的「最近值」停在 1980 年,SE.ADT.LITR.ZS 有一批停在 1970 年。 这些不是有数据,是四十年前有数据、之后再也没报过。mrnev=1 会老老实实把这些值返回给你,没有任何提示。

所以 mrnev=1 的正确用法是三字段连查:

d, meta = fetch_indicator("SH.MED.PHYS.ZS", use_mrnev=True)
# 关键:把「最近值年份」也带出来
recent = d.groupby("iso3")["year"].max()
stale = recent[recent < 2018]        # 六年没更新的实体
print(f"{(stale.index.size)} 个实体的最近值早于 2018")

必须验证「最近值」的「最近」到底有多近。 断言 mrnev=1 返回的都是新数据,会在做时间序列分析时引入长达四十年的错位。

§5.5 三步抽样法的实测节奏

回到 §5.2 的三步,实际耗时大致是:

步骤 请求 耗时
看元数据 /indicator/{id} < 1 秒
取最近值 /country/all/indicator/{id}?mrnev=1&per_page=20000 2–5 秒
取单国长序列 /country/CHN/indicator/{id}?date=1960:2026 < 1 秒

三步加起来不到十秒,就能判断一个指标能不能用。这个成本远低于「先拉全量再发现没法用」。 附录 E 给了可以直接粘贴运行的版本。


§6 AI 就绪指南

§6.1 最小可用流水线

import requests
import pandas as pd

BASE = "https://api.worldbank.org/v2"

def fetch_indicator(indicator_id, countries="all", date="1960:2026"):
    """取单个指标的全部观测,返回 DataFrame。"""
    rows, page = [], 1
    while True:
        r = requests.get(
            f"{BASE}/country/{countries}/indicator/{indicator_id}",
            params={"format": "json", "per_page": 20000,
                    "date": date, "page": page},
            timeout=60,
        )
        r.raise_for_status()
        meta = r.json()[0]
        data = r.json()[1] or []          # 越界页第二元素是 null,不是 []
        rows.extend(data)
        if page >= meta["pages"]:
            break
        page += 1

    df = pd.DataFrame([
        {
            "iso3":   x["countryiso3code"],
            "year":   int(x["date"]),
            "value":  x["value"],
            "status": x["obs_status"],
        }
        for x in rows
    ])
    return df, meta["lastupdated"]

注意三处:

  • r.json()[1] 可能是 None(不是空列表),要写 or []
  • per_page=20000 仍然可能需要翻页,因为 296 实体 × 66 年 ≈ 19,536,接近上限
  • meta["lastupdated"] 是这个指标的更新时间,应该存下来作为数据版本

§6.2 剔除 aggregate

c = requests.get(f"{BASE}/country", params={"format": "json", "per_page": 400}).json()[1]
df_c = pd.DataFrame(c)
real = df_c[df_c["region"].apply(lambda r: r["id"] != "NA")]
print(len(df_c), "→", len(real))     # 296 → 约 217

剩下的才是可建模的国家。

§6.3 拼宽表(正确姿势)

ids = ["SH.DYN.MORT", "SP.POP.TOTL", "SH.XPD.CHEX.GD.ZS", "NY.GDP.PCAP.CD"]
frames = []
for i in ids:
    d, _ = fetch_indicator(i)
    d = d.rename(columns={"value": i})
    frames.append(d[["iso3", "year", i]])

wide = frames[0]
for f in frames[1:]:
    wide = wide.merge(f, on=["iso3", "year"], how="outer")

# 关键:先看缺失结构再 dropna
print(wide.notna().mean())
wide_ok = wide.dropna(subset=["SH.DYN.MORT"])   # 只要求结局变量有值

不要用 how="inner"。世行指标年份边界不一致,inner join 会把你以为的 217 国 × 66 年砍成几十行。

§6.4 时序建模的三条纪律

  1. 按年切,不随机切。跨国面板数据的标准做法是留出最后 N 年做测试。
  2. 存 lastupdated。同一指标在不同时间拉取会有值变化(世行会回修历史数据),没有版本号的结果无法复现。
  3. 区分 null 与 0。SH.DYN.MORT = 0 是不可能值(死亡率不会为零),出现 0 说明该格点应该按缺失处理。

§6.5 一个完整的面板回归实操

这是把世行数据真正用起来的标准流程。研究问题示例:「卫生支出占 GDP 的比例,与五岁以下死亡率是否负相关?」

第一步:确定变量与年份窗口。

角色 指标代码 说明
结局 $y$ SH.DYN.MORT 五岁以下死亡率(每千活产)
主解释 $x$ SH.XPD.CHEX.GD.ZS 经常性卫生支出占 GDP
控制 NY.GDP.PCAP.CD 人均 GDP(取对数)
控制 SP.URB.TOTL.IN.ZS 城镇化率
控制 SE.ADT.LITR.ZS 成人识字率
固定效应 region.id 区域

窗口取 2000–2021。理由:这五个指标在这个窗口内才有较好的交叉覆盖;1970 年代的数据虽然存在,但健康支出口径在 2000 年前后有过一次修订。

第二步:取数与拼装。

import numpy as np
import statsmodels.formula.api as smf

IDS = {
    "mort":  "SH.DYN.MORT",
    "che":   "SH.XPD.CHEX.GD.ZS",
    "gdppc": "NY.GDP.PCAP.CD",
    "urban": "SP.URB.TOTL.IN.ZS",
    "lit":   "SE.ADT.LITR.ZS",
}

frames = {}
for k, code in IDS.items():
    d, _ = fetch_indicator(code, date="2000:2021")
    d = d[d["iso3"].isin(REAL_ISO3)]          # 已剔 aggregate 的 217 国
    # 同一 iso3-year 可能因分页重复,先去重
    d = d.drop_duplicates(subset=["iso3", "year"])
    frames[k] = d.rename(columns={"value": k})[["iso3", "year", k]]

df = frames["mort"]
for k in ["che", "gdppc", "urban", "lit"]:
    df = df.merge(frames[k], on=["iso3", "year"], how="left")

df["lgdppc"] = np.log(df["gdppc"])
df = df.merge(region_map, on="iso3", how="left")   # region_map: /country 的 region.id

第三步:看缺失结构,这一步不能跳。

print(len(df), df["mort"].notna().mean())
print(df[["mort","che","lgdppc","urban","lit"]].notna().mean())

典型结果会是这样:mort 覆盖 85–95%,che 覆盖 70–85%,lit 只有 50–70%。识字率是这里最薄的一环——它本来就是十年一报的指标,世行做了插值。

第四步:双向固定效应。

m = smf.ols(
    "mort ~ che + lgdppc + urban + lit + C(iso3) + C(year)",
    data=df.dropna(subset=["mort","che","lgdppc","urban","lit"]),
).fit(cov_type="cluster", cov_kwds={"groups": df.dropna(subset=["mort","che","lgdppc","urban","lit"])["iso3"]})

三个关键点:

  • C(iso3) 是国家固定效应,吸收了所有不随时间变的国别差异(地理、文化、制度底色)。
  • `C(year)`` 是年份固定效应,吸收了全球共同冲击(金融危机、疫情、全球医学进步)。
  • cov_type="cluster" 按国家聚类。同一国家的不同年份高度自相关,不聚类会严重低估标准误,把不显著做成显著。

第五步:解读的边界。

即使 che 的系数显著为负,也只能说「在控制国家不变特征与全球年份冲击后,卫生支出占比更高的国家死亡率更低」。不能说「提高卫生支出能降低死亡率」——那是因果推断的宣称,需要工具变量、断点、双重差分等设计,世行的观测数据本身不具备识别能力。

这一点是本条目反复强调的:它是协变量库,不是因果推断引擎。

§6.6 给影像模型接国家协变量

这是医学 AI 团队最实际的用法。场景:你有一个跨多国的影像数据集,想检验「模型在低收入国家表现更差」是不是因为当地医疗资源不足。

做法:给每个影像样本打上其采集国的国家层协变量。

# sample: 一条影像样本,字段含 country_iso3 与 study_year
row = wb_panel.loc[(sample["country_iso3"], sample["study_year"])]

features = {
    "che_pct_gdp":   row["SH.XPD.CHEX.GD.ZS"],   # 卫生支出占 GDP
    "phys_per_1k":   row["SH.MED.PHYS.ZS"],      # 每千人医生数
    "beds_per_1k":   row["SH.MED.BEDS.ZS"],      # 每千人床位
    "u5_mort":       row["SH.DYN.MORT"],         # 五岁以下死亡率(卫生系统效能的代理)
    "gdppc_log":     np.log(row["NY.GDP.PCAP.CD"]),
}

四个必须注意的点:

  1. 用 study_year,不要用当前年份。 2024 年发表的论文,影像采集可能是 2015 年。用 2024 年的卫生支出解释 2015 年的影像,是时间泄漏。
  2. 这三个人力/设施指标(医生密度、床位、支出)交叉覆盖率不高。 低收入国家恰恰是最缺这些数据的——这会让你在「资源最少」的那一端样本最少,形成系统性选择偏差。
  3. 不要把它们和目标标签一起放进模型。 死亡率与影像诊断标签可能共享上游(比如都反映疾病负担),会造成信息泄漏。正确做法是把它们当分层变量或协变量校正项,不是特征。
  4. 缺失就是信息。 一个国家连医生密度都没上报,这本身就说明其卫生统计能力弱。用 is_missing 指示变量显式建模,往往比插值更有解释力。

§6.7 常见清洗代码片段

剔除 aggregate 并建立 iso3 → region 映射:

def load_countries():
    c = requests.get(f"{BASE}/country",
                     params={"format": "json", "per_page": 400}, timeout=30).json()[1]
    rows = []
    for x in c:
        rows.append({
            "iso3":     x["id"],
            "iso2":     x["iso2Code"],
            "name":     x["name"],
            "region":   x["region"]["id"],          # "NA" = aggregate
            "income":   x["incomeLevel"]["id"],     # HIC/UMC/LMC/LIC,aggregate 为 "NA"
            "capital":  x.get("capitalCity", ""),
            "lon":      x.get("longitude"),
            "lat":      x.get("latitude"),
        })
    df = pd.DataFrame(rows)
    real = df[df["region"] != "NA"].copy()          # 296 → 217
    return df, real

安全翻页(处理 null 第二元素):

def paged(endpoint, params, max_pages=200):
    out, page = [], 1
    while page <= max_pages:
        p = dict(params, format="json", page=page)
        r = requests.get(endpoint, params=p, timeout=60)
        r.raise_for_status()
        body = r.json()
        if not isinstance(body, list) or len(body) < 2:
            break
        meta, data = body[0], body[1]
        if data is None:                            # 越界页返回 null
            break
        out.extend(data)
        if page >= int(meta.get("pages", 1)):
            break
        page += 1
    return out, (meta if out else {})

区分 null / 0 / 负值:

def flag_values(df, indicator_id):
    """按指标类型判断哪些数值不可能。"""
    ZERO_IMPOSSIBLE = ("MORT", "PREV", "INC")   # 死亡率、患病率、发病率不可能为零
    d = df.copy()
    d["is_null"] = d["value"].isna()
    if any(t in indicator_id.upper() for t in ZERO_IMPOSSIBLE):
        d["is_suspect_zero"] = d["value"].fillna(-1).eq(0)
    else:
        d["is_suspect_zero"] = False
    d["is_negative"] = d["value"].fillna(0) < 0
    return d

注意 is_negative:世行有些指标允许负值(增长率、净储蓄),但死亡率、人口数不可能为负。出现负值说明单位或口径有问题,应回查 sourceNote。


§7 质量评估、DAIMS 与口径交叉核对

§7.1 DAIMS 评估

维度 得分 说明
数据集标识 2/3 有稳定 API 与文档,但无单一条目的 DOI(批量下载有)
许可清晰度 3/3 默认 CC BY 4.0,另有 ODbL / Microdata Research License 两类例外,逐类写清
溯源信息 3/3 sourceOrganization 明确到 UNICEF / WHO 一级,含上游 URL
版本与更新 2/3 有 lastupdated,但无语义化版本号,历史版本不可回溯
字段字典 3/3 九个字段全部有明确语义与取值范围
缺失处理 1/3 有 obs_status,但缺失原因不细分
文档完备度 3/3 API 文档、指标元数据、许可页面三处齐全
访问稳定性 3/3 v2 API 十年未变结构,无需注册,无频率限制声明
合计 20/24 = 83.3% 本库较高分之一

§7.2 一处必须说清的口径

29,544 个指标不等于 29,544 份数据。

指标(indicator)与数据源(source)是多对一。World Development Indicators 一个源就有 1,498 个指标(/indicator?source=2 的 total)。71 个源平均每个约 416 个指标。

这个区别重要,因为同一份底层数据可以派生出很多指标。比如「五岁以下死亡率」这一个数,在不同源里可能同时存在为 SH.DYN.MORT(每千人)、SH.DYN.MORT.MA(模型估计)、按性别分的 .FE / .MA 变体。它们不是不同的测量,是同一测量的不同切法。

§7.3 交叉核对:世行 vs WHO vs GBD

同一个五岁以下死亡率,三个地方都有:

来源 口径 差异来源
世行 SH.DYN.MORT 转自 UN IGME 与 IGME 完全一致(转载)
WHO GHO WHO 自己的发布 多数年份与 IGME 一致,边角年份有微差
GBD IHME 建模估计 方法不同,可差 10–20%

结论:世行的健康指标在多数情况下不是独立观测,是 UN IGME 的镜像。引用时说「据世界银行数据」在技术上没错,但更准确的说法是「据 UN IGME,经世界银行分发」。


§8 生态位与相邻资源

§8.1 为什么这一层非它不可

跨国健康研究的数据源有四个层级:

第一层:原始调查(DHS、MICS、各国普查)        —— 个体级,需申请,覆盖不均
第二层:机构汇总(WHO GHO、UNICEF、ILO)       —— 指标级,单一领域深
第三层:编目分发(世界银行、OWID)             —— 指标级,跨领域宽
第四层:建模估计(GBD、IHME)                  —— 网格级,有推算

世行在第三层。它的不可替代性不在深,在宽——同一套主键下同时有健康、经济、教育、环境、贫困的指标。要做「健康的社会决定因素」这类研究,只有这一层能一次拿全。

§8.2 与 OWID 的分工

our-world-in-data(rid 185)摘要明确写「汇集 WHO、UN、世界银行、IHME 等来源并标准化」。

两者关系类似「镜像 vs 原件」:

世界银行 OWID
角色 一手编目 + 分发 二次汇编 + 标准化
优势 观测未经改写,可追到原始生产者 跨源口径已对齐,可直接画图
劣势 各源口径不统一,需自己处理 改写过程不透明,无法回溯单项
适用 需要原始观测做统计建模 需要快速做可视化或跨源比较

两者不是变体关系,是同一条数据链的上下游。

§8.3 与 WHO GHO 的分工

who-gho(rid 175)是 WHO 自己的指标库,健康指标比世行更细(有更多按年龄、按病因的切分),但只有健康。

世行的健康指标多数转自 WHO 与 UN 机构。所以:要深度用 GHO,要宽度用世行,要原始生产者信息两边都看 sourceOrganization。

§8.4 71 个数据源,按活性分四档

这是本条目最重要的一个实操结论:71 个源不是一个整体,它们的活性差十倍以上。 按 /source 返回的 lastupdated 分组:

档位 判据 源数 代表
活跃 2025–2026 更新 31 WDI、Gender Statistics、Health Nutrition and Population Statistics、PEFA 系列、ICP 2021
维护 2022–2024 更新 18 Education Statistics、Human Capital Index、Statistical Performance Indicators、Wealth Accounts
半冻结 2019–2021 更新 14 Universal Health Coverage(2019-04)、Health Equity and Financial Protection Indicators(2019-11)、Doing Business(2021-08)
停更 2013–2018 更新 8 Africa Development Indicators(2013-02)、Subnational Poverty(2015-04)、Millennium Development Goals(2018-09)

具体分布:

2026 █████████████████████ 21
2025 ██████████ 10
2024 █████ 5
2023 ██ 2
2022 ██████ 6
2021 ████ 4
2020 ██████ 6
2019 ██████ 6
2018 ███ 3
2017 █ 1
2016 ███ 3
2015 ██ 2
2013 ██ 2

44% 的源在过去两年内更新过,但在 2018 年及以前停更的仍有 8 个。 这意味着:随手挑一个指标代码,有接近一成概率它已经死了。

§8.5 与健康相关的十一个源

71 个源里有 11 个直接关系健康或健康的社会决定因素。要建健康研究的数据底盘,优先看这些:

id 源 最后更新 在本条目里的位置
2 World Development Indicators 2026-10-08 主源,1,498 指标,健康指标多数在这里
16 Health Nutrition and Population Statistics 2026-07-01 健康专门源,比 WDI 里的健康切片更全
40 Population estimates and projections 2026-07-01 分母的唯一权威来源
14 Gender Statistics 2026-10-14 孕产妇、生殖健康、性别差距
39 HNP Statistics by Wealth Quintile 2022-12-13 按财富五分位拆的健康指标,做健康不平等必用
65 Health Equity and Financial Protection Indicators 2019-11-06 健康公平专项,已半冻结
58 Universal Health Coverage 2019-04-25 UHC 服务覆盖指数,已半冻结
88 Food Prices for Nutrition 2026-07-21 营养可负担性,新的健康社会决定因素
92 Disability Data Hub 2025-03-28 残障相关指标
46 Sustainable Development Goals 2022-07-22 SDG 3 的官方对齐口径
19 Millennium Development Goals 2018-09-19 已停更,仅用于历史衔接

两个要特别记住的:

  • 源 39(按财富五分位) 是全库少见的、把健康指标拆到社会经济分层的源。想做「同一个国家里穷人 vs 富人的健康差距」,只有这里能一次拿到。但它是 2022-12 的,不算活跃。
  • 源 16 是 WDI 的健康超集。同一个指标名可能在 2 和 16 里同时存在,取值相同但更新日期不同。取数时如果指定了 source,要确认拿到的是你要的那一版。

§8.6 两个「看起来很诱人但不能用」的源

源 1:Doing Business。 2021-08-18 之后永久停更,且世行内部审计后已下架原数据集的对外发布。代码里还能查到当时的指标,但不应用于任何新研究。

源 11:Africa Development Indicators。 2013-02-22 停更。它曾是非洲研究的主力源,现在数据已被 WDI 吸收且部分口径变了。直接用它做时间序列会在 2013 年断掉。

这两个源的共同特征是:API 里还在,total 也有值,但序列已经冻结。 这正是 §11 里「指标在目录里,数据就能用」这个误读的典型受害者。

§8.7 与 GBD / IHME 的关系

gbd 条目(Global Burden of Disease)与本条目的关系最容易被误解:

世界银行 GBD / IHME
数值来源 各国上报 + UN 机构估算 协变量建模(DisMod-MR 等)
粒度 国家 × 年 国家 × 年 × 年龄 × 性别,部分到次国家
不确定性 不发布置信区间 每个估计都带 95% UI
覆盖 指标宽,健康指标浅 健康指标极深,无经济/教育
一致性 各源口径不一 内部自洽(同一模型体系)

实用判断:如果你要「这个国家儿童死亡率多少」这个数,两者都能给,但会有 10–20% 的差。差在哪?世行转的是 UN IGME 的观测+插值,GBD 是全协变量模型输出。写论文时必须说清你用的是哪一套,不能混用。

反过来,GBD 没有的东西正是世行的强项:人均 GDP、卫生支出占比、医生密度、贫困率。这两套是互补的,不是替代的。


§9 许可、伦理与合规

§9.1 三套许可,按数据类别分

世行的许可不是一套,是按数据类别分的三套 + 两类特殊情形:

类别 许可 关键约束
世行自有开放数据(默认) CC BY 4.0 可商用、可修改、可再分发,只需署名 + 标注修改
数据伙伴要求时 ODbL 署名 + 衍生库须同许可 + 开放格式
微数据(Microdata Library) World Bank Microdata Research License 不得再分发、仅限统计科研、禁止反查个体、禁止跨库链接
第三方外部数据 License Specified Externally 需去外部站点单独同意
定制许可 Custom License 看元数据里的 license 字段

§9.2 CC BY 4.0 的附加条款(容易被忽略)

CC BY 4.0 是标准许可,但世行加了强制争议解决条款:

Any and all disputes arising under this License that cannot be settled amicably shall be resolved in accordance with the following procedure: … non-binding mediation … arbitration …

即:争议先调解,45 天未解决则提交仲裁,仲裁地为世行总部所在地(华盛顿),语言为英语。

对医学 AI 项目的实际影响:如果产品要商用且涉及大规模再分发,这条要过一遍法务。纯研究用途基本无碍。

§9.3 微数据是完全不同的东西

本条目讲的是指标级数据(国家 × 年)。世行还有一个 Microdata Library,里面是个体级调查数据(DHS、LSMS 等)。

两者容易混淆,但许可天差地别:

指标数据 微数据
粒度 国家 × 年 个体 / 户
许可 CC BY 4.0(可商用) Microdata Research License(不可再分发)
获取 直接 API 注册 + 申请,分 Direct / Public use / Licensed 三档
能否进训练集 能 需逐份看条款,多数不能直接进

这一条对医学 AI 团队最关键:如果你要找个体级健康调查数据,别在世界银行 API 里找,去 Microdata Library,但那里的数据不能随意进训练集。

§9.4 伦理

本条目涉及的数据全部是国家聚合统计,不含任何个体。伦理风险不在隐私,在误用:

  • 把小国的高死亡率当作「该国医疗水平差」的证据 —— 忽略了人口基数与统计能力
  • 用 aggregate 实体做国家间比较 —— 会得到无意义的结论
  • 把估算值当观测值 —— 多数健康指标是 UN 机构估算,不是逐例登记

§10 复用边界与三条硬结论

§10.1 三条硬结论

结论一:这是协变量库,不是结局库。

医学 AI 里,世行数据最有价值的用法是当国家层协变量:给每个样本打上「来自哪个国家、那个国家医疗支出多少、医生密度多少」。而不是拿它当预测目标。用它预测健康结局,你得到的是宏观趋势,不是临床信号。

结论二:29,544 个指标里,真正能用于建模的远少于这个数。

大部分指标有严重缺失、口径断裂或已停更。真正可用的路径是:先定研究问题 → 反推需要哪 3–8 个指标 → 逐个验证覆盖年份 → 拼宽表。不要反过来,先拉 29,544 个再挑。

结论三:必须先剔 aggregate。

296 个实体里混着 World、Arab World、Euro area 等汇总项。不剔,任何跨国统计都会重复计数。判据是 region.id == "NA"。

§10.2 该用 / 不该用

场景 该不该用
给影像样本打国家协变量 ✅ 强烈推荐
做健康公平 / 社会决定因素研究 ✅ 推荐(注意区分估算与观测)
预测某国明年死亡率 ⚠️ 谨慎(属于宏观预测,非本库设计目的)
找个体级调查数据 ❌ 用 Microdata Library,不是这里
当临床决策依据 ❌ 绝对不行(国家聚合,与个体无关)
找疾病影像或病历 ❌ 这里没有

§10.3 一个判断标准:这个指标该不该进你的模型

面对 29,544 个指标,反复问四个问题,能筛掉九成以上:

问题一:它是结局还是协变量? 你想要预测的东西不能当特征。世行数据在当地国家层面上全是"背景",所以在绝大多数医学 AI 场景里它只能是协变量。只有研究问题本身就是宏观的(比如"哪些国家卫生系统效率更高"),它才是结局。

问题二:它的年份能对上你的样本吗? 你的影像样本采集于 2016 年,而该国这个指标的最近值是 2011 年——还有五年缺口。时间不匹配的协变量比没有协变量更糟,因为它带来的是有偏信息,而且很难在事后发现。

问题三:它的缺失和你的样本相关吗? 低收入国家的数据缺失最严重,而低收入国家往往正是你模型表现最差的那部分。这构成非随机缺失(MNAR),不是随手插值能解决的。要么显式建缺失指示,要么把「数据可得性」本身当作分析对象。

问题四:它和别的特征同源吗? SH.DYN.MORT 来自 UN IGME,SH.XPD.CHEX.GD.ZS 来自 WHO GHED,两者独立。但如果你同时放了世行的死亡率和 GBD 的死亡率,那是同一批底层数据的两个版本,会造成共线性虚高。

四个问题都过了,这个指标才值得写进管道。

一个反例帮助记住这套标准:假设你要研究「贫困是否导致儿童营养不良」。SI.POV.DDAY 与 SH.STA.STNT.ZS 都过了前两问(都是协变量/结局,年份也对得上),但第三问就暴露问题——两个指标的缺失模式高度相似,都集中在撒哈拉以南非洲的低收入小国。这意味着有效样本恰恰排除了你最关心的那部分人群,得出的结论只适用于「有统计能力的国家」,不能外推。识别出这一点,比跑出一个显著系数更重要。

§10.4 一句话记住它

世界银行开放数据把「国家」变成一行,让你能把宏观背景 join 到任何一条医学数据上。


§11 常见误读

这一节把散落在前文的坑集中一次讲清。每一条都对应一次实测中的真实失败——不是理论风险。

常见说法 实际情况 证据
「世行有 29,544 个数据集」 29,544 是指标数,底下只有 71 个数据源。WDI 一个源就占 1,498 个指标 /source 的 total=71;/indicator?source=2 的 total=1498
「296 个国家和地区」 296 里 217 个是真国家,79 个是 World / Arab World / Euro area / IBRD-only 这类汇总项 /country 里 region.id == "NA" 的正好 79 条
「响应是个数组,直接 for 就完了」 响应是二元组 [分页元数据, 观测数组]。直接 for x in r.json() 会先把元数据当观测 len(r.json()) == 2,第一元素是字典不是列表
「翻到最后一页得到空列表」 越界页第二元素是 null,不是 []。rows.extend(None) 会抛 TypeError /country/CHN/indicator/SH.DYN.MORT?page=99 返回 [meta, null]
「不设 per_page 也能拉全」 默认 50。296 实体 × 66 年 ≈ 19,536 条,要翻 300 多页 官方文档;实测 per_page=5 时 pages=2 对 6 条数据
「用 country.id 做主键」 country.id 是 ISO2(CN),countryiso3code 是 ISO3(CHN)。和别的表 join 用错就是全空 单条观测里两个字段同时存在且值不同
「value 为 0 就是 0」 部分指标里 0 是不可能值(死亡率不会为零),出现 0 多半是上报占位。而 null 是真的没有 SH.DYN.MORT 的中国序列无 0 值;小国序列有 0
「date 是数字,可以直接排」 date 是字符串。字典序排 “999” > “1000”,混入非四位年份时会乱序 type(x["date"]) == str
「指标在目录里,数据就能用」 目录收录 ≠ 有观测。Doing Business 源最后更新 2021-08-18 且已停更;很多指标只有零星国家有值 /source 的 lastupdated
「inner join 最安全」 世行指标年份边界极不一致,how="inner" 会把 217 国 × 66 年砍成几十行 不同指标 dataavailability 差异极大
「fillna(0) 补齐缺失」 等于把「未报告」写成「值为零」。在死亡率、患病率类指标上是灾难性错误 value 的 null 占比在很多指标里超过 50%
「引用时写『据世界银行数据』」 多数健康指标的原始生产者是 UN IGME / WHO / UNICEF,世行只是编目与分发方 /indicator/{id} 的 sourceOrganization 字段

每一行背后的展开:

「世行有 29,544 个数据集」 —— 这是最常见的层级错认。指标(indicator)与数据源(source)是多对一:71 个源平均每个约 416 个指标,WDI 一个源就有 1,498 个(/indicator?source=2 的 total)。而且同一份底层测量会派生出多个指标代码,比如五岁以下死亡率同时存在 SH.DYN.MORT(每千人)、按性别分的 .FE / .MA 变体。它们不是不同的测量,是同一测量的不同切法。

「296 个国家和地区」 —— /country 的 total 是 296,但按 region.id == "NA" 一筛,只剩 217 条。剔掉的 79 条里有 WLD(世界)、ARB(阿拉伯世界)、EMU(欧元区)、BHI(高收入 IBRD 国家)这类汇总。它们不是「国家」,是集合。把它们和真国家放一张表里做统计,等于把同一批人算两次以上。

「响应是个数组」 —— 这是新手最先撞的一堵墙。真实的响应结构永远是二元组:[0] 是分页元数据(page / pages / per_page / total / lastupdated),[1] 才是观测数组。for x in r.json() 得到的第一个 x 是个字典,下一个 x 才是列表,第三个就没有了。

「翻到最后一页得到空列表」 —— 越界请求不报错,返回 [{"page": 99, "pages": 2, ...}, null]。很多翻页模板写 for x in r.json()[1] or [] 就对了,写 if r.json()[1] == []: break 就会在 None 上崩。这是本条目反复强调的一处细节,因为它在批量采集时必然发生。

「不设 per_page 也能拉全」 —— 默认值是 50。单个指标全量约 296 × 66 ≈ 19,536 条,按 50 一条要翻 300 多页。更麻烦的是每页都要经过一次 CDN 往返,实际耗时以分钟计。per_page=20000 是必须加的。

「用 country.id 做主键」 —— 一条观测里同时有 country.id(ISO2,如 CN)和 countryiso3code(ISO3,如 CHN)。世行自己的 /country 端点用 ISO3 作 id,但观测里的 country.id 却是 ISO2。这种不一致会让人在和外部表(DHS、GBD、ISO 国家表)join 时全表落空,而且不报错——只是结果为空。

「value 为 0 就是 0」 —— 有两类 0 要分清:真 0(如某地某年疟疾死亡数确实是 0)和占位 0(本该缺失却填了 0)。判据是指标语义:死亡率、患病率、医生密度不可能是 0。而 null 是明确的「没有这个值」。三种状态必须分开存:null / 0 / 有值。

「date 是数字」 —— 它是四位字符串。绝大多数排序不受影响,但一旦出现非标准年份(世行个别指标有季度或半年数据,date 形如 "2020Q1"),按字典序就会乱。稳妥做法:int(x["date"][:4])。

「指标在目录里,数据就能用」 —— 目录是历史累积的,收录过的指标不会因为停更而被删掉。71 个源里有 8 个在 2018 年及以前就停更了,另有 14 个在 2019–2021 之间冻结。随手挑一个指标代码,有接近一成概率它已经死了。

「inner join 最安全」 —— 直觉上 inner 最干净,实际上最危险。世行各指标的年份边界极不一致:死亡率可能到 2023 年,卫生支出只到 2021 年,识字率五年才报一次。how="inner" 会取交集,把 217 国 × 66 年砍成几十行,而且你看不到被砍掉的部分。正确做法是 how="outer" 拼完后打印 notna().mean(),先看缺失结构再决定。

「fillna(0) 补齐缺失」 —— 在死亡率指标上,这等于断言「这个国家这一年没有儿童死亡」。这是本条目见过最严重的误用。缺失就是缺失,要么剔除,要么用 obs_status 标记,要么显式建 is_missing 指示变量。

「据世界银行数据」 —— 技术上没错(世行确实是分发方),但会误导读者以为是世行算的。准确写法是看 sourceOrganization:五岁以下死亡率应写「据 UN IGME,经世界银行分发」。这个字段就在 /indicator/{id} 的响应里,一眼可见。

§11.2 四类容易忽略的结构性问题

第一,lastupdated 藏在分页元数据里,不在观测里。

[{"page":1,"pages":2,"per_page":5,"total":6,"sourceid":"2","lastupdated":"2026-10-08"}, [...]]

这个字段是唯一能判断「这批数字是哪天冻结的」的线索。丢掉第一元素,你的结果就无法复现——因为世行会回修历史序列。

第二,85 个实体没有经纬度。

296 个实体里 85 个的 longitude / latitude 为空。做地图时这些点会掉到 (0,0)。而且有坐标的那些,给的是首都坐标,不是国土质心——做空间插值会引入系统偏差。

第三,同一年份不同指标的「最新年」不一样。

SH.DYN.MORT 的中国序列到 2023 年,SH.XPD.CHEX.GD.ZS 可能只到 2021 年。这意味着「取最新一年做横截面」这件事,对不同指标取到的是不同年份。要么显式指定年份,要么接受错位。

第四,aggregate 里有嵌套层级。

79 个汇总实体不是平级的:WLD(世界)包含 AFR(非洲),AFR 又包含 SSF(撒哈拉以南非洲),下面才是国家。如果为了「看区域趋势」把多层汇总和国家混在一张表里求和,同一批人口会被算三次以上。

§11.3 上线前的九项自检

  • [ ] 用的是 r.json()[1] or [],不是 r.json()[1]
  • [ ] 主键用 countryiso3code,不是 country.id
  • [ ] 剔掉了 region.id == "NA" 的 79 个实体
  • [ ] 记录了每个指标的 lastupdated
  • [ ] value 的 null 与 0 分开处理
  • [ ] date 转成了整数再排序
  • [ ] 拼宽表用 how="outer",并打印 notna().mean() 看缺失结构
  • [ ] 明确指定了分析年份,而不是「各取最新」
  • [ ] 引用时写的是原始生产者,不是「世界银行」

附录 A:本篇数字速查

数字 值 来源
指标总数 29,544 /indicator?per_page=1 的 total
国家与地区数 296 /country?per_page=1 的 total
数据源数 71 /source?per_page=1 的 total
主题数 21 /topic 返回长度
健康主题指标数 658 /topic/8/indicator 的 total
World Development Indicators 指标数 1,498 /indicator?source=2 的 total
WDI 最后更新 2026-10-08 /source 的 lastupdated
健康营养人口统计源最后更新 2026-07-01 同上
时间起点 1960 官方口径
默认许可 CC BY 4.0 Data Access And Licensing
默认 per_page 50 官方文档
DAIMS 20/24 = 83.3% 本条目评估

附录 B:五个可复制的查询

# 1. 指标总数(最轻的一次握手)
curl -s "https://api.worldbank.org/v2/indicator?format=json&per_page=1" | python3 -c "import sys,json;print(json.load(sys.stdin)[0]['total'])"
# 29544

# 2. 各国某指标的最新值(mrnev=1 是关键)
curl -s "https://api.worldbank.org/v2/country/all/indicator/SH.DYN.MORT?format=json&per_page=20000&mrnev=1"

# 3. 单国长序列
curl -s "https://api.worldbank.org/v2/country/CHN/indicator/SH.DYN.MORT?format=json&per_page=100&date=1960:2026"

# 4. 指标元数据(看 sourceOrganization)
curl -s "https://api.worldbank.org/v2/indicator/SH.DYN.MORT?format=json"

# 5. 国家分类(region / incomeLevel,用于剔 aggregate)
curl -s "https://api.worldbank.org/v2/country?format=json&per_page=400"

附录 C:核对清单

拿到数据后按顺序跑:

  • [ ] 用 /country 剔掉 region.id == "NA" 的 aggregate 实体
  • [ ] 检查每个指标的年份覆盖(total / 实得条数)
  • [ ] 检查 obs_status 非空的占比,这些值要打标
  • [ ] 确认 value 的 null 占比,别用 fillna(0)
  • [ ] 记录每个指标的 lastupdated 作为版本号
  • [ ] 确认用的是 countryiso3code 而不是 country.id
  • [ ] 时序划分按年切,不随机切
  • [ ] 如果引用了健康指标,引用原始生产者(UN IGME / WHO),不是「世界银行」

附录 D:三十个指标代码速查

做跨国健康研究最常用的指标,按用途分五组。全部可在 api.worldbank.org/v2/country/all/indicator/{代码} 直接取。

组一:结局变量(健康结果)

代码 名称 单位
SH.DYN.MORT 五岁以下死亡率 每千活产
SH.DYN.NMRT 新生儿死亡率 每千活产
SP.DYN.LE00.IN 出生时预期寿命 年
SH.DYN.MORT.MA 五岁以下死亡率(男性) 每千活产
SH.DYN.MORT.FE 五岁以下死亡率(女性) 每千活产
SH.MMR.RISK 终身孕产死亡风险 1 in N
SH.TBS.INCD 结核发病率 每 10 万
SH.HIV.INCD.ZS HIV 感染率(15–49 岁) %
SH.STA.STNT.ZS 五岁以下发育迟缓率 %
SH.SVR.WAST.ZS 五岁以下重度消瘦率 %

组二:分母与人口结构

代码 名称 单位
SP.POP.TOTL 总人口 人
SP.POP.0014.TO.ZS 0–14 岁人口占比 %
SP.POP.65UP.TO.ZS 65 岁以上人口占比 %
SP.URB.TOTL.IN.ZS 城镇化率 %
SP.DYN.TFRT.IN 总和生育率 每妇女
SP.DYN.CBRT.IN 粗出生率 每千人

组三:卫生投入与系统能力

代码 名称 单位
SH.XPD.CHEX.GD.ZS 经常性卫生支出占 GDP %
SH.XPD.CHEX.PC.CD 人均经常性卫生支出 现价美元
SH.XPD.OOPC.CH.ZS 自付费用占卫生支出 %
SH.MED.PHYS.ZS 每千人医生数 每千人
SH.MED.BEDS.ZS 每千人床位数 每千人
SH.UHC.SRVS.CV.XD UHC 服务覆盖指数 0–100
SH.IMM.MEAS 麻疹疫苗覆盖率 %
SH.H2O.SMDW.ZS 安全饮水可及率 %

组四:社会经济决定因素

代码 名称 单位
NY.GDP.PCAP.CD 人均 GDP 现价美元
NY.GDP.PCAP.PP.CD 人均 GDP(PPP) 现价国际元
SI.POV.DDAY 每日 1.90 美元以下人口比 %
SI.POV.GINI 基尼系数 0–100
SE.ADT.LITR.ZS 成人识字率 %
SE.SEC.ENRR 中学毛入学率 %

组五:环境与治理

代码 名称 单位
EN.ATM.PM25.MC.M3 PM2.5 年均浓度 μg/m³
EN.ATM.GHGT.KT.CE 温室气体排放 kt CO₂e
EG.ELC.ACCS.ZS 电力可及率 %
GE.EST 政府效能(WGI) 约 −2.5 至 2.5

使用提示:第二组的六个分母指标覆盖率最高(几乎全球全覆盖、年份最长),可以放心当基准表。第三组的五个(支出、医生、床位、UHC、疫苗)覆盖率最低,正是 §6.6 提到的选择性缺失重灾区。

附录 E:一次最小可复现实验

十五分钟内能跑完,用来验证本条目所有关于响应结构的断言。

# 1. 看响应是不是二元组
curl -s "https://api.worldbank.org/v2/country/CHN/indicator/SH.DYN.MORT?format=json&per_page=5" \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(type(d), len(d), type(d[0]), type(d[1]))"
# 期望: <class 'list'> 2 <class 'dict'> <class 'list'>

# 2. 看越界页第二元素是不是 null
curl -s "https://api.worldbank.org/v2/country/CHN/indicator/SH.DYN.MORT?format=json&page=999" \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['pages'], repr(d[1]))"
# 期望: 1 None

# 3. 看 aggregate 有多少
curl -s "https://api.worldbank.org/v2/country?format=json&per_page=400" \
  | python3 -c "import sys,json; r=json.load(sys.stdin)[1]; print(len(r), sum(1 for x in r if x['region']['id']=='NA'))"
# 期望: 296 79

# 4. 看 date 是不是字符串
curl -s "https://api.worldbank.org/v2/country/CHN/indicator/SH.DYN.MORT?format=json&per_page=1" \
  | python3 -c "import sys,json; d=json.load(sys.stdin)[1][0]; print(type(d['date']), type(d['value']))"
# 期望: <class 'str'> <class 'float'>

# 5. 看 lastupdated 在哪
curl -s "https://api.worldbank.org/v2/country/CHN/indicator/SH.DYN.MORT?format=json&per_page=1" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)[0])"
# 期望: 含 sourceid / lastupdated 的字典

跑完这五条,本条目 §3.2、§3.3、§4.3、§11 里的每条断言都被独立验证过一遍。


相关数据集导航

以下为站内 AI-Ready 数据集百科中与本词条共享多个主题标签的相关数据集,按相关度降序排列:

  • oecd-health — 共享标签:公共卫生与流行病学 / 电子健康记录 / 评测基准
  • ucdp — 共享标签:公共卫生与流行病学 / 电子健康记录 / 评测基准
  • mimicel — 共享标签:公共卫生与流行病学 / 电子健康记录
  • mimiciii-demo — 共享标签:公共卫生与流行病学 / 电子健康记录
  • mimiciv — 共享标签:公共卫生与流行病学 / 电子健康记录
  • armd-mgb — 共享标签:公共卫生与流行病学 / 电子健康记录 / 评测基准
  • amr-uti — 共享标签:公共卫生与流行病学 / 电子健康记录 / 评测基准
  • lacuna — 共享标签:电子健康记录 / 多模态 / 评测基准
  • sdoh-adverse-pregnancy — 共享标签:公共卫生与流行病学 / 电子健康记录 / 评测基准
  • clif — 共享标签:电子健康记录 / 多模态 / 评测基准

导航说明:本章节由全站统一标签体系自动计算生成(标签重合度算法),双向可达;点击链接可跳转至对应数据集词条。

返回 AI-Ready 数据集