Hugdrigo's picture
Upload 15 files
1044fe4 verified
Raw History Blame Contribute Delete
4.92 kB
export const finite = x => typeof x === 'number' && Number.isFinite(x);
export const safeDiv = (a,b) => finite(a)&&finite(b)&&b!==0 ? a/b : null;
export const validNum = v => typeof v === 'number' && Number.isFinite(v) ? v : null;
export const isAdjacent = (a,b) => b.year===a.year+1;
// Observations are represented as country:year records, missing values remain null.
export function mergeRows(rows){
const byKey = new Map();
for(const r of rows){
if(!r || typeof r.iso!=='string' || !Number.isInteger(r.year) || !['exports','imports','gdp'].includes(r.indicator)) continue;
const key=`${r.iso}:${r.year}`;
if(!byKey.has(key)) byKey.set(key,{iso:r.iso,year:r.year,exports:null,imports:null,gdp:null});
byKey.get(key)[r.indicator]=validNum(r.value);
}
return byKey;
}
export function calculateRecord(byKey,iso,year){
const r=byKey.get(`${iso}:${year}`) || {iso,year,exports:null,imports:null,gdp:null};
const e=r.exports, i=r.imports, g=r.gdp;
const complete=finite(e)&&finite(i);
const old=byKey.get(`${iso}:${year-1}`);
const eprev=old?.exports;
const iprev=old?.imports;
return {
...r,
balance:complete ? e-i : null,
turnover:complete ? e+i : null,
cover:complete && i>0 ? (e/i)*100 : null,
openness:complete && finite(g) && g>0 ? (e+i)/g*100 : null,
exportGrowth:finite(e)&&finite(eprev)&&eprev>0 ? ((e/eprev)-1)*100 : null,
importGrowth:finite(i)&&finite(iprev)&&iprev>0 ? ((i/iprev)-1)*100 : null
};
}
export function snapshot(byKey,countries,year){return countries.map(c=>calculateRecord(byKey,c,year));}
export function periodRecords(byKey,iso,start,end){const a=[];for(let y=start;y<=end;y++)a.push(calculateRecord(byKey,iso,y));return a;}
export function stdev(values){
const a=values.filter(finite); if(a.length<2)return null;
const avg=a.reduce((s,x)=>s+x,0)/a.length;
return Math.sqrt(a.reduce((s,x)=>s+(x-avg)**2,0)/a.length);
}
export function cagr(records,key){
const a=records.filter(r=>finite(r[key])&&r[key]>0);
if(a.length<2) return null;
const first=a[0],last=a[a.length-1];
const span=last.year-first.year;
return span>0 ? (Math.pow(last[key]/first[key],1/span)-1)*100 : null;
}
export function volatility(records,key='exportGrowth'){return stdev(records.map(r=>r[key]));}
export function coverage(byKey,countries,start,end){
const expected=countries.length*(end-start+1);
const out={exports:0,imports:0,gdp:0,paired:0,allThree:0};
for(const iso of countries){
for(let y=start;y<=end;y++){
const r=calculateRecord(byKey,iso,y);
for(const key of ['exports','imports','gdp']) if(finite(r[key]))out[key]++;
if(finite(r.balance))out.paired++;
if(finite(r.openness))out.allThree++;
}
}
return {...out,expected};
}
export function aggregate(records){
const paired=records.filter(r=>finite(r.balance));
const exports=paired.reduce((a,r)=>a+r.exports,0);
const imports=paired.reduce((a,r)=>a+r.imports,0);
const withGdp=paired.filter(r=>finite(r.gdp)&&r.gdp>0);
const tradeGdp=withGdp.reduce((a,r)=>a+r.turnover,0);
const gdp=withGdp.reduce((a,r)=>a+r.gdp,0);
return {
exports:paired.length?exports:null,
imports:paired.length?imports:null,
balance:paired.length?exports-imports:null,
openness:gdp>0?tradeGdp/gdp*100:null,
paired:paired.length,
total:records.length,
gdpPaired:withGdp.length
};
}
export function shock(record,exportPercent,importPercent){
if(!finite(record.exports)||!finite(record.imports))return null;
const ex=record.exports*(1+exportPercent/100), im=record.imports*(1+importPercent/100);
return {exports:ex,imports:im,balance:ex-im,delta:(ex-im)-(record.exports-record.imports),cover:im>0?ex/im*100:null};
}
export function rankAvailable(records,metric){
return records.filter(r=>finite(r[metric])).sort((a,b)=>b[metric]-a[metric]);
}
export function computeInsights(records,allRecords,start,end){
const comparable=rankAvailable(records,'balance');
const trends=allRecords.map(({iso,records:r})=>({iso,cagr:cagr(r,'exports'),vol:volatility(r)}));
const leads=trends.filter(x=>finite(x.cagr)).sort((a,b)=>b.cagr-a.cagr);
const bullets=[];
if(comparable.length){
bullets.push({kind:'balance',iso:comparable[0].iso,title:'Highest merchandise balance',value:comparable[0].balance,unit:'usd',year:comparable[0].year});
bullets.push({kind:'balance',iso:comparable[comparable.length-1].iso,title:'Lowest merchandise balance',value:comparable[comparable.length-1].balance,unit:'usd',year:comparable[comparable.length-1].year});
}
if(leads.length)bullets.push({kind:'growth',iso:leads[0].iso,title:`Fastest export CAGR (${start}–${end})`,value:leads[0].cagr,unit:'pct',note:'CAGR between first and last positive available observations'});
return bullets;
}
export function rowsForCsv(byKey,countries,start,end){
return countries.flatMap(iso=>periodRecords(byKey,iso,start,end));
}