Skip to content
Methodology · scoring spec v18.1

GGRI Methodology

Global Geopolitical Risk Index · Scoring spec v18.1 · Updated 2026-05-16

Scoring spec and deploy build are versioned independently. The current public deploy is on the v22.10.x build; the scoring spec has been v18.1 since 2026-05-10. API responses include the build version as methodology_version.

At a glance

The Global Geopolitical Risk Index (GGRI) is AtlasRisks' country-level risk scoring engine. It fuses public OSINT — wire services, mainstream media, specialist conflict reporting, regional outlets, alternative media, government and humanitarian advisories, optionally GDELT and ACLED — into per-country risk scores, refreshed twice daily and on demand. Every country in the active set carries two numbers:

A score is meaningful when both are high. A high risk with low confidence is a flag to investigate; a low risk with high confidence is a stable signal. The two are reported separately, never collapsed.

finalRisk = clamp( base + eventSeverityMod + trendMod + corroborationMod + officialAdvisoryMod + breakingMod − staleDataPenalty, 0, 95 )

Tier mapping

TierRangeOperational meaning
LOW0–39Stable. Routine geopolitical activity; no acute threat indicators.
MODERATE40–59Elevated baseline or active news cycle; specific theaters bear watching.
HIGH60–74Severe instability, active sanctions or kinetic events, no rule-of-law collapse.
CRITICAL75–95Active war, state collapse, mass casualty events, or sustained chokepoint disruption.

The OSINT fusion layer

Every public intelligence pipeline trades coverage against credibility. Wire services break stories first but compress them; specialist conflict outlets verify but lag; regional papers know the ground but carry domestic framing; state-affiliated outlets disclose policy posture but distort facts; humanitarian feeds report disasters with high accuracy but limit themselves to humanitarian topics. No single source class gives you the full picture.

AtlasRisks' fusion approach is to ingest all of them, tag each item with its source class and reliability weight, then score countries from the corroborated picture rather than from any single feed. A wire-service-only signal scores moderately. A wire-service signal corroborated by a regional outlet AND a humanitarian advisory scores higher. A signal that exists only in alternative media or state-affiliated sources, without corroboration, scores low — even if the headline is dramatic.

Country-level risk scoring

For each country with a defined baseline (~70 countries) plus any country with current news activity, the engine computes:

  1. Baseline risk — a manually curated 0–95 score reflecting structural condition (active war, state collapse, ungoverned space, peer-state confrontation, etc.). Reviewed quarterly.
  2. Event severity modifier — capped at +10. Each clustered event contributes its raw severity (drawn from the event-type lexicon, e.g. coup=9, airstrike/terrorism=8, mass casualty=8, kidnapping=6, civil unrest=5, sanctions=4) weighted by the average reliability of its sources and amplified by corroboration count.
  3. Trend modifier — capped at +3. Reflects whether today's event volume on this country exceeds the baseline-quiet condition.
  4. Corroboration modifier — capped at +6. Adds +2 per event cluster confirmed by three or more independent sources.
  5. Official advisory modifier — capped at +5. Adds weight when government advisories or humanitarian alerts (NHC, USGS, GDACS, ReliefWeb, future State Department advisories) attribute to the country.
  6. Breaking modifier — capped at +10. Existing v10 signal preserved for compatibility: high-density armed-conflict keyword matches flagged as breaking news.
  7. Stale data penalty — up to −2. If all attributed items are more than 48 hours old, the score is dampened so cold news doesn't pretend to be live.

The resulting number is clamped to 0–95 and bucketed into a tier. Every modifier is exposed in the API payload so consumers can audit which factors moved a country today.

v16 scoring upgrades — locus, actor, cluster-first

