// Builds the curated CSV tables and saved-analysis results for dataset sex-gap-sports-performance. // Input: wiaa2026.txt (text extracted by pdftext.js from the WIAA 2026 2A/3A/4A results PDF). // Output: ../data/*.csv and ../data/analyses.json const fs = require('fs'); const path = require('path'); const here = __dirname; const out = path.join(here, '..', 'data'); const secs = s => s.split(':').map(Number).reduce((a, b) => a * 60 + b, 0); const ftToM = s => { const [f, i] = s.split('-').map(Number); return (f * 12 + (i || 0)) * 0.0254; }; const r2 = x => Math.round(x * 100) / 100; const r1 = x => Math.round(x * 10) / 10; // ---------- WIAA 2026 finals ---------- const EVENTS = { '100 Meter Dash': ['100m', 'time'], '200 Meter Dash': ['200m', 'time'], '400 Meter Dash': ['400m', 'time'], '800 Meter Run': ['800m', 'time'], '1600 Meter Run': ['1600m', 'time'], '3200 Meter Run': ['3200m', 'time'], 'High Jump': ['High jump', 'distance'], 'Pole Vault': ['Pole vault', 'distance'], 'Long Jump': ['Long jump', 'distance'], 'Triple Jump': ['Triple jump', 'distance'], }; const markRe = /^[JX]?(\d+:\d{2}\.\d{2}|\d+\.\d{2}|\d+-\d{2}(\.\d{2})?)$/; const L = fs.readFileSync(path.join(here, 'wiaa2026.txt'), 'latin1').split('\n'); const wiaa = []; for (let i = 0; i < L.length; i++) { const h = L[i].match(/^Event \d+\s+(Boys|Girls) (.+) (\dA)$/); if (!h || !EVENTS[h[2]]) continue; let j = i + 1; while (j < L.length && !/^Event \d+ /.test(L[j])) j++; const b = L.slice(i, j); const si = b.indexOf('School'); if (si < 0 || !b.slice(si, si + 6).includes('Finals') || b.slice(si, si + 6).includes('Preliminaries')) continue; const [event, kind] = EVENTS[h[2]]; const seen = new Set(); for (let k = 0; k < b.length - 4; k++) { if (!/^\d+$/.test(b[k]) || !b[k + 1].startsWith('#')) continue; const place = +b[k]; if (seen.has(place) || place > 8) continue; let m = null; for (let q = k + 4; q < Math.min(k + 8, b.length); q++) if (markRe.test(b[q].trim())) { m = b[q].trim().replace(/^[JX]/, ''); break; } if (!m) continue; seen.add(place); const value = kind === 'time' ? secs(m) : ftToM(m); wiaa.push({ classification: h[3], sex: h[1] === 'Boys' ? 'Boys' : 'Girls', event, place, mark: m, value: kind === 'time' ? r2(value) : Math.round(value * 1000) / 1000, unit: kind === 'time' ? 'seconds' : 'meters' }); } } // ---------- Paris 2024 Olympic gold-medal marks ---------- const OLY = [ ['Athletics', '100m', '9.79', '10.72'], ['Athletics', '200m', '19.46', '21.83'], ['Athletics', '400m', '43.40', '48.17'], ['Athletics', '800m', '1:41.19', '1:56.72'], ['Athletics', '1500m', '3:27.65', '3:51.29'], ['Athletics', '5000m', '13:13.66', '14:28.56'], ['Athletics', '10000m', '26:43.14', '30:43.25'], ['Athletics', 'Marathon', '2:06:26', '2:22:55'], ['Athletics', 'High jump', '2.36', '2.00'], ['Athletics', 'Pole vault', '6.25', '4.90'], ['Athletics', 'Long jump', '8.48', '7.10'], ['Athletics', 'Triple jump', '17.86', '15.02'], ['Swimming', '50m freestyle', '21.25', '23.71'], ['Swimming', '100m freestyle', '46.40', '52.16'], ['Swimming', '200m freestyle', '1:44.72', '1:53.27'], ['Swimming', '400m freestyle', '3:41.78', '3:57.49'], ['Swimming', '800m freestyle', '7:38.19', '8:11.04'], ['Swimming', '1500m freestyle', '14:30.67', '15:30.02'], ['Swimming', '100m backstroke', '52.00', '57.33'], ['Swimming', '200m backstroke', '1:54.26', '2:03.73'], ]; const isJump = e => /jump|vault/i.test(e); const oly = []; for (const [sport, event, m, w] of OLY) for (const [sex, mark] of [['Men', m], ['Women', w]]) oly.push({ sport, event, sex, mark, value: isJump(event) ? +mark : r2(secs(mark)), unit: isJump(event) ? 'meters' : 'seconds' }); // Gap: how much better the male mark is, as % of the female mark's speed or distance. // Timed events: female time / male time - 1 (equivalently male average speed / female average speed - 1). // Jumps: male distance / female distance - 1. const gap = (male, female, unit) => r1(100 * (unit === 'seconds' ? female / male - 1 : male / female - 1)); // ---------- Physical traits ---------- const TRAITS = [ // trait, measure, ages, male, female, unit, sourceId ['Height', 'Mean standing height', '20+', 175.3, 161.3, 'cm', 'nhanes-anthro'], ['Height', 'Mean standing height', '12', 154.2, 154.8, 'cm', 'nhanes-anthro'], ['Height', 'Mean standing height', '17', 175.2, 163.0, 'cm', 'nhanes-anthro'], ['Skeletal muscle mass', 'Whole-body skeletal muscle (MRI)', '18-88', 33.0, 21.0, 'kg', 'janssen-2000'], ['Grip strength', 'Mean maximum grip', '10', 17.2, 16.7, 'kg', 'dodds-2014'], ['Grip strength', 'Mean maximum grip', '15', 29.6, 23.9, 'kg', 'dodds-2014'], ['Grip strength', 'Mean maximum grip', '30', 51.6, 31.4, 'kg', 'dodds-2014'], ['Bone', 'Total hip bone mineral content (DXA)', '8-11', 18.15, 16.98, 'g', 'nhanes-bone'], ['Bone', 'Total hip bone mineral content (DXA)', '16-19', 43.36, 30.05, 'g', 'nhanes-bone'], ['Bone', 'Total hip bone mineral content (DXA)', '20-29', 43.72, 30.48, 'g', 'nhanes-bone'], ['Bone', 'Total hip bone mineral density (DXA)', '20-29', 1.081, 0.973, 'g/cm2', 'nhanes-bone'], ['Hemoglobin', 'Mean blood hemoglobin', '9-11', 132.8, 131.0, 'g/L', 'nhanes-hemoglobin'], ['Hemoglobin', 'Mean blood hemoglobin', '15-19', 150.7, 131.5, 'g/L', 'nhanes-hemoglobin'], ['Hemoglobin', 'Mean blood hemoglobin', '20-29', 154.1, 131.4, 'g/L', 'nhanes-hemoglobin'], ['Heart', 'Left ventricular mass (cardiac MRI)', '45-74', 103, 70, 'g', 'petersen-2017'], ['Heart', 'Left ventricular stroke volume (cardiac MRI)', '45-74', 96, 75, 'mL', 'petersen-2017'], ['Aerobic capacity', 'Median VO2max (treadmill)', '20-29', 48.0, 37.6, 'mL/kg/min', 'friend-2015'], ['Reaction time', 'Mean sprint-start reaction time, Beijing 2008 (lower is faster)', 'Olympic sprinters', 166, 189, 'ms', 'lipps-2011'], ]; const traits = TRAITS.map(([trait, measure, ages, male, female, unit, sourceId]) => ({ trait, measure, ages, male, female, unit, maleVsFemalePct: unit === 'ms' ? r1(100 * (female / male - 1)) : r1(100 * (male / female - 1)), sourceId, })); // ---------- CSV writer ---------- const csv = (rows) => { const cols = Object.keys(rows[0]); const esc = v => { const s = String(v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }; return cols.join(',') + '\n' + rows.map(r => cols.map(c => esc(r[c])).join(',')).join('\n') + '\n'; }; fs.writeFileSync(path.join(out, 'wiaa-2026-finals.csv'), csv(wiaa)); fs.writeFileSync(path.join(out, 'paris-2024-winners.csv'), csv(oly)); fs.writeFileSync(path.join(out, 'physical-traits.csv'), csv(traits)); // ---------- Saved analyses ---------- const olyGap = OLY.map(([sport, event]) => { const m = oly.find(r => r.event === event && r.sex === 'Men'), w = oly.find(r => r.event === event && r.sex === 'Women'); return { level: 'Paris 2024 Olympic champion', sport, event, menMark: m.mark, womenMark: w.mark, gapPct: gap(m.value, w.value, m.unit) }; }); const get = (c, s, e, p) => wiaa.find(r => r.classification === c && r.sex === s && r.event === e && r.place === p); const wiaaGap = []; for (const c of ['4A', '3A', '2A']) for (const e of Object.values(EVENTS).map(x => x[0])) { const b1 = get(c, 'Boys', e, 1), b8 = get(c, 'Boys', e, 8), g1 = get(c, 'Girls', e, 1); if (!b1 || !g1) continue; wiaaGap.push({ classification: c, event: e, boysChampion: b1.mark, boys8th: b8 ? b8.mark : null, girlsChampion: g1.mark, championGapPct: gap(b1.value, g1.value, b1.unit), boys8thGapPct: b8 ? gap(b8.value, g1.value, b8.unit) : null, boys8thBeatGirlsChampion: b8 ? (b8.unit === 'seconds' ? b8.value < g1.value : b8.value > g1.value) : null }); } const h2h = []; for (const e of ['100m', '200m', '400m', '800m', 'High jump', 'Pole vault', 'Long jump', 'Triple jump']) { const w = oly.find(r => r.event === e && r.sex === 'Women'); const boys = wiaa.filter(r => r.sex === 'Boys' && r.event === e); const beat = boys.filter(r => r.unit === 'seconds' ? r.value < w.value : r.value > w.value); const best = boys.reduce((a, r) => !a ? r : ((r.unit === 'seconds' ? r.value < a.value : r.value > a.value) ? r : a), null); h2h.push({ event: e, olympicWomensGold: w.mark, wiaaBoysFinalists: boys.length, finalistsWhoBeatIt: beat.length, bestWiaaBoysMark: best.mark, bestWiaaBoysValue: best.value, unit: w.unit }); } const chartRows = []; for (const e of ['100m', '200m', '400m', '800m', 'High jump', 'Pole vault', 'Long jump', 'Triple jump']) { const o = olyGap.find(r => r.event === e); chartRows.push({ event: e, level: 'Olympic champions (Paris 2024)', gapPct: o.gapPct }); const w = wiaaGap.find(r => r.classification === '4A' && r.event === e); chartRows.push({ event: e, level: 'Washington 4A state champions (2026)', gapPct: w.championGapPct }); } fs.writeFileSync(path.join(out, 'analyses.json'), JSON.stringify({ olyGap, wiaaGap, h2h, chartRows, traits }, null, 2)); console.log('wiaa rows', wiaa.length, 'oly rows', oly.length, 'traits', traits.length); console.table(olyGap); console.table(wiaaGap); console.table(h2h); console.table(chartRows);