/* ImpactLab LATAM — pure, documented analytical functions. MIT license. Exploratory estimates are not causal identifications. */ (function(root,factory){const lib=factory();if(typeof module==='object'&&module.exports)module.exports=lib;else root.ImpactEngine=lib;})(typeof globalThis!=='undefined'?globalThis:this,function(){ 'use strict'; const finite=x=>typeof x==='number'&&Number.isFinite(x); const avg=a=>{const v=a.filter(finite);return v.length?v.reduce((s,x)=>s+x,0)/v.length:null;}; const sum=a=>a.reduce((s,x)=>s+(finite(x)?x:0),0); const sd=a=>{const m=avg(a);return m===null?null:Math.sqrt(avg(a.map(x=>(x-m)**2)));}; const round=(x,d=4)=>finite(x)?Number(x.toFixed(d)):null; const clamp=(v,min,max)=>Math.min(max,Math.max(min,v)); const safeYear=y=>Number.isInteger(Number(y))?Number(y):null; const encodeCSV=v=>{const str=String(v??'');const safe=typeof v==='string'&&/^[\s]*[=+@-]/.test(str)?"'"+str:str;return '"'+safe.replace(/"/g,'""')+'"';}; const byYear=(rows,country,indicator,start,end)=>new Map(rows.filter(r=>r.country===country&&r.indicator===indicator&&r.year>=start&&r.year<=end).map(r=>[r.year,finite(r.value)?r.value:null])); const timeline=(rows,opts)=>{ const {country,controls,indicator,start,end}=opts; const t=byYear(rows,country,indicator,start,end); const donors=(controls||[]).filter(c=>c!==country).map(c=>({country:c,values:byYear(rows,c,indicator,start,end)})); return Array.from({length:end-start+1},(_,i)=>{ const year=start+i;const tv=t.get(year)??null;const cv=donors.map(d=>d.values.get(year)??null).filter(finite); return {year,treated:tv,controls:cv.length===donors.length&&cv.length?avg(cv):null,availableControls:cv.length}; }); }; const checkOptions=opts=>{ if(!opts||!opts.country||!opts.indicator)throw Error('Select an outcome and a treated country.'); if(!Array.isArray(opts.controls)||!opts.controls.length)throw Error('Select at least one comparison country.'); if(opts.controls.includes(opts.country))throw Error('The treated country cannot also be a control.'); if(!(Number.isInteger(opts.start)&&Number.isInteger(opts.end)&&Number.isInteger(opts.breakYear)))throw Error('Years must be integers.'); if(opts.end-opts.start<7)throw Error('Use at least eight calendar years.'); if(opts.breakYear-opts.start<3||opts.end-opts.breakYear+1<3)throw Error('Need at least three years before and after the break.'); }; const estimateDid=(rows,opts)=>{ checkOptions(opts); const points=timeline(rows,opts); const paired=points.filter(p=>finite(p.treated)&&finite(p.controls)); const pre=paired.filter(p=>p.yearp.year>=opts.breakYear); if(pre.length<3||post.length<3) return {ok:false,error:`Not enough paired observations: ${pre.length} pre / ${post.length} post; three per period required.`,points,preYears:pre.length,postYears:post.length}; const tpre=avg(pre.map(p=>p.treated)),tpost=avg(post.map(p=>p.treated)); const cpre=avg(pre.map(p=>p.controls)),cpost=avg(post.map(p=>p.controls)); const effect=(tpost-tpre)-(cpost-cpre); return {ok:true,effect,treatedChange:tpost-tpre,controlChange:cpost-cpre,treatedPre:tpre,treatedPost:tpost,controlPre:cpre,controlPost:cpost,preYears:pre.length,postYears:post.length,points,paired,pre,post}; }; const trendSlope=arr=>{ const a=arr.filter(p=>finite(p.value));if(a.length<3)return null; const mx=avg(a.map(p=>p.year)),my=avg(a.map(p=>p.value)); const denom=sum(a.map(p=>(p.year-mx)**2)); return denom>0?sum(a.map(p=>(p.year-mx)*(p.value-my)))/denom:null; }; const pretrend=did=>{ if(!did.ok)return {ok:false}; const t=trendSlope(did.pre.map(p=>({year:p.year,value:p.treated}))); const c=trendSlope(did.pre.map(p=>({year:p.year,value:p.controls}))); return {ok:finite(t)&&finite(c),treatedSlope:t,controlSlope:c,slopeDifference:finite(t)&&finite(c)?t-c:null,preDifferences:did.pre.map(p=>({year:p.year,gap:p.treated-p.controls}))}; }; const seeded=(seed=24317)=>{let a=(Number(seed)||24317)>>>0;return ()=>{a=(a*1664525+1013904223)>>>0;return a/4294967296;};}; const bootstrapDid=(did,n=400,seed=24317)=>{ if(!did.ok)return {ok:false,interval:null,draws:[]}; const rnd=seeded(seed),out=[]; for(let k=0;kArray.from({length:a.length},()=>a[Math.floor(rnd()*a.length)]); const pre=sample(did.pre),post=sample(did.post); out.push((avg(post.map(p=>p.treated-p.controls)))-(avg(pre.map(p=>p.treated-p.controls)))); } const s=out.sort((a,b)=>a-b),q=p=>{const pos=(s.length-1)*p,lo=Math.floor(pos),hi=Math.ceil(pos);return s[lo]+(s[hi]-s[lo])*(pos-lo);}; return {ok:true,draws:out,interval:[q(.025),q(.975)],n,method:'Year-resampling descriptive interval; not causal inference uncertainty.'}; }; function projectSimplex(v){ const u=[...v].sort((a,b)=>b-a);let sumU=0,rho=-1; for(let i=0;i0)rho=i;} if(rho===-1)return Array(v.length).fill(1/v.length); const theta=(sum(u.slice(0,rho+1))-1)/(rho+1); return v.map(x=>Math.max(0,x-theta)); } function fitSimplex(X,y,lambda=.001,iter=1000){ // minimize average squared residual in outcome units scaled by y RMS const n=X.length,m=X[0].length;let w=Array(m).fill(1/m); const scale=Math.max(1,Math.sqrt(avg(y.map(v=>v*v)))||1); const xs=X.map(row=>row.map(v=>v/scale)),ys=y.map(v=>v/scale); const L=(2/n)*sum(xs.map(row=>sum(row.map(v=>v*v))))+2*lambda; const step=1/Math.max(.001,L); for(let k=0;kv*w[j]))-ys[i];for(let j=0;jv-step*grad[j])); if(sum(next.map((v,j)=>Math.abs(v-w[j])))<1e-10){w=next;break;}w=next; } return w; } const syntheticControl=(rows,opts)=>{ checkOptions(opts); if(opts.controls.length<2)return {ok:false,error:'Synthetic control requires at least two distinct donor countries.'}; const years=Array.from({length:opts.end-opts.start+1},(_,i)=>opts.start+i); const mapped=[opts.country,...opts.controls].map(c=>byYear(rows,c,opts.indicator,opts.start,opts.end)); const complete=years.filter(year=>mapped.every(m=>finite(m.get(year)))); const pre=complete.filter(y=>yy>=opts.breakYear); if(pre.length<5||post.length<2)return {ok:false,error:`Need at least five shared pre-break and two post-break years (${pre.length} pre / ${post.length} post available).`,preYears:pre.length,postYears:post.length}; const X=pre.map(y=>mapped.slice(1).map(m=>m.get(y))),y=pre.map(y=>mapped[0].get(y)); const w=fitSimplex(X,y); const points=complete.map(year=>{ const treated=mapped[0].get(year),synthetic=sum(mapped.slice(1).map((m,j)=>m.get(year)*w[j])); return {year,treated,synthetic,gap:treated-synthetic}; }); const prePts=points.filter(p=>p.yearp.year>=opts.breakYear); const rmse=a=>Math.sqrt(avg(a.map(p=>p.gap*p.gap))); const preRmse=rmse(prePts),postRmse=rmse(postPts); return {ok:true,weights:opts.controls.map((country,i)=>({country,weight:w[i]})),points,preYears:pre.length,postYears:post.length,preRmse,postRmse,postMeanGap:avg(postPts.map(p=>p.gap)),postCumulativeGap:sum(postPts.map(p=>p.gap)),fitRatio:preRmse>1e-10?postRmse/preRmse:null}; }; const unitPlacebos=(rows,opts)=>{ const candidates=[opts.country,...opts.controls],results=[]; for(const fake of candidates){const donor=candidates.filter(c=>c!==fake);if(donor.length<2)continue; try{const r=syntheticControl(rows,{...opts,country:fake,controls:donor});if(r.ok)results.push({country:fake,treated:fake===opts.country,postMeanGap:r.postMeanGap,preRmse:r.preRmse,fitRatio:r.fitRatio});}catch(e){} } return results; }; const timePlacebos=(rows,opts)=>{ const years=[];for(let t=opts.start+3;t<=opts.breakYear-3;t++){ try{const r=estimateDid(rows,{...opts,breakYear:t,end:opts.breakYear-1});if(r.ok)years.push({year:t,effect:r.effect});}catch(e){} }return years; }; const coverage=(rows,opts)=>{ const relevant=rows.filter(r=>r.indicator===opts.indicator&&[opts.country,...opts.controls].includes(r.country)&&r.year>=opts.start&&r.year<=opts.end); return [opts.country,...opts.controls].map(country=>{ const arr=relevant.filter(r=>r.country===country&&finite(r.value));const years=arr.map(r=>r.year); return {country,present:arr.length,expected:opts.end-opts.start+1,latest:years.length?Math.max(...years):null,missing:opts.end-opts.start+1-arr.length}; }); }; function parseWorldBank(payload,indicator){ if(!Array.isArray(payload)||!Array.isArray(payload[1]))throw Error('World Bank API response has no observation array.'); if(payload[0]&&payload[0].message)throw Error('World Bank returned an error.'); return payload[1].map(r=>({country:r.countryiso3code||r.country?.id||'',year:safeYear(r.date),indicator,value:finite(r.value)?r.value:null,source:'worldbank'})).filter(r=>r.country&&r.year!==null); } function validateRows(input){ if(!Array.isArray(input))throw Error('Rows must be an array.'); const out=[],seen=new Set();for(const r of input){ if(!r||!r.country||!r.indicator||!Number.isInteger(Number(r.year)))continue; const country=String(r.country).slice(0,20),indicator=String(r.indicator).slice(0,60),year=Number(r.year); if(year<1900||year>2100)continue;const value=r.value===null||r.value===''?null:Number(r.value); if(value!==null&&!finite(value))continue; const id=`${country}|${indicator}|${year}`;if(seen.has(id))continue;seen.add(id); out.push({country,indicator,year,value,source:String(r.source||'user').slice(0,60)}); }return out; } function toCSV(rows,keys){return [keys.map(encodeCSV).join(','),...rows.map(r=>keys.map(k=>encodeCSV(r[k])).join(','))].join('\r\n')+'\r\n';} const indicators={ 'SP.DYN.LE00.IN':{name:'Life expectancy at birth',unit:'years',decimals:2,domain:'Health'}, 'NY.GDP.PCAP.KD.ZG':{name:'GDP per capita growth',unit:'annual %',decimals:2,domain:'Economy'}, 'SL.UEM.TOTL.ZS':{name:'Unemployment (modeled ILO)',unit:'% of labor force',decimals:2,domain:'Labor'}, 'SE.SEC.ENRR':{name:'School enrollment, secondary (gross)',unit:'%',decimals:2,domain:'Education'}, 'IT.NET.USER.ZS':{name:'Individuals using the Internet',unit:'% of population',decimals:2,domain:'Digital'}, 'SH.XPD.CHEX.GD.ZS':{name:'Current health expenditure',unit:'% of GDP',decimals:2,domain:'Health'}, 'SE.XPD.TOTL.GD.ZS':{name:'Government expenditure on education',unit:'% of GDP',decimals:2,domain:'Education'}, 'SP.POP.TOTL':{name:'Population, total',unit:'people',decimals:0,domain:'Demography'} }; const countries={ARG:'Argentina',BOL:'Bolivia',BRA:'Brazil',CHL:'Chile',COL:'Colombia',CRI:'Costa Rica',DOM:'Dominican Republic',ECU:'Ecuador',GTM:'Guatemala',MEX:'Mexico',PAN:'Panama',PER:'Peru',PRY:'Paraguay',URY:'Uruguay',SLV:'El Salvador'}; return {finite,avg,sum,sd,round,clamp,timeline,checkOptions,estimateDid,pretrend,bootstrapDid,projectSimplex,fitSimplex,syntheticControl,unitPlacebos,timePlacebos,coverage,parseWorldBank,validateRows,toCSV,indicators,countries}; });