The v11 scoring model worked, but in operation it had a recognizable failure mode: structurally stable countries that simply appeared in a lot of news stories on a given day got over-elevated. A quiet democracy mentioned in 80 wire stories about a foreign crisis would creep toward HIGH because the engine couldn't distinguish "this country is the locus of the news" from "this country is mentioned in the news about somewhere else." v16 fixes that with five concrete changes.

  1. Locus / actor / mention country-role classification. For every country detected in a headline, the engine now asks: where in the sentence does the country name sit relative to the kinetic verbs? A country whose name appears within 12 words of an attack/strike/coup/raid/explosion verb is classified as the locus (the place the event happens at). A country mentioned outside that window but tied to government / military / diplomatic actor language is classified as the actor (an involved party). A country whose name appears with no proximity to either is classified as a mention (referenced, but not the subject).
  2. Cluster-first event severity from top-5 LOCUS clusters only. Severity is no longer aggregated per article. The engine clusters items, ranks each country's clusters by reliability-weighted severity, and computes the country's event-severity modifier from the top 5 clusters where that country is the locus. A country mentioned 80 times in actor-only or mention-only stories contributes nothing to its own severity score under this rule.
  3. Reliability-weighted volume. Volume signals (the trend modifier) are now multiplied by the average source reliability of the contributing items. A flood of 50 RT or alt-media items can no longer fake high volume the way 50 wire-service stories can.
  4. Baseline-asymmetric modifier cap. The maximum amount the news can elevate any country's risk above its baseline is now max(8, round((95 − base) × 0.40)). Concretely: a baseline-90 country (active war zone) can move ±8 from news. A baseline-30 country (stable democracy) is also capped at ±8 by the floor — but more importantly, no amount of news traffic about other countries can drag a baseline-30 country's score above ~38. The structurally stable can no longer be elevated to HIGH purely by being mentioned in dramatic stories.
  5. Cluster-locus filtering. A cluster only contributes to country X's score if at least one of the cluster's items has X as the locus. Mention-only and actor-only items still appear in country X's evidence trail (so analysts can see what was said about it) but they don't move the score.

v16.2 added a related correctness fix to country detection itself: the substring match used since v9 (text.includes("India")) was matching Indiana, Iranian, Chinatown, and South Korean. The v16.2 detector uses a precompiled word-boundary regex ((?<![A-Za-z0-9])needle(?![A-Za-z0-9])) so only whole-word matches count. The hotspot drill-down panel and the daily brief's events list also filter to locus + actor articles per country, so the country brief no longer surfaces stories that happened to mention the country in passing.

Net effect: tier transitions are more conservative and more accurate. Countries don't move into HIGH because of the news cycle alone, and ties at the CRIT ceiling now genuinely reflect parallel real-world conditions (active wars, state collapse, sustained kinetic events) rather than scoring artefacts.

v17 — Static / Active decomposition

Even with the v16 fixes, the highest-baseline countries (Sudan 95, Ukraine 92, Palestine 92, Russia 88, Israel 88) all converged to a tied 95.0 headline score whenever any meaningful news activity registered. The cause was structural: with a 95 ceiling and a baseline-asymmetric modifier cap of max(8, round((95 − base) × 0.40)), every country with baseline ≥ 87 hit the ceiling on its first news event. Sudan literally couldn't move from 95 because its baseline already equalled the cap. Operationally this meant a quiet day in a war zone and an active escalation day produced the same headline number — and the Top Movers list was uninformative when 5 conflict countries tied at 95.

v17 fixes this by reporting two numbers per country instead of one:

risk_active = clamp(0, 100,
    5 × event_severity_modifier (0–10)
  + 4 × breaking_modifier (0–10)
  + 3 × corroboration_modifier (0–6)
  + 3 × official_advisory_modifier (0–5)
  + 4 × trend_modifier (0–3)
  + 2 × volume_bump (0–5)
  − 5 × stale_data_penalty (0–2)
)

v17.8 hybrid headline:
if (risk_static ≥ 75)
  risk_headline = clamp(0, 100, risk_static + (risk_active − 50) × 0.25)
else
  risk_headline = clamp(0, 100, max(risk_static, risk_active))

For conflict countries (baseline ≥ 75), the headline is the curated baseline MODULATED by current news intensity — active=50 is the "average news day" with no nudge to baseline; values above 50 push up (escalation), values below 50 nudge down (quiet day). The 0.25 multiplier means active news can swing the headline ±12.5 from baseline. Sudan, Russia, Ukraine, Israel — all CRIT — now produce differentiated decimal headlines that reflect what's happening today, not just round baseline ceilings.

