Chris Parmer — home

Lorenz curve + Gini coefficient

World Bank US income quintile shares

Example from the compendium of canonical charts

Lorenz curve + Gini coefficient — World Bank US income quintile shares

Python Code

"""Lorenz curve + Gini coefficient — World Bank US income quintile shares."""


import numpy as np
import plotly.graph_objects as go
import requests, warnings

WB_BASE = "https://api.worldbank.org/v2/country/USA/indicator/{indicator}?format=json&date=2018:2023&mrv=1"

INDICATORS = {
    "Q1 (bottom 20%)": "SI.DST.FRST.20",
    "Q2":               "SI.DST.02ND.20",
    "Q3":               "SI.DST.03RD.20",
    "Q4":               "SI.DST.04TH.20",
    "Q5 (top 20%)":    "SI.DST.05TH.20",
}


def fetch_latest(indicator):
    warnings.filterwarnings("ignore")
    requests.packages.urllib3.disable_warnings()
    url = WB_BASE.format(indicator=indicator)
    r = requests.get(url, verify=False, timeout=30)
    r.raise_for_status()
    data = r.json()
    for entry in data[1]:
        if entry.get("value") is not None:
            return float(entry["value"]), entry.get("date", "?")
    return None, None


def generate():
    print("fetching World Bank US income quintile shares …")
    shares = []
    year_label = "?"
    failed = False
    for label, ind in INDICATORS.items():
        try:
            val, yr = fetch_latest(ind)
        except Exception as e:
            print(f"  World Bank API failed ({e}); using synthetic quintiles")
            failed = True
            break
        if val is None:
            print(f"  WARNING: {label} returned None — using synthetic quintiles")
            failed = True
            break
        shares.append(val)
        year_label = yr
        print(f"  {label}: {val:.1f}% ({yr})")
    if failed:
        shares = [3.1, 8.3, 14.8, 23.0, 50.8]   # approximate US 2022
        year_label = "2022 (synthetic)"

    shares = np.array(shares)
    shares = shares / shares.sum() * 100   # normalize to 100%

    # Lorenz curve: cumulative income share vs cumulative population share
    pop_shares  = np.array([0, 20, 40, 60, 80, 100])
    inc_cumsum  = np.concatenate([[0], np.cumsum(shares)])

    # Gini = 1 - 2 * area under Lorenz = (area between diagonal and curve) / 0.5
    gini = 1 - 2 * np.trapz(inc_cumsum / 100, pop_shares / 100)

    fig = go.Figure()

    # Equality diagonal
    fig.add_trace(go.Scatter(
        x=[0, 100], y=[0, 100],
        mode="lines", name="Perfect equality",
        line=dict(color=GRID, width=1.5, dash="dot"),
        hoverinfo="skip",
    ))

    # Lorenz curve
    fig.add_trace(go.Scatter(
        x=pop_shares, y=inc_cumsum,
        mode="lines+markers",
        name=f"Lorenz curve (Gini = {gini:.3f})",
        line=dict(color=VIOLET, width=2.5),
        marker=dict(size=8),
        fill="tonexty",
        fillcolor="rgba(132,94,238,0.12)",
        hovertemplate="Bottom %{x:.0f}% of pop → %{y:.1f}% of income<extra></extra>",
        text=["", "Q1", "Q2", "Q3", "Q4", "Q5"],
    ))

    fig.add_annotation(
        x=60, y=25, text=f"Gini = {gini:.3f}",
        showarrow=False,
        font=dict(size=14, color=VIOLET),
        bgcolor="rgba(255,255,255,0.8)",
        bordercolor=VIOLET,
        borderwidth=1,
    )

    fig.update_layout(
        title=dict(text=f"Lorenz Curve — US Income Distribution ({year_label})", x=0.5),
        xaxis=dict(title="Cumulative population share (%)", range=[0, 100]),
        yaxis=dict(title="Cumulative income share (%)", range=[0, 100]),
        legend=dict(orientation="h", y=1.08),
        margin=dict(t=60, b=50, l=70, r=40),
        height=460,
    )
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly