Supporting source-data and code supplement for the Spanish occupational mortality analysis (2022–2023). Source-level verification completed 14 October 2026: 108 of 108 age × sex × occupation cells matched the original Mike’s Balance research archive, with zero discrepancies in deaths or employed denominators.
This is an audit supplement, not a second research report. Read the accessible article at Mike’s Balance.
Official INE inputs
Deaths: INE table 32458, single-year ages. Employed-person counts: INE table 69961, five-year age groups. We queried the official INE API (national total, nine occupation groups, both sexes, 2022 and 2023), collapsed single-year deaths into five-year groups, and retained ages 35–64. The API calls are encoded in the script below.
Verification summary
All 108 cells matched the archived MB extraction; zero deaths/count mismatches. Pooled standardised rates (per 100,000 annual employed counts): men, scientific/intellectual professionals 101.565 and elementary occupations 201.666; women, 60.428 and 78.580. Rate ratios: men 1.9856 (approx. 95% CI 1.8810–2.0960); women 1.3004 (1.2188–1.3875); ratio of those ratios 1.5269 (1.4033–1.6614).
Year-specific rate ratios using common pooled age weights: men 2.0658 in 2022 and 1.9008 in 2023; women 1.4418 and 1.1757. These checks do not establish causation.
Reproduction instructions
Download/copy the CSV and Python script shown below into files with the indicated names in one folder. Run python replicate_ine_standalone.py for an offline numerical reproduction; run python replicate_ine_standalone.py --refresh to retrieve fresh series from the INE API. Python 3 standard library only.
Official INE source-level CSV (108 cells, copy to INE_2022_2023_official_API_108_cells.csv)
k,sex,age,dead2022,dead2023,empl2022,empl2023
1,F,35,1,6,23380,24292
1,F,40,10,11,29489,29791
1,F,45,19,13,37988,37590
1,F,50,28,22,38903,39291
1,F,55,47,41,34525,34999
1,F,60,53,51,24219,25264
1,M,35,8,12,33019,33674
1,M,40,19,18,48579,46633
1,M,45,53,43,73891,69998
1,M,50,114,99,88900,86395
1,M,55,208,192,90167,89111
1,M,60,266,265,66574,68810
2,F,35,41,49,282546,281314
2,F,40,73,83,338861,338112
2,F,45,129,137,315499,333974
2,F,50,145,186,250935,262472
2,F,55,219,252,228818,236253
2,F,60,237,276,137050,148578
2,M,35,35,40,178026,180413
2,M,40,89,75,228738,224595
2,M,45,152,150,235976,245293
2,M,50,200,198,192738,203008
2,M,55,318,302,169093,174227
2,M,60,379,363,109951,115946
3,F,35,16,18,106137,103696
3,F,40,35,27,126867,123823
3,F,45,49,64,135171,138472
3,F,50,71,74,110021,116255
3,F,55,94,99,80832,86143
3,F,60,98,104,44774,48926
3,M,35,46,47,144232,139081
3,M,40,72,85,177366,169935
3,M,45,127,147,189001,192323
3,M,50,230,220,164567,169531
3,M,55,274,302,129992,136337
3,M,60,348,325,69578,76314
4,F,35,31,23,148933,144130
4,F,40,52,60,206022,196369
4,F,45,89,94,225552,232544
4,F,50,141,148,179718,189992
4,F,55,155,153,126149,134737
4,F,60,128,137,64437,71898
4,M,35,19,24,78197,78939
4,M,40,56,43,95993,94184
4,M,45,80,89,103669,106618
4,M,50,114,112,88209,92270
4,M,55,193,199,72636,74927
4,M,60,199,187,39994,43592
5,F,35,61,61,303007,304433
5,F,40,103,115,350208,355604
5,F,45,152,168,340797,356607
5,F,50,263,240,297194,310939
5,F,55,313,330,242717,256122
5,F,60,294,347,149966,162252
5,M,35,91,92,214512,213450
5,M,40,159,158,249241,251906
5,M,45,237,258,242856,250533
5,M,50,395,375,215047,223781
5,M,55,602,580,178254,185350
5,M,60,586,584,108601,116384
6,F,35,2,4,8115,9842
6,F,40,4,4,11498,13339
6,F,45,3,8,13895,15668
6,F,50,18,12,16924,18335
6,F,55,24,22,18493,19775
6,F,60,30,34,17780,18664
6,M,35,13,18,26856,27752
6,M,40,23,35,34704,35166
6,M,45,55,52,38046,38829
6,M,50,96,88,41091,40959
6,M,55,121,138,40155,41121
6,M,60,131,148,29919,31565
7,F,35,4,8,25103,28281
7,F,40,16,12,32853,36362
7,F,45,15,11,34661,39649
7,F,50,25,20,31484,36631
7,F,55,29,41,25949,29484
7,F,60,31,42,17366,19590
7,M,35,77,101,219812,217839
7,M,40,220,210,283674,284896
7,M,45,266,304,286935,302124
7,M,50,429,441,249109,261872
7,M,55,590,576,199896,213061
7,M,60,552,585,117242,128571
8,F,35,2,10,16493,17799
8,F,40,11,8,23318,24235
8,F,45,18,14,24810,27305
8,F,50,21,17,21369,23690
8,F,55,19,17,15708,17468
8,F,60,22,28,9025,9948
8,M,35,70,39,110182,105961
8,M,40,105,80,158640,150664
8,M,45,186,184,180785,182049
8,M,50,326,323,169045,171512
8,M,55,410,426,142648,149095
8,M,60,394,431,74448,82117
9,F,35,31,28,128732,120318
9,F,40,67,51,181010,171679
9,F,45,124,105,207479,202533
9,F,50,202,175,220557,214210
9,F,55,277,288,201636,203820
9,F,60,294,260,136755,143303
9,M,35,79,75,152453,142412
9,M,40,172,124,188698,179367
9,M,45,239,195,179302,180578
9,M,50,346,288,148068,150333
9,M,55,444,416,113698,118047
9,M,60,374,346,59349,64677
Python source (copy to replicate_ine_standalone.py)
#!/usr/bin/env python3
"""Recalculate Spanish occupational mortality from official INE data (2022-2023).
Run with --refresh for fresh INE API values, or use the CSV copied below.
Python 3 standard library only. Not an estimate of life expectancy.
"""
import argparse, csv, json, math, pathlib, re, urllib.parse, urllib.request
CSV = pathlib.Path(__file__).with_name('INE_2022_2023_official_API_108_cells.csv')
IDS = [22324,22374,22541,22672,22716,22807,22834,22948,23016]
COLS = ['k','sex','age','dead2022','dead2023','empl2022','empl2023']
def fetch():
cells = {}
for table in (32458,69961):
for k, occ in enumerate(IDS,1):
q = urllib.parse.urlencode([('nult','2'),('tv','349:16473'),('tv',f'456:{occ}')])
url = f'https://servicios.ine.es/wstempus/js/es/DATOS_TABLA/{table}?{q}'
with urllib.request.urlopen(url,timeout=120) as response:
series = json.load(response)
if not isinstance(series,list): raise ValueError(f'Unexpected INE API response: {url}')
for item in series:
name = item['Nombre']
sx = re.search(r'\. (Hombres|Mujeres)\. ',name)
ag = (re.search(r'\. (\d{2}) años\. ',name) if table == 32458 else
re.search(r'\. De (\d{2}) a (\d{2}) años\. ',name))
if not sx or not ag: continue
sex = 'M' if sx[1]=='Hombres' else 'F'
age = int(ag[1])
if table==32458: age = age//5*5
if age not in (35,40,45,50,55,60): continue
row = cells.setdefault((k,sex,age),dict(k=k,sex=sex,age=age,
dead2022=0,dead2023=0,empl2022=None,empl2023=None))
for point in item['Data']:
yr=point['Anyo']
if yr not in (2022,2023): continue
if point.get('Valor') is None: raise ValueError('Missing INE value')
field=('dead' if table==32458 else 'empl')+str(yr)
if table==32458: row[field]+=int(point['Valor'])
else: row[field]=int(point['Valor'])
if len(cells)!=108: raise ValueError(f'Expected 108 cells, received {len(cells)}')
with CSV.open('w',encoding='utf-8',newline='') as f:
writer=csv.DictWriter(f,fieldnames=COLS); writer.writeheader()
writer.writerows(cells[key] for key in sorted(cells))
ap=argparse.ArgumentParser(); ap.add_argument('--refresh',action='store_true')
args=ap.parse_args()
if args.refresh: fetch()
with CSV.open(newline='',encoding='utf-8-sig') as f: rows=list(csv.DictReader(f))
if len(rows)!=108: raise ValueError('Expected 108 age-sex-occupation cells')
for r in rows:
r['age']=int(r['age']); r['k']=int(r['k'])
for key in COLS[3:]: r[key]=int(r[key])
D=sum(r['empl2022']+r['empl2023'] for r in rows)
w={a:sum(r['empl2022']+r['empl2023'] for r in rows if r['age']==a)/D
for a in (35,40,45,50,55,60)}
def rate(sex,k,years):
out=var=0.
for r in rows:
if r['sex']!=sex or r['k']!=k: continue
d=sum(r['dead'+str(y)] for y in years)
n=sum(r['empl'+str(y)] for y in years)
if not n: raise ValueError('Zero denominator')
out+=1e5*w[r['age']]*d/n
var+=(1e5*w[r['age']]/n)**2*d
return out,math.sqrt(var)
for years in ((2022,2023),(2022,),(2023,)):
m2,m9,f2,f9=[rate(s,k,years) for s,k in [('M',2),('M',9),('F',2),('F',9)]]
male,female=m9[0]/m2[0],f9[0]/f2[0]; ratio=male/female
vm=math.hypot(m9[1]/m9[0],m2[1]/m2[0])
vf=math.hypot(f9[1]/f9[0],f2[1]/f2[0])
vr=math.hypot(vm,vf)
print('Years:',years,'rates:',*[round(v[0],5) for v in (m2,m9,f2,f9)])
print('Male RR:',round(male,5),'Female RR:',round(female,5),
'ratio of RRs:',round(ratio,5))
if len(years)==2:
print('Ratio-of-RRs approx 95% CI:',round(ratio*math.exp(-1.96*vr),5),
round(ratio*math.exp(1.96*vr),5))
Statistical specification and limitations
Age-specific indicator = annual registered deaths divided by the annual January 1 employed-person count. Pooled years combine death counts and annual employed counts. Common direct-standardisation weights use the combined employment counts across all nine groups and both sexes, ages 35–64. Confidence intervals apply a fixed-weight Poisson approximation and log-ratio delta method.
Not a measure of occupational life expectancy or the causal effect of work. The denominator is employment stock, not observed person-time; employment selection, occupation changes, numerator–denominator mismatch, register coding and social determinants may bias comparisons. The 108-cell agreement establishes accurate extraction and arithmetic for these selected INE tables, not freedom from systematic bias, nor independent peer review.
Verification: The Null Institute, 14 October 2026. Contact: office@nullinstitute.org.