For non-conflict countries (baseline < 75), the headline is max(risk_static, risk_active) — the static baseline is the floor, but a major incident can escalate the headline. A terror attack in Sweden (static 25, active 78) pushes the headline to 78 / CRIT for the day; a quiet day in Switzerland (static 8, active 0) reads 8 / LOW.

The active modifiers themselves are still computed under the v16 rules (locus-only attribution, cluster-first severity, reliability-weighted volume). v17 only changes how those component modifiers are combined into a final score: instead of being added on top of the baseline and capped tightly, they now produce an independent 0–100 score that competes with (rather than tops off) the baseline.

How to read the two numbers

Country stateStaticActiveHeadlineOperational read
Sudan, quiet day953591.3Structural CRIT, but news quieter than average — headline modulated down ~3.7 from baseline.
Sudan, active escalation9588100.0News intensity well above average — headline pushed past 95 to ceiling. Operationally escalating.
Sudan, mass-casualty event9595100.0Maximum signal. Operator should treat as catastrophic.
Russia, quiet day884286.0Borderline CRIT/HIGH. Routine news cycle pulls headline 2 below baseline.
Russia, missile exchange day888296.0Active news pushes headline 8 above baseline. CRIT, very active.
Ukraine, normal week925593.3Active slightly above neutral — small upward modulation.
Israel, active operations887895.0News-driven escalation visible in the headline number, not just secondary fields.
Sweden, terror incident257878.0Static below 75 → max(static, active) rule applies. Active dominates; headline jumps to CRIT.
Sweden, normal week251225.0Static dominates — baseline LOW.
Switzerland, quiet day808.0Both low. No risk to operate.

The crucial behavioral change: Top Movers in the daily brief and the dashboard now sort by abs(delta_active_vs_yesterday), not by headline delta. Static doesn't change daily, so headline delta is uninformative for CRIT-pegged countries (Sudan's headline can read 95 every day for a year while the active number swings between 20 and 90). The active delta is what changed in the world; that's what surfaces.

Both numbers are surfaced in the API payload (hotspot.risk_static, hotspot.risk_active, hotspot.risk_active_components with the per-modifier breakdown, and hotspot.delta_active_vs_yesterday). The headline risk field continues to populate exactly as before for backward compat with the v9–v16 dashboard adapters. The dashboard status strip and the daily brief now display Static / Active alongside the headline; the country drill-down panel shows the active component breakdown.

v18 — Ten-category baseline decomposition

v17.8 produced decimal differentiation only for the active component — the static baseline was still a single curated number per country, so Sudan (95), Ukraine (92), Palestine (92), Russia (88), and Israel (88) all reported their round baselines on the headline. Operationally these are wildly different countries: Sudan is state-collapse + security; Russia is sanctions + cyber + political; Israel is security + advisory; etc. v18 surfaces that structural difference by decomposing every country's baseline into ten weighted category subscores.

Categories and weights

#CategoryWeight
1Political Stability12%
2Security and Violence18%
3Civil Unrest and Social Tension10%
4Crime and Personal Safety8%
5Economic and Financial Stability10%
6Regulatory, Legal, and Corruption8%
7Infrastructure and Operational8%
8Cyber and Information8%
9Health, Environmental, Natural Hazard8%
10Travel, Diplomatic, Government Advisory10%
Total100%

Country profiles + algorithmic seeding

