| |
| |
| |
| |
| |
|
|
| const ALPHA = 0.3; |
| const MIN_WINDOW = 6; |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| export function updateWindow(region, count, prior) { |
| const prevWindow = Array.isArray(prior?.window) ? prior.window : []; |
| const window = [...prevWindow, count].slice(-24); |
|
|
| const prevEma = typeof prior?.ema === 'number' ? prior.ema : count; |
| const ema = ALPHA * count + (1 - ALPHA) * prevEma; |
|
|
| const { mean, stddev } = computeWindowStats(window); |
|
|
| return { region, window, ema, mean, stddev, updatedAt: Date.now() }; |
| } |
|
|
| |
| function normalizeCountry(name) { |
| return (name ?? '').trim().toLowerCase(); |
| } |
|
|
| |
| |
| |
| |
| export function computeWindowStats(window) { |
| if (window.length === 0) return { mean: 0, stddev: 0 }; |
|
|
| const mean = window.reduce((s, v) => s + v, 0) / window.length; |
|
|
| const variance = window.reduce((s, v) => s + (v - mean) ** 2, 0) / window.length; |
| const stddev = Math.sqrt(variance); |
|
|
| return { mean, stddev }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function computeEmaWindows(priorWindows, acledEvents, ucdpEvents, nowMs = Date.now()) { |
| const cutoff = nowMs - 24 * 60 * 60 * 1000; |
|
|
| |
| const counts24h = new Map(); |
|
|
| const safeAcled = Array.isArray(acledEvents) ? acledEvents : []; |
| const safeUcdp = Array.isArray(ucdpEvents) ? ucdpEvents : []; |
|
|
| for (const ev of safeAcled) { |
| const country = normalizeCountry(ev?.country); |
| if (!country) continue; |
| const ts = Date.parse(ev.event_date); |
| if (Number.isFinite(ts) && ts >= cutoff) { |
| counts24h.set(country, (counts24h.get(country) ?? 0) + 1); |
| } |
| } |
|
|
| for (const ev of safeUcdp) { |
| const country = normalizeCountry(ev?.country ?? ev?.country_name); |
| if (!country) continue; |
| const ts = Date.parse(ev.date_start); |
| if (Number.isFinite(ts) && ts >= cutoff) { |
| counts24h.set(country, (counts24h.get(country) ?? 0) + 1); |
| } |
| } |
|
|
| const safePrior = priorWindows instanceof Map ? priorWindows : new Map(); |
| const allCountries = new Set([...safePrior.keys(), ...counts24h.keys()]); |
|
|
| |
| const updated = new Map(); |
|
|
| for (const country of allCountries) { |
| const count = counts24h.get(country) ?? 0; |
| const prior = safePrior.get(country) ?? null; |
| const ws = updateWindow(country, count, prior); |
| updated.set(country, ws); |
| } |
|
|
| return updated; |
| } |
|
|
| |
| |
| |
| |
| export function computeRisk24h(windows) { |
| |
| const result = new Map(); |
|
|
| for (const [country, state] of windows) { |
| if (state.window.length < MIN_WINDOW) { |
| result.set(country, { risk24h: 0, zscore: 0, velocitySpike: false, region: country }); |
| continue; |
| } |
|
|
| const zscore = state.stddev > 0 ? (state.ema - state.mean) / state.stddev : 0; |
| const risk24h = Math.min(100, Math.max(0, Math.round(50 + zscore * 20))); |
| const velocitySpike = risk24h >= 75; |
|
|
| result.set(country, { risk24h, zscore, velocitySpike, region: country }); |
| } |
|
|
| return result; |
| } |
|
|