A Huge Democratic Divergence

economy
python
Where demographic weight does not match economic influence
Published

Nov 25, 2026

Keywords

divergence

Summary

The gap between a country’s share of the world’s population and its share of the global economy highlights a profound divergence. Some nations host large populations yet hold only a small fraction of global economic power, while others exert outsized influence relative to their demographic weight.

Code
# Libraries
# =====================================================================
import os
from io import BytesIO
import requests
import wbgapi as wb
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import matplotlib.ticker as ticker
from matplotlib.offsetbox import OffsetImage, AnnotationBbox

# Data Extraction (Countries)
# =====================================================================
# Extract JSON and bring data to a dataframe
url = 'https://raw.githubusercontent.com/guillemmaya92/world_map/main/Dim_Country.json'
response = requests.get(url)
data = response.json()
df = pd.DataFrame(data)
df = pd.DataFrame.from_dict(data, orient='index').reset_index()
df_countries = df.rename(columns={'index': 'ISO3'})

# Data Extraction - WBD (2024)
# ========================================================
#Parametro
parameters = ['LP', 'NGDPD']

# Create an empty list
records = []

# Iterar sobre cada parámetro
for parameter in parameters:
    # Request URL
    url = f"https://www.imf.org/external/datamapper/api/v1/{parameter}"
    response = requests.get(url)
    data = response.json()
    values = data.get('values', {})

    # Iterate over each country and year
    for country, years in values.get(parameter, {}).items():
        for year, value in years.items():
            records.append({
                'parameter': parameter,
                'iso3': country,
                'year': int(year),
                'value': float(value)
            })
    
# Create dataframe
df = pd.DataFrame(records)

# Pivot Parameter to columns and filter nulls
df = df.pivot(index=['iso3', 'year'], columns='parameter', values='value').reset_index()

# Add URSS 1980
urss = {'iso3': 'SUN', 'year': 1980, 'LP': 264, 'NGDPD': 354}
df = pd.concat([df, pd.DataFrame([urss])], ignore_index=True)

# Filter after 2024
df = df[df['year'] == 2024]
df = df.dropna(subset=['LP', 'NGDPD'])

# Data Manipulation
# =====================================================================
# Merge queries
df = df.merge(df_countries, how='left', left_on='iso3', right_on='ISO3')
df = df[(df['Country'].notna()) | (df['iso3'] == 'SUN')]
df = df[['iso3', 'Country', 'year', 'LP', 'NGDPD']]
df.columns = df.columns.str.lower()

# Add calculated fields
df['lp_percent'] = df['lp'] / df['lp'].sum()
df['ngdpd_percent'] = df['ngdpd'] / df['ngdpd'].sum()
df['gap'] = df['ngdpd_percent'] - df['lp_percent']

# Ordenar por gap
df = df.sort_values(by='gap', ascending=True)

# Calculate cumulative and left positions
df['lp_cum'] = df['lp'].cumsum()
df['lp_per'] = df['lp'] / df['lp'].sum()
df['lp_cum_per'] = df['lp_cum'] / df['lp_cum'].max()
df['left'] = df['lp_cum_per'] - df['lp_per']

print(df)

# Data Visualization
# =====================================================================
# Font and style
plt.rcParams.update({'font.family': 'sans-serif', 'font.sans-serif': ['Franklin Gothic'], 'font.size': 9})
sns.set(style="white", palette="muted")

# Create figure
fig, ax = plt.subplots(figsize=(10, 6))

# Create a palette
palette = sns.color_palette("coolwarm", as_cmap=True).reversed()
gdp_min = -0.05
gdp_max = 0.05
norm = plt.Normalize(gdp_min, gdp_max)
colors = palette(norm(df['gap']))

# Barplot
bars = plt.bar(
    df['left'],
    df['gap'],
    width=df['lp_per'],  # lp_percent asumido como lp_per
    alpha=1,
    align='edge',
    edgecolor='grey',
    color=colors,
    linewidth=0.1
)

# Add title and subtitle
fig.add_artist(plt.Line2D([0.085, 0.085], [0.87, 0.97], linewidth=6, color='#203764', solid_capstyle='butt'))
plt.text(0.02, 1.13, f'A Huge Democratic Divergence', fontsize=16, fontweight='bold', ha='left', transform=plt.gca().transAxes)
plt.text(0.02, 1.09, f'Where demographic weight does not match economic influence', fontsize=11, color='#262626', ha='left', transform=plt.gca().transAxes)
plt.text(0.02, 1.05, f'(difference between economic share and population share)', fontsize=9, color='#262626', ha='left', transform=plt.gca().transAxes)

# Remove spines
ax = plt.gca()
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set(color='gray', linewidth=1)
ax.spines['left'].set_linewidth(False)

