// Build hic_panel.csv from HUD 2007-2025 HIC Counts by CoC (sheet CSVs) + HUD PIT sheets + existing panel population. // Usage: node build_hic.js const fs = require('fs'), path = require('path'); const { readCSV, num, toCSV } = require('./csvlib'); const HIC = path.resolve(process.argv[2]), DATA = path.resolve(process.argv[3]); const YEARS = []; for (let y = 2012; y <= 2025; y++) YEARS.push(y); // CoC list and population from existing panel const panel = readCSV(path.join(DATA, 'coc_homelessness_rent_panel.csv')).filter(r => r.length > 1); const ph = panel[0]; const P = r => Object.fromEntries(ph.map((h, i) => [h, r[i]])); const cocs = {}; const pop = {}; for (const r of panel.slice(1).map(P)) { cocs[r.coc_code] = cocs[r.coc_code] || { short: r.short_name, cat: r.hud_category_2025, name: r.coc_name }; cocs[r.coc_code].name = r.coc_name; // latest name pop[r.coc_code + '|' + r.year] = num(r.population); } const map = readCSV(path.join(DATA, 'coc_geography_map.csv')).filter(r => r.length > 1); const alias = {}; for (const r of map.slice(1)) for (const a of (r[1] || '').split(';').filter(Boolean)) alias[a] = r[0]; const codeOf = c => alias[c] || c; console.error('CoCs:', Object.keys(cocs).length); // HIC function hicYear(y) { const rows = readCSV(path.join(HIC, 'raw', 'hic2025_csv', `${y}.csv`)); const hi = rows.findIndex(r => /^CoC( Number)?$/.test((r[0] || '').trim())); const h = rows[hi].map(s => s.trim()); const first = (...names) => { for (const n of names) { const i = h.indexOf(n); if (i >= 0) return i; } return -1; }; const col = { es: first('Total Year-Round Beds (ES)', 'Total Year-Round ES Beds'), th: first('Total Year-Round Beds (TH)', 'Total Year-Round TH Beds'), sh: first('Total Year-Round Beds (SH)', 'Total Year-Round SH Beds'), rrh: first('Total Year-Round Beds (RRH & DEM)', 'Total Year-Round Beds (RRH)', 'Total Year-Round RRH Beds'), psh: first('Total Year-Round Beds (PSH)', 'Total Year-Round PSH Beds'), oph: first('Total Year-Round Beds (OPH)'), seasonal: first('Total Seasonal Beds (ES)', 'Total Seasonal ES Beds'), overflow: first('Total Overflow Beds (ES)', 'Total Overflow ES Beds'), }; for (const k of ['es', 'th', 'sh', 'psh', 'seasonal', 'overflow']) if (col[k] < 0) throw new Error(`${y} missing ${k}`); const out = {}; for (const r of rows.slice(hi + 1)) { const c = (r[0] || '').trim(); if (!/^[A-Z]{2}-\d/.test(c)) continue; const o = {}; for (const [k, i] of Object.entries(col)) o[k] = i >= 0 ? num(r[i]) : null; out[codeOf(c)] = o; } return out; } // PIT function pitYear(y) { const rows = readCSV(path.join(DATA, 'raw', 'pit2025_csv', `${y}.csv`)); const h = rows[0].map(s => s.trim()); const iT = h.indexOf('Overall Homeless'), iS = h.indexOf('Sheltered Total Homeless'), iU = h.indexOf('Unsheltered Homeless'), iC = h.indexOf('Count Types'); const out = {}; for (const r of rows.slice(1)) { const c = (r[0] || '').trim(); if (!/^[A-Z]{2}-\d/.test(c)) continue; out[codeOf(c)] = { total: num(r[iT]), sheltered: num(r[iS]), unsheltered: num(r[iU]), countType: iC >= 0 ? r[iC] : '' }; } return out; } const rnd = (v, d) => v == null || !isFinite(v) ? null : Math.round(v * 10 ** d) / 10 ** d; const sum = (...a) => a.some(v => v == null) ? null : a.reduce((s, v) => s + v, 0); const header = ['coc_code', 'coc_name', 'short_name', 'hud_category_2025', 'year', 'es_beds', 'th_beds', 'sh_beds', 'rrh_beds', 'psh_beds', 'oph_beds', 'es_seasonal_beds', 'es_overflow_beds', 'temp_beds', 'perm_beds', 'pit_count_type', 'pit_total', 'pit_sheltered', 'pit_unsheltered', 'population', 'temp_beds_per_homeless', 'unsheltered_share', 'temp_beds_per_10k', 'perm_beds_per_10k']; const out = [header]; const missing = []; for (const y of YEARS) { const H = hicYear(y), PT = pitYear(y); for (const c of Object.keys(cocs).sort()) { const h = H[c], p = PT[c] || {}; if (!h) { missing.push(`${y} ${c} HIC`); continue; } if (!PT[c]) missing.push(`${y} ${c} PIT`); const oph = h.oph == null && y <= 2013 ? null : (h.oph ?? 0), rrh = h.rrh == null && y <= 2012 ? null : (h.rrh ?? 0); const temp = sum(h.es, h.th, h.sh ?? 0), perm = sum(rrh ?? 0, h.psh, oph ?? 0); // 2012-13: RRH/OPH not reported -> treated as 0 in perm (flagged in notes) const popv = pop[c + '|' + y] ?? null; const shelteredOnly = /sheltered.only/i.test(p.countType || ''); out.push([c, cocs[c].name, cocs[c].short, cocs[c].cat, y, h.es, h.th, h.sh, rrh, h.psh, oph, h.seasonal, h.overflow, temp, perm, p.countType, p.total, p.sheltered, p.unsheltered, popv, rnd(p.total ? temp / p.total : null, 3), rnd(p.total && !shelteredOnly ? p.unsheltered / p.total : null, 3), rnd(popv ? temp / popv * 1e4 : null, 2), rnd(popv ? perm / popv * 1e4 : null, 2)]); } } fs.writeFileSync(path.join(HIC, 'hic_panel.csv'), toCSV(out)); console.error('rows', out.length - 1, 'missing:', missing.join('; ') || 'none');