// Builds the saved-analysis results for dataset testosterone-suppression-athletic-measures // from data/harper-12-month-study-groups.csv and data/air-force-fitness-trans-women.csv. // Usage: node build.js -> writes data/analyses.json const fs = require('fs'), path = require('path'); const dd = path.join(__dirname, 'data'); function readCsv(file) { const [head, ...lines] = fs.readFileSync(path.join(dd, file), 'utf8').trim().split(/\r?\n/); const cols = head.split(','); return lines.map(l => { const v = l.split(','); return Object.fromEntries(cols.map((c, i) => [c, v[i] ?? ''])); }); } const num = s => (s === '' ? null : Number(s)); const round1 = x => Math.round(x * 10) / 10; const median = a => { const s = [...a].sort((x, y) => x - y), m = s.length >> 1; return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; }; // 1. Median % change after about 12 months, adult study groups in Harper et al. (2021) Tables 2-5. const groups = readCsv('harper-12-month-study-groups.csv'); const pct = g => g.pctChangeReported !== '' ? num(g.pctChangeReported) : round1((num(g.followUp) - num(g.baseline)) / num(g.baseline) * 100); const MEASURES = [ ['testosterone', 'Testosterone'], ['hemoglobin', 'Hemoglobin (red blood cells)'], ['muscleArea', 'Muscle size'], ['leanMass', 'Lean body mass'], ['strength', 'Strength'], ]; const twelveMonth = MEASURES.map(([id, label], order) => { const vals = groups.filter(g => g.measure === id && g.included === 'yes').map(pct); return { label, measure: id, order: order + 1, estimate: round1(median(vals)), ciLow: Math.min(...vals), ciHigh: Math.max(...vals), intervalType: 'range', unit: '% change after about 12 months', n: vals.length, method: 'Median of the percentage changes in the included study groups; the range is the lowest and highest study group.', }; }); // Exact per-group values, so readers can check every median. const groupRows = groups.map(g => ({ measure: g.measure, study: g.study, group: g.group || null, detail: g.detail, n: num(g.n), months: g.months, pctChange: pct(g), included: g.included === 'yes', exclusionReason: g.exclusionReason || null })); // 2. Air Force fitness tests: trans women vs the average Air Force woman under 30 (Roberts et al. 2021, Table 4). const af = readCsv('air-force-fitness-trans-women.csv'); const BIN = ['Year 0 (before)', 'Year 0-1', 'Year 1-2', 'Year 2-2.5']; const avgWoman = m => { const r = af.find(x => x.measure === m && x.binOrder === '0'); return num(r.transWomenMean) - num(r.diffVsAverageWoman); }; const runGap = af.filter(r => r.measure === 'run').map(r => ({ timeOnHormones: BIN[num(r.binOrder)], estimate: -num(r.diffVsAverageWoman), ciLow: -num(r.diffCiHigh), ciHigh: -num(r.diffCiLow), ciLevel: 0.95, unit: 'seconds faster than the average Air Force woman over 1.5 miles', transWomenSeconds: num(r.transWomenMean), averageWomanSeconds: avgWoman('run'), speedAdvantagePct: round1((avgWoman('run') / num(r.transWomenMean) - 1) * 100), method: 'Roberts et al. (2021) Table 4 mean difference from the average Air Force woman under 30, sign reversed so faster is positive; 95% CI from the paper.', })); const allMeasures = af.map(r => { const w = avgWoman(r.measure), t = num(r.transWomenMean); return { measure: r.measure, timeOnHormones: BIN[num(r.binOrder)], transWomenMean: t, averageWoman: round1(w), diffVsAverageWoman: num(r.diffVsAverageWoman), diffCiLow: num(r.diffCiLow), diffCiHigh: num(r.diffCiHigh), // Run: % higher speed (= paper's convention). Push-ups and sit-ups: % more repetitions than the average woman. advantagePct: r.measure === 'run' ? round1((w / t - 1) * 100) : round1((t / w - 1) * 100), paperConventionPct: round1(Math.abs(num(r.diffVsAverageWoman)) / t * 100) * Math.sign(r.measure === 'run' ? -num(r.diffVsAverageWoman) : num(r.diffVsAverageWoman)), significantAt95: !(num(r.diffCiLow) < 0 && num(r.diffCiHigh) > 0), }; }); const out = { twelveMonth, groupRows, runGap, allMeasures, averageWoman: { pushups: round1(avgWoman('pushups')), situps: round1(avgWoman('situps')), runSeconds: avgWoman('run') } }; fs.writeFileSync(path.join(dd, 'analyses.json'), JSON.stringify(out, null, 1) + '\n'); console.log(JSON.stringify({ twelveMonth, runGap, allMeasures, averageWoman: out.averageWoman }, null, 1));