# Configuration
plt.ylim(-0.25, 0.25)
plt.xlim(0, df['lp_cum_per'].max())
ax.yaxis.set_major_locator(ticker.MultipleLocator(0.05))
ax.xaxis.set_major_locator(ticker.MultipleLocator(0.1))
plt.grid(axis='y', linestyle='--', linewidth=0.5, color='lightgray')
plt.xlabel('Cumulative Global Population (%)', fontsize=10, fontweight='bold')
plt.ylabel('GAP GDP-Population Share', fontsize=10, fontweight='bold')
plt.tick_params(axis='x', labelsize=9)
plt.tick_params(axis='y', labelsize=9)
plt.gca().xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f'{int(x*100):,}%'))
plt.gca().yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f'{round(x*100, 1):.0f}%'))

# Define flags
flag_urls = {
    'IND': 'https://raw.githubusercontent.com/matahombres/CSS-Country-Flags-Rounded/master/flags/IN.png',
    'CHN': 'https://raw.githubusercontent.com/matahombres/CSS-Country-Flags-Rounded/master/flags/CN.png',
    'USA': 'https://raw.githubusercontent.com/matahombres/CSS-Country-Flags-Rounded/master/flags/US.png',
    'IDN': 'https://raw.githubusercontent.com/matahombres/CSS-Country-Flags-Rounded/master/flags/ID.png',
    'PAK': 'https://raw.githubusercontent.com/matahombres/CSS-Country-Flags-Rounded/master/flags/PK.png',
    'NGA': 'https://raw.githubusercontent.com/matahombres/CSS-Country-Flags-Rounded/master/flags/NG.png',
    'BRA': 'https://raw.githubusercontent.com/matahombres/CSS-Country-Flags-Rounded/master/flags/BR.png',
    'RUS': 'https://raw.githubusercontent.com/matahombres/CSS-Country-Flags-Rounded/master/flags/RU.png',
    'MEX': 'https://raw.githubusercontent.com/matahombres/CSS-Country-Flags-Rounded/master/flags/MX.png',
    'JPN': 'https://raw.githubusercontent.com/matahombres/CSS-Country-Flags-Rounded/master/flags/JP.png',
    'SUN': 'https://raw.githubusercontent.com/guillemmaya92/circle_flags/refs/heads/gh-pages/flags/su.png',
}

# Load flags once
flags = {country: mpimg.imread(BytesIO(requests.get(url).content)) 
         for country, url in flag_urls.items()}

# For each bar, add the corresponding flag and country name
for bar, iso3 in zip(bars, df['iso3']):
    if iso3 in flags:
        img = flags[iso3]
        imagebox = OffsetImage(img, zoom=0.021)
        x = bar.get_x() + bar.get_width() / 2

        offset = 0.005 
        text_offset = 0.025

        # Positioning logic
        if bar.get_height() >= 0:
            y_flag = bar.get_height() + offset
            box_align = (0.5, 0)
            text_y = y_flag + text_offset
            va_text = 'bottom'
        else:
            y_flag = bar.get_height() - offset
            box_align = (0.5, 1)
            text_y = y_flag - text_offset
            va_text = 'top'

        # Add flag
        ab = AnnotationBbox(imagebox, (x, y_flag), frameon=False, box_alignment=box_align)
        ax.add_artist(ab)

        # Add country name
        country_name = df.loc[df['iso3'] == iso3, 'iso3'].values[0]
        ax.text(x, text_y, country_name, ha='center', va=va_text, fontsize=6, fontweight='bold', color='#404040')

# Add Year label
plt.text(1, 1.15, f'{df['year'].max()}',
    transform=plt.gca().transAxes,
    fontsize=22, ha='right', va='top',
    fontweight='bold', color='#D3D3D3',
    bbox=dict(facecolor='white', edgecolor='none', boxstyle='square,pad=0.3'))

# Add label "Underrepresented" and "Overrepresented"
plt.text(0, -0.1, 'Under-represented',
    transform=ax.transAxes,
    fontsize=10, fontweight='bold', color='darkred', ha='left', va='center')
plt.text(0.88, -0.1, 'Over-represented',
    transform=ax.transAxes,
    fontsize=10, fontweight='bold', color='darkblue', va='center')

# Add Data Source
plt.text(0, -0.15, 'Data Source:', 
    transform=plt.gca().transAxes, 
    fontsize=8,
    fontweight='bold',
    color='gray')
space = " " * 23
plt.text(0, -0.15, space + 'IMF World Economic Outlook Database, 2024', 
    transform=plt.gca().transAxes, 
    fontsize=8,
    color='gray')

# Adjust layout
plt.tight_layout()

# Save it...
max_year = df['year'].max()
download_folder = os.path.join(os.path.expanduser("~"), "Downloads")
filename = os.path.join(download_folder, f"FIG_IMG_GAP_GDP_Population_{max_year}.png")
plt.savefig(filename, dpi=300, bbox_inches='tight')

# Show :)
plt.show()

Difference between economic share and population share.

Back to top