// Agreement between the first coding and a blind second coder on a 20-story sample. // Usage: node millionaires-tax/data/media_recode_agreement.js const fs = require('fs'), path = require('path'); function parseCsv(t){const rows=[];let row=[],f='',q=false;for(let i=0;i r.length > 1).map(r => { const o = Object.fromEntries(H.map((h, i) => [h, r[i]])); return [o.story_id, o]; })); const lines = fs.readFileSync(process.argv[2], 'utf8').split(/\r?\n/).filter(l => (l.match(/\|/g) || []).length > 15); const hdr = lines[0].split('|').map(s => s.trim()); const second = lines.slice(1).filter(l => !/^-+/.test(l.trim())).map(l => { const c = l.split('|').map(s => s.trim()); return Object.fromEntries(hdr.map((h, i) => [h, c[i] ?? ''])); }); const CODE_RE = /\b(N13B|N460K|NVOTE|N05|NMEALS|N300B|NHEDGE|NBREAK|YCOME|YPENS|YUNCON|YUNEMP|Y90|YLEAVE|YLEAK|YNOK12)\b/g; const codes = v => new Set((v || '').match(CODE_RE) || []); const bin = v => (String(v || '').trim().match(/^[01]/) || [''])[0]; const lean = r => Math.sign((parseFloat(r.yes_sources) || 0) - (parseFloat(r.no_sources) || 0)); const fields = {}; const add = (k, a, b) => { (fields[k] = fields[k] || []).push([a, b]); }; const ids = []; for (const s of second) { const f = first[s.story_id]; if (!f) continue; ids.push(s.story_id); for (const o of ['O1','O2','O3a','O3b','O4','O5','O6','O7','O8','O9']) add(o, bin(f[o]), bin(s[o])); add('first_word', f.first_word, s.first_word); add('last_word', f.last_word, s.last_word); add('rebuttal', f.rebuttal, s.rebuttal); add('source_lean', lean(f), lean(s)); add('any_unchecked_no_claim', codes(f.no_claims_unchecked).size > 0, codes(s.no_claims_unchecked).size > 0); add('any_unchecked_yes_claim', codes(f.yes_claims_unchecked).size > 0, codes(s.yes_claims_unchecked).size > 0); for (const c of ['N13B','N460K','NMEALS','YCOME','YPENS','YLEAVE']) { const st = (r, side) => codes(r[side]).has(c) ? 'unchecked' : codes(r.claims_checked).has(c) ? 'checked' : 'absent'; const side = c[0] === 'N' ? 'no_claims_unchecked' : 'yes_claims_unchecked'; add('claim_' + c, st(f, side), st(s, side)); } } const out = []; // First-pass agreement on O3b and O4, before the voice re-pass that this check triggered (recorded 2026-10-21). const BEFORE = { O3b: { agree: 18, percent_agreement: 90, cohens_kappa: 0.46, disagreements: "K08:0→1 K07:0→1" }, O4: { agree: 18, percent_agreement: 90, cohens_kappa: 0.62, disagreements: "K07:0→1 PUB-12:0→1" } }; for (const [k, pairs] of Object.entries(fields)) { const n = pairs.length, agree = pairs.filter(([a, b]) => String(a) === String(b)).length; // Cohen's kappa const cats = [...new Set(pairs.flat().map(String))]; const pe = cats.reduce((s, c) => s + (pairs.filter(p => String(p[0]) === c).length / n) * (pairs.filter(p => String(p[1]) === c).length / n), 0); const po = agree / n; const kappa = pe < 1 ? (po - pe) / (1 - pe) : null; const dis = pairs.map((p, i) => String(p[0]) !== String(p[1]) ? `${ids[i]}:${p[0]}→${p[1]}` : null).filter(Boolean); if (BEFORE[k]) out.push({ field: k + " (first pass, before re-pass)", stories: n, ...BEFORE[k] }); out.push({ field: k + (BEFORE[k] ? " (after re-pass)" : ""), stories: n, agree, percent_agreement: Math.round(po * 1000) / 10, cohens_kappa: kappa === null ? null : Math.round(kappa * 100) / 100, disagreements: dis.join(' ') }); } const esc = v => /[",\n]/.test(String(v)) ? '"' + String(v).replace(/"/g, '""') + '"' : String(v ?? ''); const cols = ['field', 'stories', 'agree', 'percent_agreement', 'cohens_kappa', 'disagreements']; fs.writeFileSync(path.join(__dirname, 'media_recode_agreement.csv'), [cols.join(',')].concat(out.map(r => cols.map(c => esc(r[c])).join(','))).join('\n') + '\n'); fs.writeFileSync(path.join(__dirname, 'media_recode_agreement.json'), JSON.stringify(out.map(({ disagreements, ...r }) => r), null, 1)); for (const r of out) console.log(r.field.padEnd(26), r.agree + '/' + r.stories, r.percent_agreement + '%', 'k=' + r.cohens_kappa, '|', r.disagreements.slice(0, 160));