"""New York's subway against London's Underground, the Paris Métro and Tokyo Metro since the pandemic: the
analysis behind a SparkyData article.

    py src/build_article.py four-metros-since-2019

How much of its last normal year before the pandemic each of four big metros carries now, and how each grew
over the thirty years before (card ``world_metro_trips``; New York's daily counts, card ``mta_ridership``).
Each system is compared only with itself, because each counts a trip its own way:
- New York: subway entries by calendar year (the MTA's published totals; 2025 the sum of its daily
  estimates), against 2019; this year from the daily file, and weekdays and weekends against the MTA's own
  comparable pre-pandemic days;
- London: Underground journeys by financial year (April to March), against 2018/19, the last year March 2020
  did not cut; this year from TfL's four-weekly periods, per day against the same periods of 2019/20; and the
  Elizabeth line beside it, since it opened through central London in May 2022;
- Paris: RATP métro trips by calendar year, against 2019 (its 2019 ran 4% below 2018, so 2018 is shown as a check);
- Tokyo: Tokyo Metro's boardings by fiscal year (April to March), against fiscal 2018, the last year the
  pandemic did not touch; fiscal 2025 is not yet published.

A share of a base year is a ratio of two totals, with no sampling error; it is reported as it is. The long
view indexes each system to its own base year (= 100) from its first year in the files.

Writes results.json, viz.json, charts.json, data.csv and build.json into output/articles/four_metros_since_2019/
and site/public/articles/four-metros-since-2019/, and copies this script there as analysis.py. Deterministic:
no sampling, and SEED is kept for any resampling.
"""

from __future__ import annotations

import re

import pandas as pd

from article_kit import Article, lines, r, strip
from nycdata import mta, world_population as P, world_transit as W
from sparkylib import yoy

SLUG = "four-metros-since-2019"
SEED = 20261106
PARIS_RA = ("paris_rapport_annuel_2025_transports_en_commun", "paris_ra2025_transports_en_commun.pdf")
MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September",
               "October", "November", "December"]
#: system -> (label, base year, latest year, year kind)
SHORT = {"new_york": "New York", "london": "London", "paris": "Paris", "tokyo": "Tokyo"}   # end labels on the long view
SYSTEMS = {"new_york": ("New York subway", 2019, 2025, "calendar"),
           "london": ("London Underground", 2018, 2025, "financial"),
           "paris": ("Paris Métro", 2019, 2025, "calendar"),
           "tokyo": ("Tokyo Metro", 2018, 2024, "fiscal")}


def system_trips() -> dict[str, pd.Series]:
    """Each system's trips by year, in millions (London and Tokyo by the year their fiscal year starts in)."""
    t = W.tokyo()
    return {"new_york": W.new_york().set_index("year")["trips"] / 1e6,
            "london": W.london().set_index("fy")["journeys"],
            "paris": W.paris().set_index("year")["trips"],
            "tokyo": t[t["operator"].eq("Tokyo Metro")].set_index("fy")["boardings"] / 1e3}


def label(system: str, year: int) -> str:
    kind = SYSTEMS[system][3]
    return f"{year}/{(year + 1) % 100:02d}" if kind == "financial" else (f"fiscal {year}" if kind == "fiscal" else str(year))


