信息速览
§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 抽样:怎么用最小的代价摸清一个指标
不建议一上来拉全量。推荐三步:
- 看元数据:
/indicator/{id}—— 确认单位、口径、原始生产者 - 取最近值:
/country/all/indicator/{id}?mrnev=1—— 一次拿到各国最新,看覆盖了多少国家 - 取一个长序列:
/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 时序建模的三条纪律
- 按年切,不随机切。跨国面板数据的标准做法是留出最后 N 年做测试。
- 存
lastupdated。同一指标在不同时间拉取会有值变化(世行会回修历史数据),没有版本号的结果无法复现。 - 区分 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"]),
}
四个必须注意的点:
- 用 study_year,不要用当前年份。 2024 年发表的论文,影像采集可能是 2015 年。用 2024 年的卫生支出解释 2015 年的影像,是时间泄漏。
- 这三个人力/设施指标(医生密度、床位、支出)交叉覆盖率不高。 低收入国家恰恰是最缺这些数据的——这会让你在「资源最少」的那一端样本最少,形成系统性选择偏差。
- 不要把它们和目标标签一起放进模型。 死亡率与影像诊断标签可能共享上游(比如都反映疾病负担),会造成信息泄漏。正确做法是把它们当分层变量或协变量校正项,不是特征。
- 缺失就是信息。 一个国家连医生密度都没上报,这本身就说明其卫生统计能力弱。用
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 — 共享标签:电子健康记录 / 多模态 / 评测基准
导航说明:本章节由全站统一标签体系自动计算生成(标签重合度算法),双向可达;点击链接可跳转至对应数据集词条。

