Hugdrigo's picture
Upload 19 files
3939ca3 verified
Raw History Blame Contribute Delete
7.06 kB
/* Education Data Lab: deterministic analytics, no external dependencies. */
export const COUNTRIES = Object.freeze({
ARG:'Argentina',BOL:'Bolivia',BRA:'Brazil',CHL:'Chile',COL:'Colombia',CRI:'Costa Rica',DOM:'Dominican Republic',ECU:'Ecuador',MEX:'Mexico',PAN:'Panama',PER:'Peru',PRY:'Paraguay',URY:'Uruguay',GTM:'Guatemala',HND:'Honduras',SLV:'El Salvador'
});
export const INDICATORS = Object.freeze({
'SE.PRM.CMPT.ZS':{short:'Primary completion',label:'Primary completion rate',unit:'%',group:'Education',tip:'Gross intake ratio to last grade; may exceed 100. Not cohort graduation.'},
'SE.SEC.ENRR':{short:'Secondary enrollment',label:'Secondary school enrollment, gross',unit:'%',group:'Education',tip:'Gross enrollment is not net enrollment; can exceed 100.'},
'SE.TER.ENRR':{short:'Tertiary enrollment',label:'Tertiary school enrollment, gross',unit:'%',group:'Education',tip:'Gross enrollment, not degree completion; can exceed 100.'},
'SE.XPD.TOTL.GD.ZS':{short:'Education spending',label:'Government expenditure on education, total',unit:'% GDP',group:'Education',tip:'Government spending as a share of GDP; not per-student investment.'},
'SE.ADT.LITR.ZS':{short:'Adult literacy',label:'Literacy rate, adult total (15+)',unit:'%',group:'Education',tip:'Survey/reporting coverage can be sparse.'},
'IT.NET.USER.ZS':{short:'Internet use',label:'Individuals using the Internet',unit:'%',group:'Context',tip:'Context series; not a direct education outcome.'},
'NY.GDP.PCAP.CD':{short:'GDP per capita',label:'GDP per capita, current US$',unit:'US$',group:'Context',tip:'Nominal current USD is affected by prices and FX.'},
'SP.POP.TOTL':{short:'Population',label:'Population, total',unit:'people',group:'Context',tip:'Context series; not a schooling population.'}
});
export const IDS = Object.keys(INDICATORS);
export const COUNTRY_IDS = Object.keys(COUNTRIES);
export function validYear(y){return Number.isInteger(Number(y))&&Number(y)>=1990&&Number(y)<=2026;}
export function finite(v){return typeof v==='number'&&Number.isFinite(v);}
export function normalizeRows(rows){
if(!Array.isArray(rows))throw new Error('Expected array of observations');
const out=[],seen=new Set();
for(const row of rows){
if(!row||!COUNTRIES[row.iso3]||!INDICATORS[row.indicator]||!validYear(row.year))continue;
if(row.value===null||row.value===undefined||row.value==='')continue;
const val=typeof row.value==='number'?row.value:Number(row.value);
if(!finite(val))continue;
const key=[row.iso3,Number(row.year),row.indicator].join('|');
if(seen.has(key))continue;seen.add(key);
out.push({iso3:row.iso3,year:Number(row.year),indicator:row.indicator,value:val,footnote:String(row.footnote||'').slice(0,240),status:String(row.status||'').slice(0,24)});
}
return out.sort((a,b)=>a.year-b.year||a.iso3.localeCompare(b.iso3)||a.indicator.localeCompare(b.indicator));
}
export function subset(rows,cc,indicator,start,end){return rows.filter(r=>cc.includes(r.iso3)&&r.indicator===indicator&&r.year>=start&&r.year<=end);}
export function latest(rows,country,ind,start,end){const a=rows.filter(r=>r.iso3===country&&r.indicator===ind&&r.year>=start&&r.year<=end); return a.length?a.reduce((best,x)=>x.year>best.year?x:best):null;}
export function paired(rows,x,y,cc,start,end){
const selected=rows.filter(r=>cc.includes(r.iso3)&&[x,y].includes(r.indicator)&&r.year>=start&&r.year<=end);const g=new Map();
for(const r of selected){const k=r.iso3+'|'+r.year;if(!g.has(k))g.set(k,{iso3:r.iso3,year:r.year});g.get(k)[r.indicator]=r.value;}
return [...g.values()].filter(o=>finite(o[x])&&finite(o[y])).sort((a,b)=>a.year-b.year||a.iso3.localeCompare(b.iso3));
}
export function pearson(points,x,y){if(points.length<3)return null;const a=points.map(p=>p[x]),b=points.map(p=>p[y]);const n=a.length,ma=a.reduce((s,v)=>s+v,0)/n,mb=b.reduce((s,v)=>s+v,0)/n;let num=0,da=0,db=0;for(let i=0;i<n;i++){const u=a[i]-ma,v=b[i]-mb;num+=u*v;da+=u*u;db+=v*v;}return da>0&&db>0?num/Math.sqrt(da*db):null;}
export function growth(oldValue,newValue){return finite(oldValue)&&finite(newValue)&&oldValue!==0?100*(newValue/oldValue-1):null;}
export function change(first,last){return first&&last&&first.year!==last.year?{amount:last.value-first.value,from:first.year,to:last.year}:null;}
export function coverage(rows,countries,inds,start,end){const years=end-start+1,eligible=countries.length*inds.length*years; const count=rows.filter(r=>countries.includes(r.iso3)&&inds.includes(r.indicator)&&r.year>=start&&r.year<=end).length;return {count,eligible,ratio:eligible?count/eligible:0};}
export function status(rows,countries,inds,start,end){const c=coverage(rows,countries,inds,start,end);let lags=[];for(const cc of countries)for(const i of inds){const x=latest(rows,cc,i,start,end);if(x)lags.push(end-x.year);}return { ...c,meanLag:lags.length?lags.reduce((a,b)=>a+b,0)/lags.length:null,availablePairs:lags.length,totalPairs:countries.length*inds.length};}
export function formatValue(n,ind){if(!finite(n))return 'N/A';const unit=INDICATORS[ind]?.unit||'%';if(unit==='US$')return '$'+Intl.NumberFormat('en-US',{notation:'compact',maximumFractionDigits:2}).format(n);if(unit==='people')return Intl.NumberFormat('en-US',{notation:'compact',maximumFractionDigits:1}).format(n);return n.toFixed(Math.abs(n)<10?2:1)+(unit==='% GDP'?'% GDP':'%');}
export function escapeHTML(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
export function csvCell(v){let s=String(v??'');if(typeof v==='string'&&/^[=+\-@\t\r]/.test(s))s="'"+s;return /[",\n\r]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;}
export function toCSV(rows){const cols=['iso3','country','year','indicator','indicator_label','value','unit','footnote','observation_status'];return [cols.join(','),...rows.map(r=>[r.iso3,COUNTRIES[r.iso3],r.year,r.indicator,INDICATORS[r.indicator]?.label,r.value,INDICATORS[r.indicator]?.unit,r.footnote||'',r.status||''].map(csvCell).join(','))].join('\r\n');}
export function validateSession(x){if(!x||x.format!=='edlab-session-v1'||!Array.isArray(x.rows)||x.rows.length>25000)throw new Error('Invalid Education Data Lab session');const rows=normalizeRows(x.rows);if(!rows.length)throw new Error('No supported observations');return {rows,mode:'imported',importedAt:new Date().toISOString(),sourceNote:'Imported user session; provenance not independently verified.'};}
export function timeline(rows,countries,ind,start,end){const a=subset(rows,countries,ind,start,end);return countries.map(cc=>({iso3:cc,points:a.filter(r=>r.iso3===cc).sort((a,b)=>a.year-b.year)}));}
export function compareCountries(rows,countries,ind,start,end){return countries.map(c=>({country:c,point:latest(rows,c,ind,start,end)})).filter(x=>x.point).sort((a,b)=>b.point.value-a.point.value);}
export function crossCountryGap(rows,a,b,ind,start,end,year){const p=rows.find(r=>r.iso3===a&&r.indicator===ind&&r.year===year),q=rows.find(r=>r.iso3===b&&r.indicator===ind&&r.year===year);return p&&q?{year,delta:p.value-q.value,a:p.value,b:q.value}:null;}