Each country is assigned a profile that defines additive offsets per category. The category subscore for category c equals clamp(0, 100, baseline + offset[c]). Profiles are calibrated quarterly so the weighted sum stays close to the legacy baseline (the structural anchor doesn't move; the operational decomposition does). v18 ships ten profiles: state_collapse_war, peer_war_invader, war_defender, war_zone_active, fragile_authoritarian, fragile_state, criminal_violence, regional_tension, stable_democracy, and a fall-through default.

Worked examples — five CRIT countries that previously tied

CountryProfilePre-v18 baselinev18 risk_staticTop-3 categories
Sudanstate_collapse_war9590.8security 100, economic 100, advisory 100
Russiapeer_war_invader8889.3political 93, security 93, regulatory 93
Ukrainewar_defender9291.2security 98, advisory 97, economic 95
Israelwar_zone_active8889.3security 94, advisory 93, political 91
Palestinewar_zone_active9293.3security 98, advisory 97, political 95
Yemenstate_collapse_war8277.8security 87, economic 87, advisory 87
USAstable_democracy4240.6cyber 62, crime 54, unrest 45
Switzerlandstable_democracy1816.6cyber 38, crime 30, unrest 21

Russia and Israel both report risk_static = 89.3, but their top-3 categories are completely different (Russia: political/security/regulatory; Israel: security/advisory/political). The dashboard country drill-down surfaces the per-category breakdown so an operator can see at a glance why two CRIT countries with similar headlines have different operational profiles.

Per-category recency half-life is also category-specific: 7 days for fast-moving categories (security, unrest), 14 days for episodic categories (economic, health), 30 days for slow-moving categories (political, crime, regulatory, infrastructure, cyber, advisory).

The v17.8 hybrid headline rule is preserved unchanged. v18.0 only changes how risk_static is COMPUTED (weighted sum of categories instead of single number). Future v18.x rounds will add per-event scoring formula with business_impact, alert triggers with point uplifts and decay, sector modifiers, and subnational granularity.

Risk vs confidence

Two countries can both be scored 75 and be in very different epistemic situations. Country A might be at 75 because every wire service, ReliefWeb, and a state department advisory all reported the same kinetic event in the last six hours. Country B might be at 75 because a single alternative-media source published one dramatic headline that the engine couldn't corroborate.

The confidence score, on a 0–100 scale, makes the difference visible. It's a weighted blend of:

Source registry & reliability weights

v11 organizes sources into a structured registry with per-type reliability weights. The dashboard shows which categories drove today's score; consumers can use the registry to audit how heavily a given source class influenced an event.

Source classWeightExamples in registry
Official / govt advisory1.00UK FCDO travel advice, US State travel warnings, Smartraveller (Australia), Canada GAC, EU Council Press, Japan MOFA travel advice. v18.9 added EU Council + Japan MOFA to widen non-Anglo policy coverage.
Economic stability0.95OFAC Recent Actions (sanctions), IMF Press Center, Federal Reserve press releases, ECB press releases, FRED Blog (St. Louis Fed data commentary). v18.9 expanded the tier from 1 → 5 feeds so the v18 economic-category active signal is genuinely live (rate decisions, IMF Article IV reports, Fed/ECB statements) instead of OFAC-only.
Humanitarian official0.92Reserved for direct WHO / UN OCHA / IFRC integration in a future round.
Wire service0.92AP World, Reuters World, AFP English. v18.8 added AFP to close the European-press gap.
Mainstream media0.85BBC (7 regional desks), Guardian (3), NPR (3), Yahoo, CBS, ABC, CNN, Politico, Foreign Policy, Al Jazeera, Deutsche Welle, France 24.
Specialist conflict0.82Institute for the Study of War, Crisis Group, Long War Journal, Al-Monitor (Middle East specialist), Defense One (US defense policy specialist). Optional: ACLED API. v18.9 added Al-Monitor + Defense One to lift the tier from 3 → 5; Janes Defence Weekly remains paywalled, no public RSS.
Humanitarian disaster0.80NOAA NHC (Atlantic + Pacific), USGS Earthquakes, GDACS, ReliefWeb.
Regional media0.72Kyiv Independent (UKR), Times of Israel (ISR/PSE/LBN), Premium Times Nigeria (NGA), The East African (KEN/UGA/TZA), Mail & Guardian (ZAF), Times of India (IND), Nikkei Asia (JPN/pan-Asia), South China Morning Post (HKG/CHN), Folha de São Paulo (BRA), El Universal (MEX), Clarín (ARG). v18.7 added 9 anchors across Africa, Asia, and Latin America.
Alternative media0.45Bellingcat, War on the Rocks, Antiwar.com, Responsible Statecraft, The Intercept.
State-affiliated0.30RT (RU), TASS English (RU), Xinhua English (CN), PressTV (IR), Anadolu Agency English (TR). Always flagged in payload as state_affiliated:true. v18.8 expanded the tier from 1 to 5 outlets to cover the propaganda-surface across the major non-Western state-aligned framings; weight stays at 0.30 so they corroborate but never dominate.
Public social signal0.20Optional GDELT 2.0 DOC API. Treated as soft signal.

Alternative media, state-affiliated, and public social signals are intentionally low-weighted. They contribute corroboration value when their reporting aligns with higher-weight sources, but cannot by themselves move a country's risk score significantly. State-affiliated items are always flagged in the API payload so downstream consumers can discount or filter them.

Optional environment-gated sources

Two sources are wired in v11 but gated by Netlify environment variables; they are skipped gracefully when keys aren't set.

X / Reddit / Telegram / Google Trends are recorded as reserved env-var slots in the diagnostics object (diagnostics.env_enabled) so the admin console can see whether keys are present, but no fetcher is wired in v11. AtlasRisks does not scrape private platforms, does not bypass authentication, and does not collect personal social media data — these sources will only ever come online via lawful official APIs.

Corroboration & clustering

The engine clusters items by country + event_type + normalized_keywords + date_bucket. A cluster represents one real-world event as reported by one or more sources. For each cluster the engine tracks: source domains (distinct), source types (distinct), source count, highest reliability, average reliability, severity weighted by reliability and corroboration count, first seen and last seen timestamps, and a confidence score.

Scoring rules anchored in clusters:

Recency decay

News loses operational relevance over time. The engine applies a per-item recency weight to severity scoring:

Age windowWeightTreatment
0 – 24 h1.00Full weight in active scoring.
24 – 48 h0.60Reduced weight; still active.
48 – 72 h0.35Significant decay; trend context.
3 – 7 d0.15Trend context only; minor influence.
> 7 d0.00Excluded from active scoring window.

The 7-day rolling window is preserved as feed_history in the payload so longer-term trend analysis remains available, but items older than seven days no longer contribute to today's risk.

Country attribution

Each headline plus description is run through detailed country detection. The engine returns every matched country with a confidence score and a reason:

Multi-country articles are attributed to all detected countries. "U.S. strikes Houthi positions in Yemen" attributes to USA + YEM, not just the first match.

Event types & threat vectors

Every item is classified into one or more event types. The classification produces both a primary event type and a union of threat vectors (operational categories that downstream consumers can filter on). v11 ships with the following lexicon:

Threat vectors aggregate the event types into operational categories — military, terrorism, civil unrest, organized crime, kidnapping, travel, maritime, cyber, natural disaster, political, economic, health, infrastructure, supply chain, aviation security. Each hotspot in the API payload exposes the union of threat vectors triggered for that country.

Refresh cadence

The OSINT pipeline runs on a Netlify scheduled function with cron 0 3,10 * * * UTC — 03:00 UTC (22:00 EST previous day) and 10:00 UTC (05:00 EST). The dashboard's "Pull Now" button additionally allows on-demand refreshes, rate-limited to once per minute per pipeline instance to prevent runaway invocations.

Each refresh writes to two Netlify Blob keys:

Output schema

The /api/intel-feed endpoint returns a payload of the following shape. Backward compatibility with v10 is preserved — every v10 field still exists in v11; new fields are additive.

{
  // v10-compatible fields (preserved)
  "generated_at": "2026-05-08T10:00:14Z",
  "source": "Country baseline + structured 40+ source registry...",
  "events_24h": 247,
  "elevated_count": 12,
  "critical_count": 3,
  "global_average": 63,
  "global_tier": "high",
  "tier_counts": { "1": 0, "2": 198, "3": 49, "4": 0 },
  "hotspots": [ { ... } ],
  "feed": [ { ... } ],
  "global_recent": [ { ... } ],
  "feed_history": [ ... ],
  "window_days": 7,
  "delta_vs_yesterday": 1.4,
  "yesterday_score": 62,

  // v11 additive fields (still current under v17)
  "methodology_version": "v17",
  "global_static": 51.2,            // v17 — average of all hotspots' baselines
  "global_active": 38.6,            // v17 — average of all hotspots' news-driven score
  "delta_active_vs_yesterday": 4.1, // v17 — meaningful day-over-day signal
  "events_7d": 1342,
  "global_confidence": 78,
  "sources_checked": 47,
  "sources_successful": 36,
  "sources_failed": 11,
  "source_mix": { "mainstream_media": 751, "government_advisories": 231, "alternative_media": 160, "state_affiliated": 100, "humanitarian_disaster": 37, "specialist_conflict": 30, ... },
  "unassigned": [ ... ],   // items not attributable to any country
  "diagnostics": {
    "run_id": "run_lwf3z9_a8c2f1",
    "duration_ms": 9372,
    "sources_attempted": 42,
    "sources_successful": 39,
    "sources_failed": 3,
    "skipped_sources": [ { "source": "GDELT 2.0 DOC API", "reason": "GDELT_ENABLED env var not set" } ],
    "errors": [ ... ],
    "items_fetched_raw": 583,
    "items_after_dedupe": 412,
    "items_after_country_detection": 387,
    "blob_write_status": "ok",
    "env_enabled": { "gdelt": false, "acled": false, "social_x": false, "social_reddit": false, "google_trends": false }
  }
}

Each hotspot entry exposes the full risk decomposition:

{
  "iso": "PAK", "name": "Pakistan", "lat": 30, "lng": 69,
  "risk": 67, "tier": "high", "confidence": 82,
  "base": 58,
  "newsModifier": 9,            // legacy v10 aggregate (preserved for compat)
  "breakingBump": 0,
  "breaking_count": 0,
  "eventSeverityModifier": 6,
  "trendModifier": 1,
  "corroborationModifier": 4,
  "officialAdvisoryModifier": 0,
  "staleDataPenalty": 0,
  "count": 7,
  "source_count": 5,
  "source_types": ["mainstream_media", "wire_service", "specialist_conflict"],
  "corroborated_event_count": 2,
  "average_reliability": 0.86,
  "average_recency": 0.72,
  "signals": { "armed": 6, "unrest": 4, "political": 2, "disaster": 0 },
  "threat_vectors": ["military", "civil_unrest", "political"],
  "events": [ { "ts": "...", "headline": "...", "src": "...", "url": "...", "tier": 2 } ],
  "delta_vs_yesterday": 4
}

Intended use cases

The GGRI is built for security operators, analysts, and decision-makers who need a continuously refreshed country-level view of geopolitical and operational risk. Specifically:

AtlasRisks is not a government advisory and does not replace professional judgment. The GGRI is a starting point for analysis. Operators should consult official advisories, qualified security professionals, and ground-level information before making operational, travel, or financial decisions.

How to read the GGRI

The GGRI is a direction-of-risk indicator, not a prediction. Read tier transitions (LOW → MODERATE → HIGH → CRITICAL), the day-over-day delta, the confidence score, and the threat-vector breakdown — not last-decimal score moves. A country at risk 75 with confidence 30 is a flag to investigate; a country at 75 with confidence 85 is a stable signal worth acting on.

Use the events list and the cluster-derived corroboration metrics to investigate why today's score is what it is. The score is a starting point for analysis, not a substitute for it.

Changelog

v18.1 — 2026-05-10 (Per-event scoring + asymptotic ceiling + 2-decimal display)

v18.0 — 2026-05-10 (Ten-category baseline decomposition)

v17.8 — 2026-05-09 (Hybrid headline)

v17.7 — 2026-05-09 (Dashboard live-merger)

v17.6 — 2026-05-09 (Static / Active decomposition)

v17.5 — 2026-05-09

v17.0.x — 2026-05-09

v16.x — 2026-05-08 to 09

v15.x — 2026-05-08

v14 — 2026-05-08

v13.x — 2026-05-08

v12 — 2026-05-08

v11 — 2026-05-08

v10.1 — 2026-05-08

v10 — 2026-05-08

v9 — 2026-05-08

v5 — v8


— AtlasRisks · GGRI v18 · 2026-05-10