Python · GDP per capita — flag version
IMF WEO → ECA54 → landmark years → flag markers for top 5.
1# pip install sdmx1 pillow requests pycountry seaborn2import sdmx3import pandas as pd4import numpy as np5import matplotlib.pyplot as plt6import seaborn as sns7import pycountry8import requests9from PIL import Image10from io import BytesIO11from matplotlib.offsetbox import OffsetImage, AnnotationBbox12from datetime import datetime1314# ── 1. Country list ───────────────────────────────────────────────────────15ECA54_ISO3 = [16 "DZA","AGO","BEN","BWA","BFA","BDI","CPV","CMR","CAF","TCD","COM",17 "COG","COD","CIV","DJI","EGY","ERI","ETH","GNQ","GAB","GMB","GHA",18 "GIN","GNB","KEN","LSO","LBR","LBY","MDG","MWI","MLI","MRT","MUS",19 "MAR","MOZ","NAM","NER","NGA","RWA","STP","SEN","SYC","SLE","SOM",20 "ZAF","SSD","SDN","TZA","TGO","TUN","UGA","ZMB","ZWE","SWZ"21]2223# Landmark years only — keeps the plot uncluttered24target_years = {1980, 1985, 1990, 1995, 2000, 2005, 2010, 2015, 2020, 2025}2526# ── 2. Fetch IMF WEO ─────────────────────────────────────────────────────27# NGDPDPC = GDP per capita, current USD.28# Country-key query (ECA54.NGDPDPC.A) is faster than key="all".29IMF_DATA = sdmx.Client("IMF_DATA")30ECA54 = "+".join(ECA54_ISO3)3132resp = IMF_DATA.data(33 "WEO",34 key = f"{ECA54}.NGDPDPC.A",35 params= {"startPeriod": 1980, "endPeriod": 2025},36)3738ser = sdmx.to_pandas(resp)39df = ser.rename("value").reset_index() if isinstance(ser, pd.Series) else ser.copy()40df = df.rename(columns={"COUNTRY": "ISO3", "TIME_PERIOD": "year"})4142keep = [c for c in ["ISO3", "year", "value"] if c in df.columns]43df = df[keep]4445df["year"] = pd.to_datetime(df["year"].astype(str), errors="coerce").dt.year46df["value"] = pd.to_numeric(df["value"], errors="coerce")47df["year"] = pd.to_numeric(df["year"], errors="coerce").astype("Int64")48df = df[df["year"].isin(target_years)].copy()49df = df.dropna(subset=["value"])5051# ── 3. ISO2 + rank ────────────────────────────────────────────────────────52df["ISO2"] = df["ISO3"].apply(53 lambda x: pycountry.countries.get(alpha_3=x).alpha_254 if pycountry.countries.get(alpha_3=x) else None55)56# Namibia edge case: pycountry returns None for "NA" (looks like null)57df.loc[df["ISO3"] == "NAM", "ISO2"] = "NA"5859df["rank"] = df.groupby("year")["value"].rank(method="first", ascending=False)60top5 = df[df["rank"] <= 5].copy()61others = df[df["rank"] > 5].copy()6263mask = (df["rank"] <= 5) & (df["ISO2"].isna())64print(f"{mask.sum()} missing ISO2 in top-5" if mask.any() else "Safe to proceed")6566# ── 4. Flag helper ────────────────────────────────────────────────────────67# Cache PIL Image, not OffsetImage — OffsetImage artists cannot be reused68# across ax.add_artist() calls; doing so silently moves rather than copies.69# Flags: github.com/matahombres/CSS-Country-Flags-Rounded70flag_cache: dict = {}7172def get_flag_image(iso2: str, zoom: float = 0.045):73 if pd.isna(iso2):74 return None75 iso2 = str(iso2).upper()76 if iso2 not in flag_cache:77 try:78 url = (f"https://raw.githubusercontent.com/matahombres/"79 f"CSS-Country-Flags-Rounded/master/flags/{iso2}.png")80 r = requests.get(url, timeout=5)81 flag_cache[iso2] = (82 Image.open(BytesIO(r.content)).convert("RGBA")83 if r.status_code == 200 else None84 )85 except Exception:86 flag_cache[iso2] = None87 img = flag_cache[iso2]88 return OffsetImage(img, zoom=zoom) if img is not None else None8990# Pre-fetch all unique flags before plotting91for iso2 in top5["ISO2"].dropna().astype(str).str.upper().unique():92 get_flag_image(iso2)9394# ── 5. Plot ───────────────────────────────────────────────────────────────95sns.set_style("whitegrid")96fig, ax = plt.subplots(figsize=(16, 9))9798# Non-top-5: single vectorised call (fast)99ax.scatter(100 others["year"], others["value"],101 s=18, color="lightgrey", alpha=0.6, zorder=2102)103104# Top-5: flag markers, fallback to labelled dot105for row in top5.itertuples(index=False):106 x, y = row.year, row.value107 flag = get_flag_image(row.ISO2)108 if flag:109 ax.add_artist(AnnotationBbox(flag, (x, y), frameon=False))110 else:111 ax.scatter(x, y, s=60, color="steelblue", zorder=3)112 ax.text(x, y + 30, row.ISO3, fontsize=7, ha="center", color="steelblue")113114# ── 6. Labels & formatting ────────────────────────────────────────────────115ax.set_xlabel("Year", fontsize=12)116ax.set_ylabel("GDP per capita (current USD)", fontsize=12)117y_max = df["value"].max()118ax.set_ylim(0, y_max * 1.12)119ax.set_xlim(min(target_years) - 1, max(target_years) + 2)120ax.set_xticks(sorted(target_years))121fig.suptitle(122 "African Economies — GDP per Capita by Country-Year",123 fontsize=16, fontweight="bold"124)125ax.set_title(126 f"Each marker = one country-year. Top 5 per year shown as flag. "127 f"N = {len(df)} country-year observations. 2025 = IMF estimate.",128 fontsize=11, color="grey"129)130fig.text(131 0.08, 0.02,132 "Source: IMF World Economic Outlook (NGDPDPC). ECA 54-member grouping.",133 fontsize=9, color="grey", ha="left"134)135sns.despine()136137today = datetime.today().strftime("%d%b%Y")138plt.savefig(f"Africa_GDPpc_countryball_{today}.png", dpi=300, bbox_inches="tight")139plt.show()

