Files
website/scripts/stats/imf.ts

130 lines
4.2 KiB
TypeScript

// A4 IMF World Economic Outlook via DataMapper. The country filter in the path is ignored, so
// each call returns every country; keep the compared ones. Each WEO round is kept as a vintage
// (April / October) so forecasts can later be compared with outcomes.
import {
COMPARE_ISO3,
getJson,
insertChunks,
log,
run,
sql,
writeSeries,
type ObsRow,
type SeriesRow
} from './common';
export const IMF_INDICATORS: { id: string; title: string; unit: string; theme: string }[] = [
{ id: 'NGDP_RPCH', title: 'Real GDP growth', unit: '%', theme: 'countries' },
{ id: 'NGDPDPC', title: 'GDP per capita', unit: 'current USD', theme: 'countries' },
{
id: 'PPPPC',
title: 'GDP per capita, PPP',
unit: 'current international $',
theme: 'countries'
},
{ id: 'PCPIPCH', title: 'Inflation (average consumer prices)', unit: '%', theme: 'countries' },
{ id: 'LUR', title: 'Unemployment rate', unit: '%', theme: 'countries' },
{ id: 'GGXWDG_NGDP', title: 'General government gross debt', unit: '% of GDP', theme: 'budget' },
{
id: 'GGXCNL_NGDP',
title: 'General government net lending/borrowing',
unit: '% of GDP',
theme: 'budget'
},
// WEO revenue/spending (GGR_NGDP, GGX_NGDP) are not on DataMapper; the Fiscal Monitor ones are.
{ id: 'GGR_G01_GDP_PT', title: 'General government revenue', unit: '% of GDP', theme: 'budget' },
{
id: 'G_X_G01_GDP_PT',
title: 'General government total expenditure',
unit: '% of GDP',
theme: 'budget'
}
];
// IMF aggregate codes kept besides the countries.
const AGGREGATES: Record<string, string> = { EU: 'EU27', WEOWORLD: 'WLD' };
/** WEO round in force on a date: October and April releases. */
export function currentVintage(now = new Date()) {
const y = now.getUTCFullYear();
const m = now.getUTCMonth() + 1;
return m >= 10 ? `${y}-10` : m >= 4 ? `${y}-04` : `${y - 1}-10`;
}
export async function imf(opts: { only?: string; job: string }) {
const vintage = currentVintage();
const vintageYear = Number(vintage.slice(0, 4));
await run('imf', opts.job, async (note) => {
let total = 0;
for (const ind of IMF_INDICATORS) {
if (opts.only && !opts.only.split(',').includes(ind.id)) continue;
try {
const body = await getJson<{
values?: Record<string, Record<string, Record<string, number>>>;
}>(`https://www.imf.org/external/datamapper/api/v1/${ind.id}`);
const byCountry = body.values?.[ind.id] ?? {};
const rows: {
vintage: string;
indicator: string;
country_iso3: string;
year: number;
value: number;
is_forecast: boolean;
}[] = [];
for (const [code, years] of Object.entries(byCountry)) {
const region = COMPARE_ISO3.includes(code) ? code : AGGREGATES[code];
if (!region) continue;
for (const [y, v] of Object.entries(years))
if (v != null)
rows.push({
vintage,
indicator: ind.id,
country_iso3: region,
year: Number(y),
value: v,
is_forecast: Number(y) >= vintageYear
});
}
if (!rows.length) throw new Error('no rows');
await sql.begin(async (tx) => {
await tx`delete from stats.imf_weo where vintage = ${vintage} and indicator = ${ind.id}`;
await insertChunks(tx, 'imf_weo', rows);
});
const series: SeriesRow[] = [];
const obs: ObsRow[] = [];
for (const region of new Set(rows.map((r) => r.country_iso3))) {
const id = `imf:${ind.id}:${region}`;
series.push({
series_id: id,
source_id: 'imf',
source_code: `${ind.id}/${region}`,
title: ind.title,
unit: ind.unit,
freq: 'A',
region_code: region,
theme: ind.theme,
meta: {
vintage,
forecastFrom: vintageYear,
url: `https://www.imf.org/external/datamapper/${ind.id}`
}
});
for (const r of rows)
if (r.country_iso3 === region)
obs.push({
series_id: id,
period: `${r.year}-01-01`,
value: r.value,
status: r.is_forecast ? 'forecast' : null
});
}
await writeSeries(series, obs);
log(`imf ${ind.id}: ${rows.length} rows (vintage ${vintage})`);
total += rows.length;
} catch (error) {
note(`${ind.id}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return total;
});
}