def main(argv: list[str] | None = None) -> int:
    """Run the analysis and write every file (no options)."""
    A = Article(SLUG, __file__, seed=SEED)
    y = system_trips()

    # ---- recovery: the latest year against each system's last normal year
    rec = {}
    for k, (name, base, latest, _) in SYSTEMS.items():
        rec[k] = {"base_year": base, "latest_year": latest, "base_label": label(k, base), "latest_label": label(k, latest),
                  "base": r(y[k][base], 1), "latest": r(y[k][latest], 1), "share_pct": r(100 * y[k][latest] / y[k][base], 1)}
    A.results["recovery"] = rec
    assert min(rec, key=lambda k: rec[k]["share_pct"]) == "new_york", rec     # the page: New York has won back the least
    lon = W.london().set_index("fy")
    both = lon["journeys"] + lon["elizabeth"]
    A.results["london_with_elizabeth"] = {"base": r(both[2018], 1), "latest": r(both[2025], 1),
                                          "share_pct": r(100 * both[2025] / both[2018], 1),
                                          "elizabeth_latest": r(lon.loc[2025, "elizabeth"], 1),
                                          "elizabeth_base": r(lon.loc[2018, "elizabeth"], 1)}
    # the measures table: each system's base-year and latest totals, in millions
    A.results["table"] = {k: {"base_m": r(y[k][SYSTEMS[k][1]], 0), "latest_m": r(y[k][SYSTEMS[k][2]], 0)} for k in SYSTEMS}
    A.results["paris_vs_2019"] = r(100 * (y["paris"][2025] / y["paris"][2019] - 1), 1)
    A.results["paris_vs_2018"] = r(100 * y["paris"][2025] / y["paris"][2018], 0)
    A.results["paris_2019_vs_2018"] = r(100 * (1 - y["paris"][2019] / y["paris"][2018]), 0)
    lp = W.london_periods()
    p13 = lp[lp["period"] == 13].set_index("fy")
    A.results["london_march_2020"] = {"pct_below_2019": r(100 * (1 - (p13.loc[2019, "journeys"] / p13.loc[2019, "days"])
                                                                / (p13.loc[2018, "journeys"] / p13.loc[2018, "days"])), 0)}
    A.results["tokyo_fy2019_vs_fy2018"] = r(100 * (y["tokyo"][2019] / y["tokyo"][2018] - 1), 2)
    A.results["london_vs_2019_20"] = r(100 * y["london"][2025] / y["london"][2019], 1)
    A.results["tokyo_vs_fy2019"] = r(100 * y["tokyo"][2024] / y["tokyo"][2019], 1)
    tt = W.tokyo()
    toei = tt[tt["operator"].eq("Toei")].set_index("fy")["boardings"]
    A.results["toei"] = {"share_fy2018_pct": r(100 * toei[2024] / toei[2018], 0)}

    # ---- the thirty years before: each system's growth to its base year from its first year (or 1991)
    grow = {}
    for k, (name, base, _, _) in SYSTEMS.items():
        first = max(int(y[k].index.min()), 1991)
        grow[k] = {"first": first, "first_label": label(k, first), "pct": r(100 * (y[k][base] / y[k][first] - 1), 0),
                   "first_m": r(y[k][first], 0), "y2000_m": r(y[k][2000], 0), "y2000_label": label(k, 2000),
                   "base_m": r(y[k][base], 0), "base_label": label(k, base)}
    A.results["growth"] = grow
    g2000 = {k: r(100 * (y[k][SYSTEMS[k][1]] / y[k][2000] - 1), 0) for k in SYSTEMS}
    A.results["growth_2000"] = g2000
    assert max(g2000, key=g2000.get) == "london" and min(g2000, key=g2000.get) == "paris", g2000   # the page: fastest, slowest
    near = min((t for t in y["new_york"].index if t < 2019), key=lambda t: abs(y["new_york"][t] - y["new_york"][2025]))
    A.results["nyc_2025_like"] = {"year": int(near), "gap_pct": r(100 * (y["new_york"][2025] / y["new_york"][near] - 1), 2)}
    assert near == 1999 and abs(A.results["nyc_2025_like"]["gap_pct"]) < 1, A.results["nyc_2025_like"]   # the page: "where it stood in 1999"
    A.results["low_2020"] = {k: r(100 * y[k][2020] / y[k][SYSTEMS[k][1]], 0) for k in SYSTEMS}
    A.results["y2022"] = {k: r(100 * y[k][2022] / y[k][SYSTEMS[k][1]], 0) for k in SYSTEMS}
    ny = y["new_york"]
    peak = int(ny.loc[:2019].idxmax())         # over the whole series, from 1975
    A.results["nyc_peak"] = {"year": peak, "trips": r(ny[peak], 1), "fall_to_2019_pct": r(100 * (1 - ny[2019] / ny[peak]), 1)}
    # each other system's highest year before the pandemic, over its whole series (London from 1991/92, Paris and
    # Tokyo Metro from 2000): all three in their 2018 years
    others_peak = {k: int(y[k].loc[:2019].idxmax()) for k in ("london", "paris", "tokyo")}
    A.results["others_peak_year"] = others_peak
    assert peak == 2015 and set(others_peak.values()) == {2018}, (peak, others_peak)   # the page's However
    ev = mta.fare_evasion()
    e19, e25 = ev[ev["year"] == 2019]["share"].mean(), ev[ev["year"] == 2025]["share"].mean()
    evq = ev.set_index(["year", "quarter"])["share"]
    daily_sub = mta.long()
    daily_sub = daily_sub[daily_sub["Mode"] == "Subway"].set_index("Date")["Count"]
    q25 = daily_sub.loc["2025-01-01":"2025-12-31"].groupby(lambda t: (t.month - 1) // 3 + 1).sum()
    imp19 = mta.implied_2019_by_month("Subway", base_year=2024, skip_leap_day=True)
    q19 = imp19.groupby(lambda m: (m - 1) // 3 + 1).sum()
    f25 = sum(q25[q] / (1 - evq[(2025, q)]) for q in range(1, 5)) / q25.sum()
    f19 = sum(q19[q] / (1 - evq[(2019, q)]) for q in range(1, 5)) / q19.sum()
    A.results["evasion"] = {"share_2019_pct": r(100 * e19, 0), "share_2025_pct": r(100 * e25, 0),
                            "entries_share_pct": r(100 * (ny[2025] * f25) / (ny[2019] * f19), 0),
                            "entries_share_simple_pct": r(100 * (ny[2025] / (1 - e25)) / (ny[2019] / (1 - e19)), 1)}
    # New York on London's April-to-March year, for the latest year too
    fy25 = daily_sub.loc["2025-04-01":"2026-03-31"]
    assert len(fy25) == 365
    A.results["nyc_fy2025"] = {"share_pct": r(100 * fy25.sum() / 1e6 / ny[2019], 0)}
    daily = mta.long()
    sub = daily[daily["Mode"] == "Subway"].set_index("Date")["Count"]
    fy20 = sub.loc["2020-04-01":"2021-03-31"]
    assert len(fy20) == 365, len(fy20)
    A.results["nyc_fy2020"] = {"share_pct": r(100 * fy20.sum() / 1e6 / ny[2019], 0)}
    # per resident: a count set against a year decades away carries its reading per resident (style guide, section 4)
    pn = P.population("new_york").set_index("year")["pop"]
    pl = P.population("london").set_index("year")["pop"]
    A.results["per_resident"] = {
        "nyc": {str(t): r(1e6 * ny[t] / pn[t], 0) for t in (1999, 2019, 2025)},
        "london": {str(t): r(1e6 * y["london"][t] / pl[t], 0) for t in (1995, 2018, 2025)},
    }

    # ---- this year
    d = mta.subway_daily_years(through=(10, 28))
    d = d.set_index("year")
    summ = mta.summary()
    last = pd.Timestamp(summ["last_date"])
    imp = mta.implied_2019_by_month("Subway", base_year=2024, skip_leap_day=True)
    # whole months only, January to September, against the 2019 months the comparable days imply
    d9 = mta.subway_daily_years(through=(9, 30)).set_index("year")
    A.results["nyc_2026_vs_2019"] = {"share_pct": r(100 * d9.loc[2026, "trips"] / imp.loc[1:9].sum(), 0), "months": 9}
    dsub = mta.long()
    dsub = dsub[dsub["Mode"] == "Subway"].set_index("Date")["Count"]
    ch = yoy.compare_years(dsub, base_year=2025, current_year=2026, last=last, rng=A.rng)
    A.results["now_nyc_range"] = {"change_pct": r(ch.change_pct, 1), "lo_pct": r(ch.lo_pct, 1), "hi_pct": r(ch.hi_pct, 1)}
    if ch.direction == "up":
        A.claim("now_nyc_range", "up")
    win = yoy.compare_years(dsub, base_year=2025, current_year=2026, last="2026-08-22", start=(4, 1), rng=A.rng)
    A.results["nyc_london_window"] = {"change_pct": r(win.change_pct, 1), "lo_pct": r(win.lo_pct, 1), "hi_pct": r(win.hi_pct, 1),
                                      "per_day_m": r(win.mean_current / 1e6, 2), "per_day_2025_m": r(win.mean_base / 1e6, 2),
                                      "level": win.level}
    A.results["now_nyc"] = {"now": int(d.loc[2026, "trips"]), "before": int(d.loc[2025, "trips"]), "days": int(d.loc[2026, "days"]),
                            "change_pct": r(100 * (d.loc[2026, "trips"] / d.loc[2025, "trips"] - 1), 1),
                            "last_day": last.date().isoformat()}
    assert int(d.loc[2026, "days"]) == int(d.loc[2025, "days"]) == 301, d
    split = {(row["year"], row["day_type"]): row["Subway_vs_2019"] for row in summ["years"]}
    # whole years only: the baseline averages every day of 2024, so a part year would set January to September
    # against a whole year's weekends (the weekend baseline is seasonal; the weekday one is not)
    A.results["nyc_daytype"] = {str(yy): {"weekday": r(100 * split[(yy, "weekday")], 0), "weekend": r(100 * split[(yy, "weekend")], 0)}
                                for yy in range(2021, 2026)}
    rows25 = {row["day_type"]: row for row in summ["years"] if row["year"] == 2025}
    base = summ["baseline"]["Subway"]
    miss = {t: rows25[t]["days"] * (base[t] - rows25[t]["Subway"]) for t in ("weekday", "weekend")}
    A.results["method"] = {
        "level": f"{ch.level:.0f}%",
        "this_year": ("the same days of 2025 and 2026, a matched-week bootstrap of the ratio (sparkylib.yoy.compare_years) "
                      f"with seed {SEED}"),
        "new_york_years": "the MTA's published totals to 2024 (NYU Rudin Center's tabulation of NYCT counts before 2014); 2025 the sum of the daily file",
        "london_years": "the GLA's table to 2009/10, TfL's four-weekly periods from 2010/11",
        "paris_years": "SDES to 2024, the City of Paris for 2025",
        "tokyo_years": "each fiscal year's TMG statistical yearbook",
        "weekdays": "whole years against the 2019 baseline rebuilt from the MTA's comparable-day percentages in its daily file"}
    A.results["nyc_missing_2025"] = {"weekday_share_pct": r(100 * miss["weekday"] / sum(miss.values()), 0),
                                     "missing_millions": r(sum(miss.values()) / 1e6, 0),
                                     "annual_gap_millions": r(ny[2019] - ny[2025], 0)}
    p26, p19, p25 = W.london_part_year(2026, 5), W.london_part_year(2019, 5), W.london_part_year(2025, 5)
    per_period = lp[lp["fy"].isin([2025, 2026]) & (lp["period"] <= 5)].assign(pd_=lambda x: x["journeys"] / x["days"])
    pp = per_period.pivot_table(index="period", columns="fy", values="pd_")
    A.results["london_periods_below"] = int((pp[2026] < pp[2025]).sum())
    A.results["now_london"] = {"periods": 5, "first_day": p26["first_day"], "last_day": p26["last_day"],
                               "per_day": r(p26["per_day"], 2), "per_day_2019": r(p19["per_day"], 2), "per_day_2025": r(p25["per_day"], 2),
                               "share_2019_pct": r(100 * p26["per_day"] / p19["per_day"], 0),
                               "vs_2025_pct": r(100 * (p26["per_day"] / p25["per_day"] - 1), 1),
                               "with_elizabeth_share_pct": r(100 * (p26["per_day"] + p26["elizabeth_per_day"])
                                                             / (p19["per_day"] + p19["elizabeth_per_day"]), 0)}
    A.latest(p26["last_day"], "TfL Underground journeys, periods 1-5 of 2026/27; MTA daily subway ridership to "
             f"{A.results['now_nyc']['last_day']}")
    A.long_view(grow["new_york"]["first"], 2025, "Subway entries in New York, 1991-2025, and Underground journeys in London, "
                "1991/92-2025/26, each indexed to its last normal year; Paris from 2000, Tokyo Metro from fiscal 2000")

    # ---- the documents
    Q = A.quotes()
    quotes = {}
    for key, (src, needle) in {"paris_precovid": (PARIS_RA, "Par rapport à la situation pré-COVID, le réseau Métro est en hausse de 1,1 %"),
                               "paris_line14": (PARIS_RA, "ligne 14 en avril 2024 jusqu")}.items():
        q = Q.quote(src[0], src[1], needle)
        quotes[key] = {"quote": q["quote"], "page": q.get("page"), "source": q.get("source")}
    A.results["quotes"] = quotes
    A.results["history"] = {"paris_precovid_pct": float(re.search(r"hausse de ([\d,]+) %", quotes["paris_precovid"]["quote"])
                                                         .group(1).replace(",", "."))}

    # ---- the reproduction
    A.reproduce("London Underground journeys, 2024/25, millions (TfL Q4 2024/25 report)", published=1216, published_moe90=None,
                reproduced=r(y["london"][2024], 0), kind="count", tolerance=4)
    A.reproduce("Paris métro trips, 2025 against 2019 (the City of Paris's report), percent", published=A.results["history"]["paris_precovid_pct"],
                published_moe90=None, reproduced=A.results["paris_vs_2019"], kind="pct", tolerance=0.05)
    A.reproduce("New York subway trips, 2024, millions (the MTA's yearly page against its daily file)",
                published=r(y["new_york"][2024], 1), published_moe90=None,
                reproduced=r(mta.subway_daily_years().set_index("year").loc[2024, "trips"] / 1e6, 1), kind="count", tolerance=1.0)
    A.results["reproduction_source"] = "TfL's Q4 2024/25 Quarterly Performance Report; the City of Paris's 2025 annual report; the MTA"
    A.results["reproduction_note"] = ("TfL's report preceded later revisions (within 0.3%); the MTA's daily estimates come within "
                                      "0.06% of its published year.")

    # ---- printed figures
    R = A.results["recovery"]
    for k in SYSTEMS:
        A.printed(f"recovery.{k}.share_pct", f"{R[k]['share_pct']:.0f}%")
    A.printed("recovery.new_york.base", f"{R['new_york']['base'] / 1000:.2f} billion")
    A.printed("recovery.new_york.latest", f"{R['new_york']['latest'] / 1000:.2f} billion")
    A.printed("london_with_elizabeth.share_pct", f"{A.results['london_with_elizabeth']['share_pct']:.0f}%")
    A.printed("london_with_elizabeth.elizabeth_base", f"{A.results['london_with_elizabeth']['elizabeth_base']:.0f} million")
    for k in SYSTEMS:
        A.printed(f"table.{k}.base_m", f"{A.results['table'][k]['base_m']:,.0f}")
        A.printed(f"table.{k}.latest_m", f"{A.results['table'][k]['latest_m']:,.0f}")
    A.printed("now_nyc_range.lo_pct", f"{A.results['now_nyc_range']['lo_pct']:.1f}%")
    A.printed("now_nyc_range.hi_pct", f"{A.results['now_nyc_range']['hi_pct']:.1f}%")
    A.printed("london_with_elizabeth.elizabeth_latest", f"{A.results['london_with_elizabeth']['elizabeth_latest']:.0f} million")
    A.printed("paris_vs_2019", f"{A.results['paris_vs_2019']:.1f}%")
    A.printed("london_vs_2019_20", f"{A.results['london_vs_2019_20']:.0f}%")
    G = A.results["growth"]
    for k in SYSTEMS:
        A.printed(f"growth.{k}.pct", f"{G[k]['pct']:.0f}%")
    for k in SYSTEMS:
        A.printed(f"growth_2000.{k}", f"{g2000[k]:.0f}%")
    A.printed("low_2020.london", f"{A.results['low_2020']['london']:.0f}%")
    N = A.results["now_nyc"]
    A.printed("now_nyc.change_pct", f"{N['change_pct']:.1f}%")
    A.printed("now_nyc.last_day", f"{int(N['last_day'][8:])} {MONTH_NAMES[int(N['last_day'][5:7]) - 1]}")
    DT = A.results["nyc_daytype"]
    A.printed("nyc_daytype.2025.weekday", f"{DT['2025']['weekday']:.0f}%")
    A.printed("nyc_daytype.2025.weekend", f"{DT['2025']['weekend']:.0f}%")
    A.printed("nyc_2026_vs_2019.share_pct", f"{A.results['nyc_2026_vs_2019']['share_pct']:.0f}%")
    A.printed("nyc_peak.fall_to_2019_pct", f"{A.results['nyc_peak']['fall_to_2019_pct']:.0f}%")
    A.printed("evasion.share_2019_pct", f"{A.results['evasion']['share_2019_pct']:.0f}%")
    A.printed("evasion.share_2025_pct", f"{A.results['evasion']['share_2025_pct']:.0f}%")
    A.printed("evasion.entries_share_pct", f"{A.results['evasion']['entries_share_pct']:.0f}%")
    for t in ("1999", "2019", "2025"):
        A.printed(f"per_resident.nyc.{t}", f"{A.results['per_resident']['nyc'][t]:.0f}")
    for t in ("1995", "2018", "2025"):
        A.printed(f"per_resident.london.{t}", f"{A.results['per_resident']['london'][t]:.0f}")
    A.printed("nyc_fy2020.share_pct", f"{A.results['nyc_fy2020']['share_pct']:.0f}%")
    for k in SYSTEMS:
        A.printed(f"y2022.{k}", f"{A.results['y2022'][k]:.0f}%")
    L = A.results["now_london"]
    A.printed("now_london.vs_2025_pct", f"{abs(L['vs_2025_pct']):.1f}%")
    A.printed("now_london.last_day", f"{int(L['last_day'][8:])} {MONTH_NAMES[int(L['last_day'][5:7]) - 1]}")
    A.printed("history.paris_precovid_pct", "1.1%")
    A.printed("paris_vs_2018", f"{A.results['paris_vs_2018']:.0f}%")
    A.printed("paris_2019_vs_2018", f"{A.results['paris_2019_vs_2018']:.0f}%")
    A.printed("london_march_2020.pct_below_2019", f"{A.results['london_march_2020']['pct_below_2019']:.0f}%")
    A.printed("nyc_missing_2025.weekday_share_pct", f"{A.results['nyc_missing_2025']['weekday_share_pct']:.0f}%")
    A.printed("nyc_fy2025.share_pct", f"{A.results['nyc_fy2025']['share_pct']:.0f}%")
    A.printed("nyc_london_window.change_pct", f"{A.results['nyc_london_window']['change_pct']:.1f}%")
    A.printed("toei.share_fy2018_pct", f"{A.results['toei']['share_fy2018_pct']:.0f}%")
    A.printed("tokyo_vs_fy2019", f"{A.results['tokyo_vs_fy2019']:.0f}%")
    for k in SYSTEMS:
        if k in ("new_york", "london"):
            A.printed(f"growth.{k}.first_m", f"{G[k]['first_m']:,.0f}")
        A.printed(f"growth.{k}.y2000_m", f"{G[k]['y2000_m']:,.0f}")
    W_ = A.results["nyc_london_window"]
    A.printed("nyc_london_window.per_day_m", f"{W_['per_day_m']:.2f} million")
    A.printed("nyc_london_window.per_day_2025_m", f"{W_['per_day_2025_m']:.2f} million")
    A.printed("now_london.per_day", f"{L['per_day']:.2f} million")
    A.printed("now_london.per_day_2025", f"{L['per_day_2025']:.2f} million")
    A.printed("now_nyc.now", f"{N['now'] / 1e6:,.0f} million")
    A.printed("now_nyc.before", f"{N['before'] / 1e6:,.0f} million")
    A.printed("nyc_missing_2025.missing_millions", f"{A.results['nyc_missing_2025']['missing_millions']:,.0f} million")
    A.printed("nyc_missing_2025.annual_gap_millions", f"{A.results['nyc_missing_2025']['annual_gap_millions']:,.0f} million")

    # ---- charts
    A.chart("recovery", strip(
        title="New York's subway has won back the smallest share of its last normal year",
        subtitle="Trips in 2022 and in each system's latest year, as a share of its last normal year before the pandemic",
        x=[["2022", "2022 (2022/23 in London, fiscal 2022 in Tokyo)"], ["latest", "Latest year"]],
        series=[[k, f"{SYSTEMS[k][0]} ({R[k]['latest_label']} against {R[k]['base_label']})", SYSTEMS[k][0]] for k in SYSTEMS],
        cells={"": {k: {"2022": A.results["y2022"][k], "latest": R[k]["share_pct"]} for k in SYSTEMS}},
        format="pct", axis="Share of the last normal year",
        note="Each system against itself, because each counts a trip its own way: New York entries (2019), London's "
             "Underground journeys (2018/19, April to March), Paris's métro entries (2019), "
             "Tokyo Metro's boardings (fiscal 2018). New York's are paid entries; counting fare evaders it would stand at "
             "about four-fifths of 2019.",
        provenance="MTA; Transport for London; SDES and the City of Paris; Tokyo Metropolitan Government"))

    years = list(range(1991, 2026))
    lowest = {}
    for t in range(2021, 2026):
        shares = {k: y[k][t] / y[k][SYSTEMS[k][1]] for k in SYSTEMS if t in y[k].index}
        lowest[str(t)] = min(shares, key=shares.get)
    A.results["lowest_by_year"] = lowest            # the long-view caption: New York lowest of the four from 2021
    assert set(lowest.values()) == {"new_york"}, lowest
    def idx(k):
        base = y[k][SYSTEMS[k][1]]
        return {str(t): r(100 * y[k][t] / base, 1) for t in years if t in y[k].index}
    A.chart("long_view", lines(
        title="New York's subway peaked in 2015; the others rose to records before the pandemic",
        subtitle="Trips each year as a share of each system's last normal year before the pandemic (= 100)",
        x=[[str(t), str(t)] for t in years],
        series=[[k, SYSTEMS[k][0], SHORT[k]] for k in SYSTEMS],
        cells={"": {k: idx(k) for k in SYSTEMS}}, format="dec1", axis="Index, last normal year = 100",
        x_ticks=[str(t) for t in years if t % 5 == 0 or t in (1991, 2025)],
        note="New York and Paris by calendar year; London and Tokyo by the year their fiscal year (April to March) starts "
             "in. London from 1991/92, Paris from 2000, Tokyo Metro from fiscal 2000; Tokyo's fiscal 2025 is not yet published.",
        provenance="MTA and NYU Rudin Center; Transport for London and the GLA; SDES and the City of Paris; Tokyo Metropolitan Government"))

    dt_years = sorted(DT)
    A.chart("weekdays", lines(
        title="New York's weekend riding has recovered more than its weekday riding",
        subtitle="Average subway trips a day, Monday to Friday and Saturday and Sunday, as a share of the MTA's comparable days of 2019",
        x=[[yy, yy] for yy in dt_years],
        series=[["weekend", "Saturdays and Sundays", "Weekends"], ["weekday", "Monday to Friday (holidays included)", "Weekdays"]],
        cells={"": {t: {yy: DT[yy][t] for yy in dt_years} for t in ("weekday", "weekend")}}, format="pct",
        axis="Share of 2019",
        note="Whole years. The MTA's own comparable pre-pandemic days, recovered from its daily file; paid entries only.",
        provenance="MTA daily ridership"))

    rows_out = []
    for k in SYSTEMS:
        for t, v in y[k].items():
            rows_out.append({"system": SYSTEMS[k][0], "year": int(t), "year_kind": SYSTEMS[k][3], "trips_millions": r(v, 3)})
    for t, row in lon.iterrows():
        rows_out.append({"system": "London Elizabeth line (TfL Rail)", "year": int(t), "year_kind": "financial",
                         "trips_millions": r(row["elizabeth"], 3)})
    A.write(data=pd.DataFrame(rows_out))
    print(f"{SLUG}: wrote {A.out}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
