File size: 29,785 Bytes
ebaf8c2 c140718 ebaf8c2 c140718 6055ba8 c140718 ebaf8c2 c140718 ebaf8c2 c140718 4ccac56 6055ba8 4ccac56 d624b50 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 4ccac56 6055ba8 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 d624b50 c140718 4ccac56 6055ba8 4ccac56 6055ba8 4ccac56 6055ba8 ebaf8c2 c140718 ebaf8c2 c140718 d624b50 4ccac56 ebaf8c2 4ccac56 c140718 4ccac56 6055ba8 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 c140718 d624b50 4ccac56 6055ba8 ebaf8c2 c140718 ebaf8c2 c140718 ebaf8c2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | #!/usr/bin/env python3
"""Reproducible public-data snapshot for ASCEND (all World Bank economies); no synthetic fallback.
Usage:
python3 refresh_data.py # rebuild data.json from live public APIs
python3 refresh_data.py --validate # re-validate the committed data.json
Optional: if geo/access_summary.json exists (produced by access_pipeline.py), its
modelled geographic-access indicators are merged as a separate, clearly labelled group.
"""
from pathlib import Path
import argparse, concurrent.futures, datetime, gzip, html, json, math, re, time
import urllib.request, urllib.parse
BASE = Path(__file__).resolve().parent
DEST = BASE / 'data.json'
RAW = BASE / 'data_raw'
ACCESS = BASE / 'geo' / 'access_summary.json'
YEARS = (2000, 2024)
VERSION = '2.3.0'
WB_NOTE = 'World Bank open data (CC BY 4.0); indicator attribution retained. Check dataset terms before redistribution.'
WHO_NOTE = 'WHO Global Health Observatory open data (CC BY-NC-SA 3.0 IGO); attribution retained.'
EPMC_NOTE = 'Europe PMC bibliographic service. Article and abstract rights remain with their respective holders; linked records retain identifiers and attribution.'
CT_NOTE = 'ClinicalTrials.gov public registry data; sponsor submissions are not independent verification of efficacy. See ClinicalTrials.gov terms and conditions.'
GOV_NOTE = 'Official Brazilian Ministry of Health public API. Connector proof only; schema field names retained, no individual patient records distributed.'
# key -> definition. `source` is 'worldbank' or 'who'. `scale` guides chart axes. `target` is the
# Lancet Commission on Global Surgery 2030 target where one exists (Meara et al., Lancet 2015).
INDICATORS = {
'spending': {'id':'SH.XPD.CHEX.PC.CD','source':'worldbank','label':'Current health expenditure per capita','short':'Health spending / person','unit':'current US$','decimals':0,'scale':'log','better':'higher'},
'spending_ppp': {'id':'SH.XPD.CHEX.PP.CD','source':'worldbank','label':'Current health expenditure per capita, PPP','short':'Health spending / person (PPP)','unit':'international $','decimals':0,'scale':'log','better':'higher'},
'physicians': {'id':'SH.MED.PHYS.ZS','source':'worldbank','label':'Physicians','short':'Physicians','unit':'per 1,000 people','decimals':2,'scale':'linear','better':'higher'},
'life': {'id':'SP.DYN.LE00.IN','source':'worldbank','label':'Life expectancy at birth','short':'Life expectancy','unit':'years','decimals':1,'scale':'linear','better':'higher'},
'oop': {'id':'SH.XPD.OOPC.CH.ZS','source':'worldbank','label':'Out-of-pocket expenditure','short':'Out-of-pocket share','unit':'% of current health expenditure','decimals':1,'scale':'linear','better':'lower'},
'population': {'id':'SP.POP.TOTL','source':'worldbank','label':'Population, total','short':'Population','unit':'people','decimals':0,'scale':'log','better':None},
'sao': {'id':'SH.MED.SAOP.P5','source':'worldbank','label':'Specialist surgical workforce','short':'Surgical workforce (SAO)','unit':'per 100,000 population','decimals':1,'scale':'linear','better':'higher','target':20,'targetLabel':'LCoGS 2030 target: at least 20 per 100,000'},
'volume': {'id':'SH.SGR.PROC.P5','source':'worldbank','label':'Number of surgical procedures','short':'Surgical volume','unit':'per 100,000 population','decimals':0,'scale':'linear','better':'higher','target':5000,'targetLabel':'LCoGS 2030 target: at least 5,000 per 100,000'},
'catastrophic': {'id':'SH.SGR.CRSK.ZS','source':'worldbank','label':'Risk of catastrophic expenditure for surgical care','short':'Catastrophic cost risk (surgery)','unit':'% of people at risk','decimals':1,'scale':'linear','better':'lower','target':0,'targetLabel':'LCoGS 2030 target: 100% protection (0% at risk)'},
'impoverishing':{'id':'SH.SGR.IRSK.ZS','source':'worldbank','label':'Risk of impoverishing expenditure for surgical care','short':'Impoverishing cost risk (surgery)','unit':'% of people at risk','decimals':1,'scale':'linear','better':'lower','target':0,'targetLabel':'LCoGS 2030 target: 100% protection (0% at risk)'},
'uhc': {'id':'UHC_INDEX_REPORTED','source':'who','label':'UHC service coverage index (SDG 3.8.1)','short':'UHC service coverage index','unit':'index 0–100','decimals':0,'scale':'linear','better':'higher'},
'cata10': {'id':'FINPROTECTION_CATA_TOT_10_POP','source':'who','dim1':'RESIDENCEAREATYPE_TOTL','label':'Population with household health spending above 10% of total consumption or income (SDG 3.8.2)','short':'Catastrophic health spending (>10%)','unit':'% of population','decimals':1,'scale':'linear','better':'lower'},
}
GROUPS = [
{'id':'context','label':'System context','indicators':['spending','spending_ppp','physicians','life','oop','population'],'note':'National averages; contextual observations, not measures of surgical access.'},
{'id':'surgical','label':'Surgical system (LCoGS indicators)','indicators':['sao','volume','catastrophic','impoverishing'],'note':'Four of the six Lancet Commission on Global Surgery indicators as compiled in World Development Indicators. Sparse and irregular reporting years; the latest value can be a decade old.'},
{'id':'coverage','label':'Coverage & financial protection','indicators':['uhc','cata10'],'note':'SDG 3.8 monitoring indicators from WHO and World Bank.'},
]
# UN System Data Commons (UNSD) counterparts used for source triangulation and live refresh. factor converts to ASCEND units.
UNDC_REST = 'https://unsd-datacommons.gcp.un-icc.cloud/core/api/v2'
UNDC_NOTE = 'UN System Data Commons (United Nations Statistics Division) knowledge graph; the underlying series retain their publishers\' terms (WHO, UN SDG Global Database). Retrieved via the public V2 observation endpoint.'
UNDC = {
'uhc': {'variable':'undata/sdg/SH_ACS_UNHC_25','factor':1,'unitNote':'index 0–100, SDG Global Database (2025 methodology)'},
'cata10': {'variable':'undata/who/FINP_CHE_SH.HE_THRESHOLD--T_10','factor':1,'unitNote':'% of population, WHO financial-protection series'},
'physicians': {'variable':'undata/sdg/SH_MED_DEN.OCCUPATION--ISCO08_221','factor':0.1,'unitNote':'SDG 3.c.1 medical doctors per 10,000, converted to per 1,000'},
'spending': {'variable':'undata/who/GHED_PER_CAP.HE_CATEGORY--CHE','factor':1,'unitNote':'WHO Global Health Expenditure Database, current health expenditure per capita, current US$'},
'oop': {'variable':'undata/who/GHED_SH.HE_CATEGORY--OOP_HE__REF_EXP_CAT--CHE','factor':1,'unitNote':'WHO GHED, out-of-pocket expenditure as % of current health expenditure'},
'life': {'variable':'undata/unicef/DM_LIFE_EXP','factor':1,'unitNote':'UNICEF data warehouse (UN WPP basis), both sexes; the series extends to the current WPP estimate year'},
'population': {'variable':'undata/unicef/DM_POP','factor':1,'unitNote':'UNICEF data warehouse (UN WPP basis), total population'},
# Derived cross-check only (no single Commons variable): WHO NHWA head-counts of surgeons, anaesthesiologists and
# obstetricians divided by World Bank population of the same year, per 100,000. One observation per economy, 2009–2015.
'sao': {'derived':True,'variables':['undata/who/HWF_CNT.HWF_TYPE--SURGEON','undata/who/HWF_CNT.HWF_TYPE--ANAESTH','undata/who/HWF_CNT.HWF_TYPE--OBSTETRICIAN'],'factor':1,'unitNote':'sum of WHO head-counts / World Bank population × 100,000; counts are single 2009–2015 observations'},
}
# Indicators searched in the Commons (MCP search_indicators anchored on Brazil and Kenya, 20 Oct 2026) with no counterpart.
UNDC_NONE = {
'spending_ppp':'GHED per-capita slice is published in current US$ only; no PPP series in this deployment.',
'volume':'No surgical-volume series; nearest hits are trachoma surgeries and caesarean-section shares, which are not comparable.',
'catastrophic':'No surgery-specific catastrophic-expenditure series; the Commons holds general catastrophic (FINP_CHE_SH) and impoverishing (FINP_IMP_SH) health-spending shares.',
'impoverishing':'No surgery-specific impoverishing-expenditure series; general FINP_IMP_SH poverty-line series exist but measure all health spending.',
}
ACCESS_GROUP = {'id':'access','label':'Geographic access (modelled)','indicators':['access2h','access1h','traveltime'],'note':'Population-weighted travel time to the nearest healthcare facility, computed by ASCEND at 1 km from the Malaria Atlas Project 2019 travel-time surfaces (motorised and walking-only) and WorldPop 2020 counts. Any facility, not Bellwether surgical capacity: a modelled upper bound on geographic surgical access.'}
def utcnow():
return datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='seconds')
def fetch(job):
name,url,note = job
record = {'id':name,'name':name.replace('_',' ').title(),'url':url,'accessedAt':utcnow(),'licenseNote':note}
attempts=6 if 'ghoapi' in url else 4
for attempt in range(attempts):
try:
req = urllib.request.Request(url,headers={'User-Agent':'ASCEND-research-prototype/2.0','Accept':'application/json','Accept-Encoding':'gzip'})
with urllib.request.urlopen(req,timeout=90) as response:
body=response.read()
if body[:2] == b'\x1f\x8b': body=gzip.decompress(body)
payload=json.loads(body.decode('utf-8'))
record.update(status='success',httpStatus=response.status,bytes=len(body),attempts=attempt+1)
(RAW/f'{name}.json').write_text(json.dumps(payload,ensure_ascii=False))
return name,payload,record
except Exception as exc:
record.update(status='failed',error=str(exc)[:300],attempts=attempt+1)
time.sleep(2+3*attempt)
cached=RAW/f'{name}.json'
if cached.exists():
try:
payload=json.loads(cached.read_text())
record.update(status='cached',note='Live retrieval failed; the previously retrieved raw response in data_raw/ was reused unchanged. Timestamp is the file modification time.',
accessedAt=datetime.datetime.fromtimestamp(cached.stat().st_mtime,datetime.timezone.utc).isoformat(timespec='seconds'))
return name,payload,record
except Exception: pass
return name,None,record
def clean_text(text):
return html.unescape(re.sub('<[^>]+>', ' ', text or '')).strip()
def wb_rows(payload):
return payload[1] if isinstance(payload,list) and len(payload)>1 and isinstance(payload[1],list) else []
def to_series(values):
"""{year:value} -> sorted [[year,value],...] with nulls dropped and finite check."""
out=[]
for year in range(YEARS[0],YEARS[1]+1):
v=values.get(year)
if v is None: continue
v=float(v)
if not math.isfinite(v): continue
out.append([year,round(v,4)])
return out
def validate(data):
assert data['kind'] == 'observed/retrieved'
assert data['version'] == VERSION
ids=[c['id'] for c in data['countries']]
assert len(ids)==len(set(ids)) and len(ids) >= 190, len(ids)
keys=set(data['indicators'])
for g in data['indicatorGroups']:
assert set(g['indicators']) <= keys
observations=0; nonempty=0
for c in data['countries']:
assert re.fullmatch(r'[A-Z]{3}',c['id']) and c['name'] and c['region'] and c['income']
assert c['lat'] is None or -90 <= c['lat'] <= 90
assert c['lon'] is None or -180 <= c['lon'] <= 180
assert set(c['series']) == keys
for key,series in c['series'].items():
years=[y for y,_ in series]
assert years==sorted(years) and len(set(years))==len(years)
assert all(YEARS[0] <= y <= YEARS[1] for y in years)
assert all(isinstance(v,(int,float)) and math.isfinite(v) for _,v in series)
observations+=len(series); nonempty+=bool(series)
for source in data['sources']:
assert source['url'].startswith('https://') and source['accessedAt'] and source['licenseNote']
assert data['evidence']['classification'] == 'related literature; not model effect estimates'
for p in data['evidence']['publications']:
assert p['title'] and p['url'].startswith('https://')
assert data['trials']['classification'] == 'registered studies; not evidence of efficacy or surgery relevance'
for p in data['trials']['studies']:
assert p['nctId'].startswith('NCT') and p['url'].startswith('https://clinicaltrials.gov/study/')
for c in data['connectorProofs']:
assert 'schemaFields' in c and 'patientData' not in c
coverage={k:sum(1 for c in data['countries'] if c['series'][k]) for k in keys}
return {'countries':len(ids),'indicators':len(keys),'series':len(ids)*len(keys),'nonEmptySeries':nonempty,'observations':observations,
'countriesWithAnyValue':coverage,'publications':len(data['evidence']['publications']),'trials':len(data['trials']['studies'])}
def build():
RAW.mkdir(parents=True,exist_ok=True)
jobs=[('worldbank_countries','https://api.worldbank.org/v2/country?format=json&per_page=400',WB_NOTE)]
for key,meta in INDICATORS.items():
if meta['source']=='worldbank':
jobs.append((f'worldbank_{key}',f"https://api.worldbank.org/v2/country/all/indicator/{meta['id']}?format=json&date={YEARS[0]}:{YEARS[1]}&per_page=20000",WB_NOTE))
else:
jobs.append((f'who_{key}',f"https://ghoapi.azureedge.net/api/{meta['id']}",WHO_NOTE))
queries={
'europe_pmc_access':'(TITLE_ABS:"surgical access" OR TITLE_ABS:"access to surgery" OR TITLE_ABS:"access to surgical care") AND (TITLE_ABS:"global surgery" OR TITLE_ABS:"health system" OR TITLE_ABS:"low- and middle-income")',
'europe_pmc_capacity':'(TITLE_ABS:"surgical capacity" OR TITLE_ABS:"surgical workforce") AND (TITLE_ABS:financing OR TITLE_ABS:access OR TITLE_ABS:"health system")',
'europe_pmc_indicators':'TITLE_ABS:"Lancet Commission on Global Surgery" AND TITLE_ABS:indicators'
}
for key,m in UNDC.items():
for i,var in enumerate(m.get('variables') or [m['variable']]):
jobs.append((f'undc_{key}' if not m.get('derived') else f'undc_{key}_{i}',UNDC_REST+'/observation?'+urllib.parse.urlencode([('variable.dcids',var),('entity.expression','Earth<-containedInPlace+{typeOf:Country}'),('date',''),('select','entity'),('select','variable'),('select','value'),('select','date')]),UNDC_NOTE))
for key,q in queries.items():
jobs.append((key,'https://www.ebi.ac.uk/europepmc/webservices/rest/search?'+urllib.parse.urlencode({'query':q,'format':'json','pageSize':6,'resultType':'core','sort':'CITED desc'}),EPMC_NOTE))
jobs.extend([
('demas_cnes','https://apidadosabertos.saude.gov.br/cnes/estabelecimentos?codigo_uf=35&limit=1&offset=0',GOV_NOTE),
('demas_sih','https://apidadosabertos.saude.gov.br/assistencia-a-saude/sih-procedimentos-hospitalares?nu_ano=2023&limit=1&offset=0',GOV_NOTE),
('clinical_trials','https://clinicaltrials.gov/api/v2/studies?'+urllib.parse.urlencode({'query.term':'AREA[BriefTitle]("surgical access" OR "access to surgery" OR "access to surgical care" OR "global surgery" OR "surgical capacity" OR "surgical workforce" OR "surgical care delivery" OR "surgical outreach") AND AREA[StudyType]INTERVENTIONAL','pageSize':6,'format':'json'}),CT_NOTE)
])
responses={}; sources=[]
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
for name,payload,record in pool.map(fetch,jobs):
responses[name]=payload;sources.append(record)
print(name,record['status'],record.get('bytes',''),flush=True)
(RAW/'request_manifest.json').write_text(json.dumps(sources,indent=2))
metadata=wb_rows(responses['worldbank_countries'])
if not metadata:
raise RuntimeError('Country metadata unavailable; no substitute snapshot was fabricated. Inspect raw request manifest.')
economies=[m for m in metadata if m.get('region',{}).get('value') and m['region']['value']!='Aggregates']
# indicator tables keyed by iso3
tables={}
for key,meta in INDICATORS.items():
if meta['source']=='worldbank':
rows=wb_rows(responses.get(f'worldbank_{key}'))
table={}
for row in rows:
iso=row.get('countryiso3code')
if not iso or row.get('value') is None: continue
table.setdefault(iso,{})[int(row['date'])]=row['value']
tables[key]=(table,'success' if rows else 'unavailable',f'worldbank_{key}')
else:
payload=responses.get(f'who_{key}') or {}
table={}
for row in payload.get('value',[]):
if row.get('SpatialDimType')!='COUNTRY' or row.get('NumericValue') is None: continue
if (row.get('Dim1') or None)!=meta.get('dim1'): continue
table.setdefault(row['SpatialDim'],{})[int(row['TimeDim'])]=row['NumericValue']
tables[key]=(table,'success' if table else 'unavailable',f'who_{key}')
access=None
if ACCESS.exists():
access=json.loads(ACCESS.read_text())
for k,meta in access['indicators'].items():
INDICATORS[k]={**meta,'source':'ascend-geo'}
order=['access2h','access1h','traveltime','access2h_walk','access1h_walk','traveltime_walk']
GROUPS.append({**ACCESS_GROUP,'indicators':[k for k in order if k in access['indicators']]})
sources.append({'id':'ascend_geo_access','name':'ASCEND geographic-access pipeline','url':access['provenance']['methodUrl'],'accessedAt':access['generatedAt'],'licenseNote':access['provenance']['licenseNote'],'status':'computed','note':access['provenance']['summary']})
for s in access['provenance']['inputs']:
sources.append({'id':s['id'],'name':s['name'],'url':s['url'],'accessedAt':access['generatedAt'],'licenseNote':s['licenseNote'],'status':'success'})
undc_tables={}
def undc_table(payload,var,factor):
table={}
for entity,rec in (payload.get('byVariable',{}).get(var,{}).get('byEntity',{}) or {}).items():
mt=re.fullmatch(r'country/([A-Z]{3})',entity)
if not mt: continue
facets=rec.get('orderedFacets') or []
if not facets: continue
vals={}
for o in facets[0].get('observations',[]):
try: y=int(str(o['date'])[:4]); v=float(o['value'])*factor
except Exception: continue
if math.isfinite(v): vals[y]=v
if vals: table[mt.group(1)]=sorted((y,round(v,4)) for y,v in vals.items())
return table
for key,m in UNDC.items():
if m.get('derived'):
parts=[undc_table(responses.get(f'undc_{key}_{i}') or {},var,1) for i,var in enumerate(m['variables'])]
undc_tables[key]=('derived',parts); continue
payload=responses.get(f'undc_{key}') or {}
table={}
for entity,rec in (payload.get('byVariable',{}).get(m['variable'],{}).get('byEntity',{}) or {}).items():
mt=re.fullmatch(r'country/([A-Z]{3})',entity)
if not mt: continue
facets=rec.get('orderedFacets') or []
if not facets: continue
vals={}
for o in facets[0].get('observations',[]):
try: y=int(str(o['date'])[:4]); v=float(o['value'])*m['factor']
except Exception: continue
if math.isfinite(v): vals[y]=v
if vals: table[mt.group(1)]=sorted((y,round(v,4)) for y,v in vals.items())
undc_tables[key]=table
for key,val in list(undc_tables.items()):
if isinstance(val,tuple) and val[0]=='derived':
parts=val[1]; table={}
poptab=tables['population'][0]
for iso in set.intersection(*[set(p) for p in parts]) if parts else set():
years=set.intersection(*[{y for y,_ in p[iso]} for p in parts])
out=[]
for y in sorted(years):
pop=poptab.get(iso,{}).get(y)
if not pop: continue
total=sum(dict(p[iso])[y] for p in parts)
out.append((y,round(total/float(pop)*1e5,4)))
if out: table[iso]=out
undc_tables[key]=table
countries=[]
for meta in sorted(economies,key=lambda m:m['name']):
iso=meta['id']
c={'id':iso,'iso2':meta.get('iso2Code'),'name':meta['name'],'region':meta['region']['value'].strip(),'income':meta['incomeLevel']['value'].strip(),
'capital':meta.get('capitalCity') or None,
'lat':float(meta['latitude']) if meta.get('latitude') else None,'lon':float(meta['longitude']) if meta.get('longitude') else None,
'series':{}}
for key in INDICATORS:
if key in tables:
c['series'][key]=to_series(tables[key][0].get(iso,{}))
elif access:
rec=access['countries'].get(iso,{}); v=rec.get(key)
# Small-island artefact guard: a microstate whose nearest mapped facility is across open water
# (mean travel time > 4 h with < 100,000 people) reflects a gap in the facility database, not access.
suffix='_walk' if key.endswith('_walk') else ''
artefact=rec.get('popTotal2020',0)<100000 and (rec.get('traveltime'+suffix) or 0)>240
c['series'][key]=[[access['referenceYear'],round(float(v),4)]] if v is not None and math.isfinite(float(v)) and not artefact else []
c['crosscheck']={}
for key,table in undc_tables.items():
ser=table.get(iso)
if ser: c['crosscheck'][key]=[list(ser[-1]),len(ser)] # latest [year,value] and number of observations held by UN Data Commons
countries.append(c)
# agreement summary at the latest common year, per indicator
agreement={}
for key,table in undc_tables.items():
n=agree=0;diffs=[]
for c in countries:
ser=table.get(c['id']);snap=dict(c['series'].get(key,[]))
if not ser or not snap: continue
common=[p for p in ser if p[0] in snap]
if not common: continue
y,v=common[-1];a=snap[y];n+=1;rel=abs(a-v)/max(abs(a),abs(v),1e-9);diffs.append(rel)
if rel<=0.05: agree+=1
agreement[key]={'economiesCompared':n,'withinFivePercent':agree,'medianRelativeDifference':round(sorted(diffs)[len(diffs)//2],4) if diffs else None,'undcEconomies':len(table)}
indicator_defs={}
for key,meta in INDICATORS.items():
status=tables[key][1] if key in tables else ('computed' if access else 'unavailable')
indicator_defs[key]={**meta,'sourceId':tables[key][2] if key in tables else 'ascend_geo_access','retrievalStatus':status,
'countriesWithData':sum(1 for c in countries if c['series'][key])}
if key in UNDC:
m=UNDC[key]; first=m.get('variable') or m['variables'][0]
indicator_defs[key]['undc']={**m,'rest':UNDC_REST+'/node?'+urllib.parse.urlencode({'nodes':first,'property':'->name'}),'status':'success' if undc_tables.get(key) else 'unavailable','agreement':agreement.get(key)}
elif key in UNDC_NONE:
indicator_defs[key]['undc']={'status':'no-counterpart','note':UNDC_NONE[key]}
publications=[]; seen=set()
for key,q in queries.items():
response=responses[key] or {}
for p in response.get('resultList',{}).get('result',[]):
ident=p.get('doi') or p.get('pmid') or p['id']
if ident in seen: continue
seen.add(ident)
source=p.get('source','MED');id_=p['id']
publications.append({'id':id_,'title':clean_text(p.get('title')),'authors':p.get('authorString',''),'year':int(p['pubYear']) if p.get('pubYear') else None,
'doi':p.get('doi'),'pmid':p.get('pmid'),'url':f'https://europepmc.org/article/{source}/{id_}',
'abstract':clean_text(p.get('abstractText')),'journal':p.get('journalInfo',{}).get('journal',{}).get('title',''),
'citedBy':p.get('citedByCount'),
'classification':'related literature; not model effect estimates','query':q,'sourceId':key})
studies=[]
for row in (responses['clinical_trials'] or {}).get('studies',[]):
p=row['protocolSection']; ident=p['identificationModule']; design=p.get('designModule',{}); status=p.get('statusModule',{})
locations=p.get('contactsLocationsModule',{}).get('locations',[])
countries_in=sorted({loc.get('country') for loc in locations if loc.get('country')})
studies.append({'nctId':ident['nctId'],'title':ident.get('briefTitle',''),'status':status.get('overallStatus',''),
'phase':design.get('phases',[]),'studyType':design.get('studyType',''),'briefSummary':p.get('descriptionModule',{}).get('briefSummary',''),
'url':'https://clinicaltrials.gov/study/'+ident['nctId'],'countries':countries_in,'locations':[{k:loc[k] for k in ['facility','city','country'] if k in loc} for loc in locations[:6]],
'sourceId':'clinical_trials','lastUpdate':status.get('lastUpdatePostDateStruct',{}).get('date')})
proofs=[]
for name,key in [('demas_cnes','estabelecimentos'),('demas_sih','sih_procedimentos_hospitalares')]:
response=responses[name] or {}; rows=response.get(key,[])
proofs.append({'id':name,'label':'CNES establishment schema' if name=='demas_cnes' else 'SIH hospital-procedure schema',
'status':'success' if rows else 'unavailable','schemaFields':sorted(rows[0].keys()) if rows else [],'sampleRowsRetrieved':len(rows),
'sourceId':name,'note':'Connectivity and schema proof only (example national registry). One response row does not establish data completeness, historical coverage, surgical relevance, or analytic validity.'})
data={'version':VERSION,'kind':'observed/retrieved','accessedAt':utcnow(),'yearRange':list(YEARS),
'description':'All World Bank economies: contextual health-system indicators, Lancet Commission on Global Surgery indicators, SDG 3.8 coverage and protection indicators'+(', and ASCEND-modelled geographic access' if access else '')+'. Observational comparisons do not establish intervention effects.',
'indicatorGroups':GROUPS,'indicators':indicator_defs,'countries':countries,
'sources':sources,'apiRegistry':[{k:v for k,v in r.items() if k not in ['bytes']} for r in sources],
'evidence':{'classification':'related literature; not model effect estimates','selection':'Three explicit Europe PMC queries on surgical access, capacity and LCoGS indicators, six most-cited results per query, deduplicated. Convenience discovery set, not a systematic review. No relevance or risk-of-bias adjudication.','status':'success' if publications else 'unavailable','publications':publications},
'trials':{'classification':'registered studies; not evidence of efficacy or surgery relevance','selection':'First six ClinicalTrials.gov API results for interventional studies with surgical access, capacity, workforce or delivery terms in the title. No country filter; registry coverage of health-system interventions is thin.','status':'success' if studies else 'unavailable','studies':studies},
'connectorProofs':proofs,
'limitations':['All World Bank economies are included; small states and territories often lack observations and remain visibly missing.',
'Latest values can refer to different years; the observation year is always shown next to the value.',
'Expenditure is shown in current US dollars and, separately, in PPP international dollars; neither is inflation-adjusted across years.',
'Missing values remain null; no interpolation, imputation or carry-forward in fixed-year mode.',
'Lancet Commission on Global Surgery indicators are compiled from heterogeneous national reports with irregular years and definitions.',
'National ecological indicators cannot estimate effects of a surgical intervention.',
'Bibliographic and registry retrieval is not a systematic evidence synthesis.',
'No patient-level records are included in this distributable snapshot.']+
(['Geographic access is modelled from a travel-time surface to any healthcare facility and 2020 population counts; it is not the LCoGS Bellwether 2-hour indicator and overstates surgical access.',
'Access values are omitted where fewer than half the population lies on valid travel-time pixels, and for microstates (<100,000 people) whose mean travel time exceeds four hours, which indicates missing facility points rather than inaccessibility.'] if access else [])}
data['undc']={'rest':UNDC_REST,'mcp':'https://unsd-datacommons.gcp.un-icc.cloud/mcp','documentation':'https://projects.officialstatistics.org/undata2/undatacommons-mcp/','portal':'https://data.un.org/undatacommons/','agreement':agreement,
'note':'Eight ASCEND indicators carry a UN System Data Commons counterpart (seven single variables plus a derived SAO density) (DCID in indicators[*].undc). The snapshot stores each economy\'s latest UN Data Commons value for triangulation; the app can also refresh these series live from the browser because the endpoint allows cross-origin requests.'}
data['validation']=validate(data)
DEST.parent.mkdir(parents=True,exist_ok=True)
DEST.write_text(json.dumps(data,ensure_ascii=False,separators=(',',':')))
print(json.dumps(data['validation'],indent=2))
print('bytes',DEST.stat().st_size)
if __name__ == '__main__':
parser=argparse.ArgumentParser();parser.add_argument('--validate',action='store_true');args=parser.parse_args()
if args.validate: print(json.dumps(validate(json.loads(DEST.read_text())),indent=2))
else: build()
|