Chris Parmer — home

Population pyramid

Japan, 1960 against 2023, from the World Bank API

Example from Plotly for highly customizable print-ready data visualization · shared helpers

Population pyramid — Japan, 1960 against 2023, from the World Bank API

Python Code

"""Population pyramid — Japan, 1960 against 2023, from the World Bank API.

The Pew Research treatment: 2023 as solid bars, 1960 as an outline laid over
them, men to the left and women to the right of a shared centre line, with
the age bands labelled once down the middle instead of on a y axis.
"""
from pathlib import Path

from _shared import fetch_json, save, base_layout, titles, INK, MUTED, FAINT, GRID, VIOLET, TEAL, hex_to_rgba

import numpy as np
import plotly.graph_objects as go

CHART_NUM = 10
BANDS = ["0004", "0509", "1014", "1519", "2024", "2529", "3034", "3539", "4044", "4549", "5054",
         "5559", "6064", "6569", "7074", "7579", "80UP"]
LABELS = [f"{b[:2].lstrip('0') or '0'}–{b[2:]}" for b in BANDS[:-1]] + ["80+"]
Y0, Y1 = 1960, 2023
COUNTRY = "JPN"


def wb(indicator: str) -> dict:
    url = f"https://api.worldbank.org/v2/country/{COUNTRY}/indicator/{indicator}?format=json&per_page=200"
    rows = fetch_json(url)[1]
    return {int(r["date"]): r["value"] for r in rows if r["value"] is not None}


def generate():
    data = {}
    for sex in ("MA", "FE"):
        for band in BANDS:
            data[(sex, band)] = wb(f"SP.POP.{band}.{sex}")
    men = {yr: np.array([data[("MA", b)][yr] for b in BANDS]) for yr in (Y0, Y1)}
    women = {yr: np.array([data[("FE", b)][yr] for b in BANDS]) for yr in (Y0, Y1)}
    total = {yr: men[yr].sum() + women[yr].sum() for yr in (Y0, Y1)}
    pm = {yr: men[yr] / total[yr] * 100 for yr in (Y0, Y1)}
    pw = {yr: women[yr] / total[yr] * 100 for yr in (Y0, Y1)}

    fig = go.Figure()
    y = np.arange(len(BANDS))
    fig.add_trace(go.Bar(y=y, x=-pm[Y1], orientation="h", name=f"Men {Y1}", marker=dict(color=TEAL), width=0.82,
                         hovertemplate="Men %{customdata}: %{x:.1f}%<extra>" + str(Y1) + "</extra>", customdata=LABELS,
                         text=[f"{v:.1f}" for v in pm[Y1]], textposition="outside", textfont=dict(size=10, color=MUTED), texttemplate="%{text}%"))
    fig.add_trace(go.Bar(y=y, x=pw[Y1], orientation="h", name=f"Women {Y1}", marker=dict(color=VIOLET), width=0.82,
                         hovertemplate="Women %{customdata}: %{x:.1f}%<extra>" + str(Y1) + "</extra>", customdata=LABELS,
                         text=[f"{v:.1f}" for v in pw[Y1]], textposition="outside", textfont=dict(size=10, color=MUTED), texttemplate="%{text}%"))
    # 1960 as a stepped outline over the bars.
    def outline(vals, sign):
        xs, ys = [], []
        for i, v in enumerate(vals):
            xs += [sign * v, sign * v]
            ys += [i - 0.5, i + 0.5]
        return xs, ys
    for vals, sign in ((pm[Y0], -1), (pw[Y0], 1)):
        xs, ys = outline(vals, sign)
        fig.add_trace(go.Scatter(x=xs, y=ys, mode="lines", line=dict(color=INK, width=1.8, shape="linear"),
                                 name=f"{Y0}", hoverinfo="skip", showlegend=False))
    for i, lab in enumerate(LABELS):
        fig.add_annotation(x=0, y=i, text=lab, showarrow=False, font=dict(size=11, color=MUTED), bgcolor="white", borderpad=1)

    xmax = max(pm[Y0].max(), pw[Y0].max(), pm[Y1].max(), pw[Y1].max()) * 1.35
    fig.update_layout(**base_layout(barmode="overlay", bargap=0.1, margin=dict(l=40, r=40, t=130, b=80)))
    fig.update_xaxes(range=[-xmax, xmax], tickvals=[-4, -2, 2, 4], ticktext=["4%", "2%", "2%", "4%"], showgrid=True, zeroline=False,
                     title_text="Share of total population")
    fig.update_yaxes(visible=False, range=[-0.7, len(BANDS) - 0.3])
    fig.add_annotation(x=-xmax * 0.97, y=len(BANDS) - 0.5, text=f"<b>Men</b>", showarrow=False, xanchor="left", font=dict(size=14, color=TEAL))
    fig.add_annotation(x=xmax * 0.97, y=len(BANDS) - 0.5, text=f"<b>Women</b>", showarrow=False, xanchor="right", font=dict(size=14, color=VIOLET))
    fig.add_annotation(x=-pm[Y0][2] - 0.05, y=2, text=f"<b>{Y0}</b> outline<br>(population {total[Y0] / 1e6:.0f} million)", showarrow=True, arrowhead=0,
                       arrowcolor=INK, ax=-70, ay=-30, xanchor="right", font=dict(size=12, color=INK), align="right")
    fig.add_annotation(x=pw[Y1][16], y=16, text=f"<b>{Y1}</b> bars<br>(population {total[Y1] / 1e6:.0f} million)", showarrow=False,
                       yshift=-30, xanchor="right", font=dict(size=12, color=VIOLET), align="right")
    older = (men[Y1][13:].sum() + women[Y1][13:].sum()) / total[Y1] * 100
    titles(fig, "Japan's population pyramid has turned upside down",
           f"Population by five-year age band and sex, share of the total. In {Y1}, {older:.0f}% of people were 65 or older.",
           "Source: World Bank World Development Indicators (SP.POP.* age-band series), fetched from the World Bank API.")
    save(CHART_NUM, fig, width=1280, height=900)

Made with Plotly