Spaces:
Running
Running
Download engine.js from Hugdrigo/policylab-ai: direct link, hf CLI and curl.
- Browser
- Download file 5.06 kB
-
https://huggingface.co/spaces/Hugdrigo/policylab-ai/resolve/main/engine.js
- Command line
-
hf download hf://spaces/Hugdrigo/policylab-ai/engine.js
-
curl -L -o engine.js https://huggingface.co/spaces/Hugdrigo/policylab-ai/resolve/main/engine.js
5.06 kB
| import {COUNTRIES,IDS,INDICATORS,DIMENSIONS,DIMENSION_CODES,finite,safeNumber,clamp} from './catalog.js'; | |
| /** Preserve actual observations, do not forward-fill or interpolate. */ | |
| export function normalize(raw){ | |
| if(!Array.isArray(raw))throw Error('Rows must be an array');const seen=new Set(),out=[]; | |
| for(const row of raw){if(!row||!COUNTRIES[row.country]||!IDS.includes(row.indicator))continue; | |
| const year=Number(row.year),value=safeNumber(row.value); | |
| if(!Number.isInteger(year)||year<1990||year>2026||value===null)continue; | |
| const key=`${row.country}|${row.indicator}|${year}`;if(seen.has(key))continue;seen.add(key); | |
| out.push({country:row.country,year,indicator:row.indicator,value,obsStatus:String(row.obsStatus||'').slice(0,30)}); | |
| } | |
| return out.sort((a,b)=>a.year-b.year||a.country.localeCompare(b.country)||a.indicator.localeCompare(b.indicator)); | |
| } | |
| export function latest(rows,country,indicator,start,end){let best=null;for(const r of rows){if(r.country===country&&r.indicator===indicator&&r.year>=start&&r.year<=end&&(!best||r.year>best.year))best=r;}return best;} | |
| export function exact(rows,country,indicator,year){return rows.find(r=>r.country===country&&r.indicator===indicator&&r.year===year)||null;} | |
| export function series(rows,country,indicator,start,end){return rows.filter(r=>r.country===country&&r.indicator===indicator&&r.year>=start&&r.year<=end).sort((a,b)=>a.year-b.year);} | |
| export function score(value,indicator){const def=INDICATORS[indicator];if(!def||!DIMENSION_CODES.includes(indicator)||!finite(value))return null; | |
| let n=def.transform==='log'?100*(Math.log(Math.max(1,value))-Math.log(def.lo))/(Math.log(def.hi)-Math.log(def.lo)):100*(value-def.lo)/(def.hi-def.lo); | |
| n=clamp(n,0,100);return def.positive?n:100-n; | |
| } | |
| export function cleanWeights(weights){const w=DIMENSIONS.map(k=>{const n=Number(weights?.[k]);return Number.isFinite(n)&&n>=0?n:0;});const s=w.reduce((a,b)=>a+b,0);return Object.fromEntries(DIMENSIONS.map((k,i)=>[k,s?w[i]/s:1/5]));} | |
| /** Full five dimension completeness required to publish rankings. Partial observations remain visible separately. */ | |
| export function composite(rows,countries,weights,start,end,maxLag=5){ | |
| const normW=cleanWeights(weights);return countries.map(country=>{ | |
| const parts=DIMENSIONS.map((d,i)=>{const id=DIMENSION_CODES[i],p=latest(rows,country,id,start,end); | |
| return {dimension:d,indicator:id,year:p?.year??null,raw:p?.value??null,score:p&&end-p.year<=maxLag?score(p.value,id):null,lag:p?end-p.year:null};}); | |
| const ready=parts.every(p=>finite(p.score));const index=ready?parts.reduce((s,p)=>s+p.score*normW[p.dimension],0):null; | |
| return {country,index,eligible:ready,covered:parts.filter(p=>finite(p.score)).length,parts,oldestYear:ready?Math.min(...parts.map(p=>p.year)):null,newestYear:ready?Math.max(...parts.map(p=>p.year)):null}; | |
| }).sort((a,b)=>(b.index??-1)-(a.index??-1)); | |
| } | |
| export function coverage(rows,countries,indicators,start,end){const eligible=countries.length*indicators.length*(end-start+1);const actual=rows.filter(r=>countries.includes(r.country)&&indicators.includes(r.indicator)&&r.year>=start&&r.year<=end).length;return {eligible,actual,share:eligible?actual/eligible:0};} | |
| export function missingMatrix(rows,countries,indicators,start,end){return countries.flatMap(c=>indicators.map(id=>{const arr=series(rows,c,id,start,end);return {country:c,indicator:id,available:arr.length,expected:end-start+1,latestYear:arr.length?arr[arr.length-1].year:null,missing:end-start+1-arr.length};}));} | |
| export function paired(rows,countries,x,y,start,end){const out=[];for(const c of countries)for(let year=start;year<=end;year++){const a=exact(rows,c,x,year),b=exact(rows,c,y,year);if(a&&b)out.push({country:c,year,x:a.value,y:b.value});}return out;} | |
| export function pearson(points){if(points.length<4)return null;const a=points.map(p=>p.x),b=points.map(p=>p.y),n=a.length,ma=a.reduce((x,y)=>x+y,0)/n,mb=b.reduce((x,y)=>x+y,0)/n;let num=0,sa=0,sb=0;for(let i=0;i<n;i++){const x=a[i]-ma,y=b[i]-mb;num+=x*y;sa+=x*x;sb+=y*y;}return sa>0&&sb>0?num/Math.sqrt(sa*sb):null;} | |
| export function trendChange(points){if(points.length<2)return null;const first=points[0],last=points[points.length-1];if(first.year===last.year)return null;return {from:first.year,to:last.year,absolute:last.value-first.value,annualChange:(last.value-first.value)/(last.year-first.year)};} | |
| export function giniOfScores(results){const a=results.filter(x=>finite(x.index)).map(x=>x.index).sort((x,y)=>x-y);if(a.length<2)return null;let total=a.reduce((x,y)=>x+y,0);if(total===0)return 0;let weight=0;for(let i=0;i<a.length;i++)weight+=(i+1)*a[i];return 2*weight/(a.length*total)-(a.length+1)/a.length;} | |
| export function validateImport(data){if(data?.format!=='policylab-session-v1'||!Array.isArray(data.rows)||data.rows.length>80000)throw Error('Invalid PolicyLab session');const rows=normalize(data.rows);if(!rows.length)throw Error('No valid observations');return {rows,kind:'imported',source:'Imported session (user-provided; provenance not independently verified)',year:new Date().getFullYear()};} | |