// Builds the curated tables and saved-analysis results for dataset sex-gap-sports-performance version 2. // Inputs (build/v2in): rows-2022..2026.json from parse-multi.js (WIAA 2A/3A/4A finals), // swim-rows.json from parse-swim.js (USA Swimming 2024-2028 single-age motivational standards). // Version 1 results (olyGap, wiaaGap, h2h, chartRows, traits) are carried over unchanged from ../data/analyses.json. // Output: ../data-v2/*.csv and ../data-v2/analyses.json const fs = require('fs'); const path = require('path'); const here = __dirname; const inDir = path.join(here, 'v2in'); const out = path.join(here, '..', 'data-v2'); fs.mkdirSync(out, { recursive: true }); const r1 = x => Math.round(x * 10) / 10; const secs = s => s.split(':').map(Number).reduce((a, b) => a * 60 + b, 0); // Gap: how much better the male mark is, in percent of the female mark. const gap = (male, female, unit) => r1(100 * (unit === 'seconds' ? female / male - 1 : male / female - 1)); const better = (a, b, unit) => unit === 'seconds' ? a < b : a > b; const median = a => { const s = [...a].sort((x, y) => x - y); const n = s.length; return n % 2 ? s[(n - 1) / 2] : r1((s[n / 2 - 1] + s[n / 2]) / 2); }; const csv = rows => { const cols = Object.keys(rows[0]); const esc = v => { const s = v === null ? '' : 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'; }; const v1 = JSON.parse(fs.readFileSync(path.join(here, '..', 'data', 'analyses.json'), 'utf8')); const parisRows = fs.readFileSync(path.join(here, '..', 'data', 'paris-2024-winners.csv'), 'utf8').trim().split(/\r?\n/).slice(1) .map(l => { const [sport, event, sex, mark, value, unit] = l.split(','); return { sport, event, sex, mark, value: +value, unit }; }); // ---------- WIAA finals 2022-2026 ---------- const YEARS = [2022, 2023, 2024, 2025, 2026]; const EVENTS = ['100m', '200m', '400m', '800m', '1600m', '3200m', 'High jump', 'Pole vault', 'Long jump', 'Triple jump']; const wiaa = YEARS.flatMap(y => JSON.parse(fs.readFileSync(path.join(inDir, `rows-${y}.json`), 'utf8'))); const get = (y, c, s, e, p) => wiaa.find(r => r.year === y && r.classification === c && r.sex === s && r.event === e && r.place === p); const wiaaGap5 = []; for (const y of YEARS) for (const c of ['4A', '3A', '2A']) for (const e of EVENTS) { const b1 = get(y, c, 'Boys', e, 1), b8 = get(y, c, 'Boys', e, 8), g1 = get(y, c, 'Girls', e, 1); if (!b1 || !g1) continue; wiaaGap5.push({ year: y, classification: c, event: e, boysChampion: b1.mark, boys8th: b8 ? b8.mark : null, girlsChampion: g1.mark, championGapPct: gap(b1.value, g1.value, b1.unit), boysChampionResult: b1.value === g1.value ? 'tie' : better(b1.value, g1.value, b1.unit) ? 'boys better' : 'girls better', boys8thBeatGirlsChampion: b8 ? better(b8.value, g1.value, b8.unit) : null }); } const olyGapPct = e => { const r = v1.olyGap.find(x => x.event === e); return r ? r.gapPct : null; }; const wiaaByEvent = EVENTS.map(e => { const g = wiaaGap5.filter(r => r.event === e).map(r => r.championGapPct); return { event: e, finals: g.length, medianChampionGapPct: median(g), minChampionGapPct: Math.min(...g), maxChampionGapPct: Math.max(...g), olympicChampionGapPct: olyGapPct(e) }; }); const with8 = wiaaGap5.filter(r => r.boys8thBeatGirlsChampion !== null); const wiaaSummary5 = [{ finals: wiaaGap5.length, boysChampionBetter: wiaaGap5.filter(r => r.boysChampionResult === 'boys better').length, ties: wiaaGap5.filter(r => r.boysChampionResult === 'tie').length, girlsChampionBetter: wiaaGap5.filter(r => r.boysChampionResult === 'girls better').length, medianChampionGapPct: median(wiaaGap5.map(r => r.championGapPct)), finalsWith8thPlaceBoysMark: with8.length, boys8thBeatGirlsChampion: with8.filter(r => r.boys8thBeatGirlsChampion).length, }]; const H2H_EVENTS = ['100m', '200m', '400m', '800m', 'High jump', 'Pole vault', 'Long jump', 'Triple jump']; const h2h5 = []; for (const y of YEARS) { const row = { year: y }; let total = 0; for (const e of H2H_EVENTS) { const w = parisRows.find(r => r.event === e && r.sex === 'Women'); const n = wiaa.filter(r => r.year === y && r.sex === 'Boys' && r.event === e && better(r.value, w.value, r.unit)).length; row[e] = n; total += n; } row.total = total; h2h5.push(row); } // ---------- U.S. national high-school records (outdoor) ---------- // Transcribed from the Wikipedia list (archived 2026-10-12); athlete names omitted. const REC = [ ['100m', 'Boys', '9.92', 2025, 'Texas', 'State high-school championship'], ['100m', 'Girls', '10.89', 2023, 'North Carolina', 'Open meet'], ['400m', 'Boys', '44.10', 2025, 'Maryland', 'Open meet'], ['400m', 'Girls', '50.69', 2002, 'Florida', 'National championship'], ['800m', 'Boys', '1:42.27', 2025, 'Texas', 'National championship'], ['800m', 'Girls', '1:59.04', 2022, 'Maryland', 'National championship'], ['Mile', 'Boys', '3:53.43', 2001, 'Virginia', 'International invitational'], ['Mile', 'Girls', '4:23.50', 2025, 'Utah', 'Open meet'], ['High jump', 'Boys', '2.31', 2002, 'Texas', 'International junior championship'], ['High jump', 'Girls', '1.94', 2015, 'Nevada', 'Open meet'], ['Pole vault', 'Boys', '6.05', 2018, 'Louisiana', 'International championship'], ['Pole vault', 'Girls', '4.65', 2023, 'Washington', 'International championship'], ['Long jump', 'Boys', '8.18', 2009, 'Texas', 'National championship'], ['Long jump', 'Girls', '6.86', 2015, 'Maine', 'National high-school meet'], ['Triple jump', 'Boys', '16.72', 2004, 'Louisiana', 'National youth championship'], ['Triple jump', 'Girls', '13.71', 2004, 'California', 'National junior championship'], ]; const isJump = e => /jump|vault/i.test(e); const usRecords = REC.map(([event, sex, mark, year, state, meetType]) => ({ event, sex, mark, value: isJump(event) ? +mark : Math.round(secs(mark) * 100) / 100, unit: isJump(event) ? 'meters' : 'seconds', year, schoolState: state, meetType })); const usRecordGap = [...new Set(REC.map(r => r[0]))].map(e => { const b = usRecords.find(r => r.event === e && r.sex === 'Boys'), g = usRecords.find(r => r.event === e && r.sex === 'Girls'); return { event: e, boysRecord: b.mark, girlsRecord: g.mark, gapPct: gap(b.value, g.value, b.unit), olympicChampionGapPct: olyGapPct(e) }; }); // ---------- USA Swimming single-age motivational standards ---------- const swim = JSON.parse(fs.readFileSync(path.join(inDir, 'swim-rows.json'), 'utf8')); const swimAAAA = swim.filter(r => r.level === 'AAAA'); const AGES = [...new Set(swimAAAA.map(r => r.age))].sort((a, b) => a - b); const swimAgeGap = AGES.map(a => { const g = swimAAAA.filter(r => r.age === a).map(r => r.gapPct); const ev = n => { const r = swimAAAA.find(x => x.age === a && x.event === n); return r ? r.gapPct : null; }; return { age: a, events: g.length, medianGapPct: median(g), minGapPct: Math.min(...g), maxGapPct: Math.max(...g), free50ScyGapPct: ev('50 FR SCY'), free100ScyGapPct: ev('100 FR SCY'), free500ScyGapPct: ev('500 FR SCY') }; }); // ---------- Write ---------- fs.writeFileSync(path.join(out, 'wiaa-2022-2026-finals.csv'), csv(wiaa)); fs.writeFileSync(path.join(out, 'us-hs-records.csv'), csv(usRecords)); fs.writeFileSync(path.join(out, 'usa-swimming-standards.csv'), csv(swim)); const analyses = { ...v1, wiaaGap5, wiaaByEvent, wiaaSummary5, h2h5, usRecordGap, swimAgeGap }; fs.writeFileSync(path.join(out, 'analyses.json'), JSON.stringify(analyses, null, 2)); console.log('wiaa rows', wiaa.length, 'records', usRecords.length, 'swim rows', swim.length); console.table(wiaaSummary5); console.table(wiaaByEvent); console.table(h2h5); console.table(usRecordGap); console.table(swimAgeGap);