"""New York's murder rate against London's, Paris's, Toronto's and Tokyo's: the analysis behind a
SparkyData article.

    py src/build_article.py murder-in-five-big-cities

How New York's killings per resident compare with four other big cities' that publish comparable
counts, now and over the longest span each city's records allow. Every city's count is set on its own
population (card ``world_city_population``), each police force's definition kept and named:
- New York: murder and non-negligent manslaughter, the State's count to 2000 and the NYPD's from 2001
  (cards ``dcjs_index_crimes``, ``nypd_crime_history``; the two agree in every year 2001-2022), and the
  current year from the NYPD's CompStat report for the week to 25 October 2026;
- London: the Metropolitan Police's homicide victims, 2003 on, and its recorded homicide offences for the
  current year (card ``world_city_homicide``, as are the three below);
- Paris and its inner ring (départements 75, 92, 93, 94; 6.9 million people on about New York's land
  area): homicide victims by the place of the crime, 2016 on (SSMSI);
- Toronto: the city's police count of deceased victims, 2004 on, and Statistics Canada's count for the
  metropolitan area, 1981 on, the one series here besides New York's that reaches 1990;
- Tokyo: victims who died (the police yearbook, 2016 on), never its recognised cases, which include
  attempts.

A rate is per 100,000 residents in the same year. A change between two counts is a rate ratio with a 90%
range (``sparkylib.stats.count_change``), named only when the range clears zero, and rechecked against
the extra swing of the series' monthly counts (``Article.recheck``). The Met's comparison of 12 January
2026 is checked on its own terms: its New York figure is set beside the rate from the NYPD's count.

Writes results.json, viz.json, charts.json, data.csv and build.json into
output/articles/murder_in_five_big_cities/ and site/public/articles/murder-in-five-big-cities/, 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 crime, world_crime as C, world_population as P
import math

from sparkylib.stats import Z90, count_change, direction, dispersion

SLUG = "murder-in-five-big-cities"
SEED = 20261029
COMPSTAT_WEEK = "2026-10-25"                 # source nypd_compstat_citywide_2026_10_25, the latest week before release
LAST = 2025                                  # the last whole year every city has
CITIES = [("new_york", "New York"), ("london", "London"), ("paris_inner", "Paris and inner ring"),
          ("toronto", "Toronto"), ("tokyo", "Tokyo")]
MET = ("met_press_homicide_2025", "met_press_homicide_2025.html")
NYPD_Q3 = ("nypd_release_q3_2026", "nypd_release_q3_2026.html")
TPS_DOC = ("tps_open_data_documentation", "tps_open_data_documentation.pdf")
SSMSI_DOC = ("fr_ssmsi_methodology_2026", "ssmsi_documentation_juillet2026.pdf")
MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September",
               "October", "November", "December"]


def city_counts() -> dict[str, pd.Series]:
    """Each city's yearly count on its own definition (indexed by year)."""
    dcjs = crime.dcjs_nyc().groupby("year")["murder"].sum()
    m = crime.major_felonies()
    nypd = m[m["offense"].str.contains("MURDER")].set_index("year")["n"]
    nyc = pd.concat([dcjs[dcjs.index <= 2000], nypd[nypd.index >= 2001]])
    tor = C.toronto_victims()
    tor = tor.groupby("year").agg(v=("victims", "sum"), n=("month", "nunique"))
    tor = tor[tor.index <= LAST]                     # the service's 2026 is part of a year
    return {"new_york": nyc.astype(int),
            "london": C.london_victims().set_index("year")["victims"],
            "paris_inner": C.paris_victims("paris_inner").set_index("year")["victims"],
            "toronto": tor[tor["n"] == 12]["v"],
            "tokyo": C.tokyo_deaths().set_index("year")["deaths"]}


def per_100k(c: pd.Series, area: str) -> pd.Series:
    return P.per_1000(c, area) * 100            # per 100,000


def main(argv: list[str] | None = None) -> int:
    """Run the analysis and write every file (no options)."""
    A = Article(SLUG, __file__, seed=SEED)
    ct = city_counts()
    rates = {k: per_100k(v, k) for k, v in ct.items()}
    pops = {k: P.population(k).set_index("year")["pop"] for k, _ in CITIES}

    # ---- 2025, every city on its own count and population
    A.results["y2025"] = {k: {"count": int(ct[k][LAST]), "pop": int(pops[k][LAST]), "rate": r(rates[k][LAST], 2)}
                          for k, _ in CITIES}
    ny = rates["new_york"][LAST]
    A.results["times_2025"] = {k: r(ny / rates[k][LAST], 1) for k, _ in CITIES if k != "new_york"}
    A.results["y2016"] = {k: r(rates[k][2016], 2) for k, _ in CITIES}
    # every year all five counts exist: which city was highest
    top = {str(t): max((k for k, _ in CITIES), key=lambda k: rates[k][t]) for t in range(2016, LAST + 1)}
    A.results["highest_by_year"] = top
    assert {t for t, k in top.items() if k != "new_york"} == {"2018"} and top["2018"] == "toronto", top
    paris_city = C.paris_victims("paris").set_index("year")["victims"]
    A.results["paris_city_2025"] = {"count": int(paris_city[LAST]), "rate": r(per_100k(paris_city, "paris")[LAST], 2)}

    # ---- the long view: New York from 1990, Toronto's metropolitan area from 1981, London from 2003
    nyr = rates["new_york"]
    cma = C.toronto_cma().set_index("year")
    A.results["long"] = {
        "first_year": int(nyr.index.min()),
        "nyc_1990": {"count": int(ct["new_york"][1990]), "rate": r(nyr[1990], 1)},
        "nyc_low": {"year": int(nyr.loc[:LAST].idxmin()), "rate": r(nyr.loc[:LAST].min(), 2),
                    "count": int(ct["new_york"][nyr.loc[:LAST].idxmin()])},
        "nyc_fall_pct": r(100 * (1 - nyr[LAST] / nyr[1990]), 0),
        "nyc_2019": r(nyr[2019], 2), "nyc_2021": r(nyr[2021], 2),
        "toronto_cma_1990": r(cma.loc[1990, "rate"], 2), "toronto_cma_2025": r(cma.loc[LAST, "rate"], 2),
        "toronto_cma_fall_pct": r(100 * (1 - cma.loc[LAST, "rate"] / cma.loc[1990, "rate"]), 0),
        "london_2003": r(rates["london"][2003], 2), "london_2019": r(rates["london"][2019], 2),
        "nyc_2003": r(nyr[2003], 2),
        "london_low_year": int(rates["london"].idxmin()),
        "nyc_count_fall_pct": r(100 * (1 - ct["new_york"][LAST] / ct["new_york"][1990]), 0),
        "nyc_fall_pct_compstat_1990": None,
        "nyc_fall_since_2003_pct": r(100 * (1 - nyr[LAST] / nyr[2003]), 0),
        "london_fall_since_2003_pct": r(100 * (1 - rates["london"][LAST] / rates["london"][2003]), 0),
    }
    A.results["long"]["london_2025_is_low"] = A.results["long"]["london_low_year"] == LAST
    assert A.results["long"]["london_2025_is_low"], "the page says London's 2025 is the lowest in its file"

    # ---- the ratio with London, every year both have
    ratio = (nyr / rates["london"]).dropna()

    def ratio_change(now_years, before_years):
        """The change in New York's multiple of London's between two sets of years, as a ratio of rate
        ratios with a 90% range: Poisson on the four counts, then scaled by the larger extra swing of the two
        cities' monthly counts (the recheck)."""
        cn, cb = ct["new_york"], ct["london"]
        pn, pb = pops["new_york"], pops["london"]
        a1, b1 = cn[now_years].sum(), cb[now_years].sum()
        a0, b0 = cn[before_years].sum(), cb[before_years].sum()
        rr = (a1 / pn[now_years].sum() / (b1 / pb[now_years].sum())) / (a0 / pn[before_years].sum() / (b0 / pb[before_years].sum()))
        var = 1 / a1 + 1 / b1 + 1 / a0 + 1 / b0
        phi = max(1.0, dispersion(monthly.loc["2014-01-01":"2025-12-01"].to_numpy()),
                  dispersion(lv[(lv["year"] >= 2014) & (lv["year"] <= LAST)]["victims"].to_numpy()))
        out = {"ratio_of_ratios": r(rr, 3), "dispersion": r(phi, 2),
               "base_multiple": r((a0 / pn[before_years].sum()) / (b0 / pb[before_years].sum()), 2),
               "years": [int(t) for t in now_years], "base_years": [int(t) for t in before_years]}
        for name, scale in (("plain", 1.0), ("rechecked", phi)):
            se = math.sqrt(var * scale)
            lo, hi = math.exp(math.log(rr) - Z90 * se), math.exp(math.log(rr) + Z90 * se)
            out[name] = {"change_pct": r(100 * (rr - 1), 1), "lo_pct": r(100 * (lo - 1), 1), "hi_pct": r(100 * (hi - 1), 1)}
        return out
    lv = C.london_victims_monthly()
    c_m0, _ = crime.city_complaints()
    monthly = c_m0[c_m0["ofns_desc"].str.contains("MURDER", na=False)].groupby("m")["n"].sum()
    A.results["gap_vs_2017_2019"] = ratio_change([LAST], [2017, 2018, 2019])
    A.results["gap_vs_2022"] = ratio_change([LAST], [2022])
    assert direction(A.results["gap_vs_2017_2019"]["rechecked"]["lo_pct"], A.results["gap_vs_2017_2019"]["rechecked"]["hi_pct"]) == "up"
    assert direction(A.results["gap_vs_2022"]["plain"]["lo_pct"], A.results["gap_vs_2022"]["plain"]["hi_pct"]) == "unclear"
    A.results["ratio_london"] = {str(y): r(v, 2) for y, v in ratio.items()}
    low3 = ratio.loc[2017:2019]
    A.results["ratio"] = {"y2003": r(ratio[2003], 1), "min_2017_2019": r(low3.min(), 1), "max_2017_2019": r(low3.max(), 1),
                          "y2022": r(ratio[2022], 1), "y2025": r(ratio[LAST], 1),
                          "peak_year": int(ratio.idxmax()), "peak": r(ratio.max(), 1)}

    # ---- this year: New York's CompStat week; London's and Toronto's first nine months; Tokyo's cases
    cs = crime.compstat(week=COMPSTAT_WEEK)
    rows = dict(cs["rows"]) if isinstance(cs["rows"], list) else cs["rows"]
    mur = rows["Murder"]
    A.results["now_nyc"] = count_change(mur["ytd"], mur["ytd_prior"])
    A.results["now_nyc"].update({"week_end": cs["week_end"]})
    c_m, _ = crime.city_complaints()
    monthly = c_m[c_m["ofns_desc"].str.contains("MURDER", na=False)].groupby("m")["n"].sum()
    A.recheck("now_nyc", monthly.loc["2022-01-01":].to_numpy())
    if A.results["now_nyc"]["direction"] == "down":
        A.claim("now_nyc", "down")

    # January to August: the borough file's September and the Toronto feature service's (to 26 September) are
    # still filling in
    lm = C.london_offences_monthly()
    l26 = lm[(lm["year"] == 2026) & (lm["month"] <= 8)]["offences"].sum()
    l25 = lm[(lm["year"] == 2025) & (lm["month"] <= 8)]["offences"].sum()
    A.results["now_london"] = count_change(l26, l25)
    lv = C.london_victims_monthly()
    A.recheck("now_london", lv[lv["year"] >= 2015]["victims"].to_numpy())
    tv = C.toronto_victims()
    t26 = tv[(tv["year"] == 2026) & (tv["month"] <= 8)]["victims"].sum()
    t25 = tv[(tv["year"] == 2025) & (tv["month"] <= 8)]["victims"].sum()
    A.results["now_toronto"] = count_change(t26, t25)
    A.recheck("now_toronto", tv[tv["year"] >= 2015]["victims"].to_numpy())
    A.results["now_tokyo_cases"] = C.tokyo_recognised_so_far()
    # the agency's 2025 murder group (110) is the yearbook's murder row (107) plus infanticide (3)
    tk_ = C.tokyo_deaths().set_index("year")
    yb = C._t30_rows(C.MPD_T30[LAST])
    assert A.results["now_tokyo_cases"]["final_2025"] == yb["殺人"]["recognised"] + yb["嬰児殺"]["recognised"]
    A.results["tokyo_infanticide_cases_2025"] = int(yb["嬰児殺"]["recognised"])
    tk = C.tokyo_deaths().set_index("year")
    A.results["tokyo_2025"] = {"deaths": int(tk.loc[LAST, "deaths"]), "recognised": int(tk.loc[LAST, "recognised"]),
                               "murder_dead": int(tk.loc[LAST, "murder"]), "assault_deaths": int(tk.loc[LAST, "assault_deaths"])}
    wide = tk.loc[LAST, "deaths"] + tk.loc[LAST, "assault_deaths"]
    A.results["tokyo_2025"]["rate_with_assault"] = r(1e5 * wide / pops["tokyo"][LAST], 2)
    A.results["tokyo_2025"]["times_with_assault"] = r(rates["new_york"][LAST] / (1e5 * wide / pops["tokyo"][LAST]), 1)
    tor_city = C.toronto_victims().groupby("year")["victims"].sum()
    A.results["toronto_2024"] = {"count": int(tor_city[2024]), "count_2025": int(tor_city[LAST]),
                                 "times_nyc": r(rates["new_york"][2024] / rates["toronto"][2024], 1),
                                 "fewest_in_file": bool(tor_city.loc[2004:LAST].idxmin() == LAST),
                                 "cma_2024": r(C.toronto_cma().set_index("year").loc[2024, "rate"], 1)}
    assert A.results["toronto_2024"]["fewest_in_file"]
    tp = pops["tokyo"][LAST]
    A.results["tokyo_revision_sensitivity"] = {
        f"p{str(pct).replace('.', '')}": {"rate": r(1e5 * ct["tokyo"][LAST] / (tp * (1 - pct / 100)), 2),
                   "times": r(rates["new_york"][LAST] / (1e5 * ct["tokyo"][LAST] / (tp * (1 - pct / 100))), 1)}
        for pct in (0.26, 0.5)}
    A.results["tokyo_revision_sensitivity"].update({
        "assumption": True,
        "reason": ("TMG revised November 2020 to September 2026 to the 2025 census on 29 October 2026, moving 1 January "
                   "2026 from 14,270,748 to 14,234,309 (0.26% lower; source tmg_population_monthly's note); the "
                   "revised table was not at a fetchable address, so 2025's population is lowered by the same share "
                   "(p026) and by 0.5% (p05)")})
    A.results["paris_city_rank_2025"] = 1 + sum(rates[k][LAST] > A.results["paris_city_2025"]["rate"] for k in ("new_york", "london", "toronto", "tokyo"))
    assert A.results["paris_city_rank_2025"] == 2, A.results["paris_city_rank_2025"]
    A.latest(cs["week_end"], "NYPD CompStat citywide report, year to date (murders)")
    A.long_view(A.results["long"]["first_year"], LAST, "New York City murders per 100,000 residents, 1990-2025 "
                "(the State's count to 2000, the NYPD's from 2001); Toronto's metropolitan area from 1981 "
                "(Statistics Canada)")

    # ---- the beliefs
    lv18 = lv[lv["year"] == 2018].set_index("month")["victims"]
    nyc18 = monthly.loc["2018-01-01":"2018-12-01"]
    A.results["belief_2018"] = {"london_feb": int(lv18[2]), "nyc_feb": int(nyc18["2018-02-01"]),
                                "london_mar": int(lv18[3]), "nyc_mar": int(nyc18["2018-03-01"]),
                                "london_year": int(ct["london"][2018]), "nyc_year": int(ct["new_york"][2018]),
                                "london_rate": r(rates["london"][2018], 2), "nyc_rate": r(nyr[2018], 2),
                                "months_london_above": int(sum(lv18[k] > nyc18.iloc[k - 1] for k in range(1, 13)))}
    lvm = lv.set_index(["year", "month"])["victims"]
    above, level, months = [], [], 0
    for t, n in monthly.loc["2014-01-01":"2025-12-01"].items():
        k = (t.year, t.month)
        if k in lvm.index:
            months += 1
            if lvm[k] > n:
                above.append(f"{t.year}-{t.month:02d}")
            elif lvm[k] == n:
                level.append(f"{t.year}-{t.month:02d}")
    A.results["belief_months"] = {"months": months, "above": above, "level": level, "n_above": len(above)}
    assert above == ["2018-02", "2019-12"], above
    A.results["belief_months"]["dec2019_london"] = int(lvm[(2019, 12)])
    A.results["belief_months"]["dec2019_nyc"] = int(monthly["2019-12-01"])
    # the same months as rates, each count on its city's population for the year
    month_rates = {m: {"london": r(1e5 * lvm[(int(m[:4]), int(m[5:]))] / pops["london"][int(m[:4])], 3),
                       "new_york": r(1e5 * monthly[f"{m}-01"] / pops["new_york"][int(m[:4])], 3)} for m in above}
    A.results["belief_months"]["rates"] = month_rates
    A.results["belief_months"]["above_rate"] = [m for m, v in month_rates.items() if v["london"] > v["new_york"]]
    assert A.results["belief_months"]["above_rate"] == above, "the page says both months were a lead in rates too"
    # how the ranges are made, and the files behind two series, as the methods paragraphs say
    A.results["method"] = {
        "level": "90%",
        "change_test": ("a ratio of two counts with a Poisson 90% range (sparkylib.stats.count_change), then the range "
                        "widened by the dispersion of the city's monthly counts (Article.recheck)"),
        "dispersion_months": {"now_nyc": "New York's complaint murders by month, 2022-01 to the latest month",
                              "now_london": "London's homicide victims by month, 2015-01 to 2025-12",
                              "now_toronto": "Toronto's victims by month, 2015-01 to 2026-09",
                              "gap": "the larger of New York's and London's monthly dispersions, 2014-01 to 2025-12"},
        "gap_test": ("the ratio of New York's rate ratio to London's between two sets of years, a log-normal 90% range "
                     "from the four counts' Poisson variances, scaled by the gap's dispersion")}
    A.results["toronto_sources"] = {"open_data_last": "2023-12",
                                    "feature_service": f"{C.TPS_SERVICE_FIRST[0]}-{C.TPS_SERVICE_FIRST[1]:02d} to "
                                                       f"{C.TPS_SERVICE_LAST[0]}-{C.TPS_SERVICE_LAST[1]:02d}"}
    A.results["population_dates"] = {k: str(P.population(k)["date"].iloc[-1]) for k, _ in CITIES}
    lo25 = lm[lm["year"] == LAST]["offences"].sum()
    A.results["london_files_2025"] = {"victims": int(ct["london"][LAST]), "offences": int(lo25)}
    A.results["belief_toronto_2018"] = {"toronto": r(rates["toronto"][2018], 2), "nyc": r(nyr[2018], 2)}

    # ---- the documents
    Q = A.quotes()
    quotes = {}
    for key, (src, needle) in {
            "met_97": (MET, "there were 97 homicides in 2025"),
            "met_rate": (MET, "London’s homicide rate now stands at 1.1 per 100,000 people"),
            "met_cities": (MET, "below many comparable global cities, including New York (2.8), Berlin (3.2), Milan (1.6) "
                                "and Toronto (1.6)"),
            "met_credit": (MET, "London’s record‑low homicide rate is the result of relentless work"),
            "nypd_q3": (NYPD_Q3, "there were 203 murders, beating the previous record of 215 set in 2017"),
    }.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
    met_ny = float(re.search(r"New York \(([\d.]+)\)", quotes["met_cities"]["quote"]).group(1))
    met_tor = float(re.search(r"Toronto \(([\d.]+)\)", quotes["met_cities"]["quote"]).group(1))
    met_ber = float(re.search(r"Berlin \(([\d.]+)\)", quotes["met_cities"]["quote"]).group(1))
    met_mil = float(re.search(r"Milan \(([\d.]+)\)", quotes["met_cities"]["quote"]).group(1))
    A.results["history"] = {"met_new_york": met_ny, "met_toronto": met_tor, "met_berlin": met_ber, "met_milan": met_mil, "met_london": 1.1, "met_homicides": 97,
                            "nypd_q3_murders": int(re.search(r"there were (\d+) murders", quotes["nypd_q3"]["quote"]).group(1)),
                            "nypd_q3_record": int(re.search(r"record of (\d+) set in 2017", quotes["nypd_q3"]["quote"]).group(1))}
    assert float(re.search(r"stands at ([\d.]+) per", quotes["met_rate"]["quote"]).group(1)) == A.results["history"]["met_london"]
    A.results["met_check"] = {"nyc_rate": r(ny, 1), "gap": r(ny - met_ny, 1), "toronto_city": r(rates["toronto"][LAST], 1),
                              "toronto_cma": r(cma.loc[LAST, "rate"], 1),
                              "nyc_lowest_since_1990": r(nyr.loc[:LAST].min(), 1)}
    # the page: the Met's New York figure is below New York's rate in every year since 1990, on the NYPD's count
    assert nyr.loc[:LAST].min() > met_ny, (nyr.loc[:LAST].min(), met_ny)

    # ---- the reproduction: each city's published figure from the files
    cs1990 = float(cs["history"]["Murder"][1990])
    A.results["long"]["nyc_compstat_1990"] = int(cs1990)
    A.results["long"]["nyc_fall_pct_compstat_1990"] = r(100 * (1 - nyr[LAST] / (1e5 * cs1990 / pops["new_york"][1990])), 0)
    A.reproduce("London homicides, 2025 (the Met's release)", published=97, published_moe90=None,
                reproduced=int(ct["london"][LAST]), kind="count", tolerance=0)
    A.reproduce("New York murders, 2025 (CompStat's complete-year table)", published=int(cs["history"]["Murder"][2025]),
                published_moe90=None, reproduced=int(ct["new_york"][LAST]), kind="count", tolerance=0)
    A.reproduce("Toronto metropolitan area, homicides per 100,000, 2025 (Statistics Canada)",
                published=float(cma.loc[LAST, "rate"]), published_moe90=None,
                reproduced=r(1e5 * cma.loc[LAST, "victims"] / P.population("toronto_cma").set_index("year").loc[LAST, "pop"], 2),
                kind="rate", tolerance=0.05)
    A.results["reproduction_source"] = "The Met's release of 12 January 2026; the NYPD's CompStat report; Statistics Canada, Table 35-10-0071"
    A.results["reproduction_note"] = ("Counts must match exactly; Statistics Canada's rate is on its own population "
                                      "estimate, so ours, on the July 2025 estimate, may differ by a few hundredths.")

    # ---- printed figures
    y = A.results["y2025"]
    for k, _ in CITIES:
        A.printed(f"y2025.{k}.rate", f"{y[k]['rate']:.1f}")
    for k in ("new_york", "london", "tokyo"):
        A.printed(f"y2025.{k}.count", f"{y[k]['count']:,}")
    A.results["per_base"] = 100000          # the rates' base, as the summary prints it
    A.printed("per_base", "100,000")
    for k in ("london", "paris_inner", "toronto", "tokyo"):
        A.printed(f"times_2025.{k}", f"{A.results['times_2025'][k]:.1f}")
    L = A.results["long"]
    A.printed("long.nyc_1990.rate", f"{L['nyc_1990']['rate']:.1f}")
    A.printed("long.nyc_1990.count", f"{L['nyc_1990']['count']:,}")
    A.printed("long.nyc_fall_pct", f"{L['nyc_fall_pct']:.0f}%")
    A.printed("long.nyc_low.rate", f"{L['nyc_low']['rate']:.1f}")
    A.printed("long.nyc_2019", f"{L['nyc_2019']:.1f}")
    A.printed("long.nyc_2021", f"{L['nyc_2021']:.1f}")
    A.printed("long.toronto_cma_1990", f"{L['toronto_cma_1990']:.1f}")
    A.printed("long.toronto_cma_2025", f"{L['toronto_cma_2025']:.1f}")
    A.printed("long.london_2003", f"{L['london_2003']:.1f}")
    A.printed("long.london_2019", f"{L['london_2019']:.1f}")
    A.printed("long.nyc_2003", f"{L['nyc_2003']:.1f}")
    R = A.results["ratio"]
    A.printed("ratio.min_2017_2019", f"{R['min_2017_2019']:.1f}")
    A.printed("ratio.max_2017_2019", f"{R['max_2017_2019']:.1f}")
    A.printed("ratio.y2025", f"{R['y2025']:.1f}")
    A.printed("ratio.y2003", f"{R['y2003']:.1f}")
    A.printed("ratio.peak", f"{R['peak']:.1f}")
    A.printed("ratio.y2022", f"{R['y2022']:.1f}")
    G1, G2 = A.results["gap_vs_2017_2019"]["rechecked"], A.results["gap_vs_2022"]["rechecked"]
    A.printed("gap_vs_2017_2019.rechecked.change_pct", f"{G1['change_pct']:.0f}%")
    A.printed("gap_vs_2017_2019.base_multiple", f"{A.results['gap_vs_2017_2019']['base_multiple']:.1f}")
    A.printed("gap_vs_2017_2019.rechecked.lo_pct", f"{G1['lo_pct']:.0f}%")
    A.printed("gap_vs_2017_2019.rechecked.hi_pct", f"{G1['hi_pct']:.0f}%")
    A.printed("gap_vs_2022.rechecked.lo_pct", f"{G2['lo_pct']:.0f}%")
    A.printed("gap_vs_2022.rechecked.hi_pct", f"{G2['hi_pct']:.0f}%")
    A.printed("long.nyc_compstat_1990", f"{A.results['long']['nyc_compstat_1990']:,}")
    A.printed("long.nyc_fall_pct_compstat_1990", f"{A.results['long']['nyc_fall_pct_compstat_1990']:.0f}%")
    A.printed("tokyo_revision_sensitivity.p026.times", f"{A.results['tokyo_revision_sensitivity']['p026']['times']:.1f}")
    Rn, Rl, Rt = A.results["robustness"]["changes"]["now_nyc"], A.results["robustness"]["changes"]["now_london"], A.results["robustness"]["changes"]["now_toronto"]
    for nm, node in (("now_nyc", Rn), ("now_london", Rl), ("now_toronto", Rt)):
        A.printed(f"robustness.changes.{nm}.lo_pct", f"{node['lo_pct']:.0f}%")
        A.printed(f"robustness.changes.{nm}.hi_pct", f"{node['hi_pct']:.0f}%")
    A.printed("long.nyc_count_fall_pct", f"{L['nyc_count_fall_pct']:.0f}%")
    A.printed("long.nyc_fall_since_2003_pct", f"{L['nyc_fall_since_2003_pct']:.0f}%")
    A.printed("long.london_fall_since_2003_pct", f"{L['london_fall_since_2003_pct']:.0f}%")
    A.printed("tokyo_2025.assault_deaths", f"{A.results['tokyo_2025']['assault_deaths']}")
    A.printed("tokyo_2025.rate_with_assault", f"{A.results['tokyo_2025']['rate_with_assault']:.2f}")
    A.printed("tokyo_2025.times_with_assault", f"{A.results['tokyo_2025']['times_with_assault']:.1f}")
    A.printed("toronto_2024.count", f"{A.results['toronto_2024']['count']}")
    A.printed("toronto_2024.count_2025", f"{A.results['toronto_2024']['count_2025']}")
    A.printed("toronto_2024.times_nyc", f"{A.results['toronto_2024']['times_nyc']:.1f}")
    A.printed("toronto_2024.cma_2024", f"{A.results['toronto_2024']['cma_2024']:.1f}")
    A.printed("belief_months.months", f"{A.results['belief_months']['months']}")
    A.printed("belief_months.dec2019_london", f"{A.results['belief_months']['dec2019_london']}")
    A.printed("belief_months.dec2019_nyc", f"{A.results['belief_months']['dec2019_nyc']}")
    N = A.results["now_nyc"]
    A.printed("now_nyc.now", f"{N['now']:,}")
    A.printed("now_nyc.before", f"{N['before']:,}")
    A.printed("now_nyc.change_pct", f"{abs(N['change_pct']):.0f}%")
    A.printed("now_nyc.week_end", f"{int(cs['week_end'][8:])} {MONTH_NAMES[int(cs['week_end'][5:7]) - 1]}")
    A.printed("now_london.now", f"{A.results['now_london']['now']}")
    A.printed("now_london.before", f"{A.results['now_london']['before']}")
    A.printed("now_toronto.now", f"{A.results['now_toronto']['now']}")
    A.printed("now_toronto.before", f"{A.results['now_toronto']['before']}")
    T = A.results["now_tokyo_cases"]
    A.printed("now_tokyo_cases.recognised", f"{T['recognised']}")
    A.printed("now_tokyo_cases.recognised_prior", f"{T['recognised_prior']}")
    A.printed("now_tokyo_cases.final_2025", f"{T['final_2025']}")
    A.printed("tokyo_infanticide_cases_2025", f"{A.results['tokyo_infanticide_cases_2025']}")
    A.printed("tokyo_2025.recognised", f"{A.results['tokyo_2025']['recognised']}")
    A.printed("tokyo_2025.murder_dead", f"{A.results['tokyo_2025']['murder_dead']}")
    B = A.results["belief_2018"]
    for k in ("london_feb", "nyc_feb"):
        A.printed(f"belief_2018.{k}", f"{B[k]}")
    A.printed("belief_2018.london_rate", f"{B['london_rate']:.1f}")
    A.printed("belief_2018.nyc_rate", f"{B['nyc_rate']:.1f}")
    A.printed("belief_toronto_2018.toronto", f"{A.results['belief_toronto_2018']['toronto']:.2f}")
    A.printed("belief_toronto_2018.nyc", f"{A.results['belief_toronto_2018']['nyc']:.2f}")
    A.printed("history.met_new_york", f"{met_ny:.1f}")
    A.printed("history.met_toronto", f"{met_tor:.1f}")
    A.printed("history.met_berlin", f"{met_ber:.1f}")
    A.printed("history.met_milan", f"{met_mil:.1f}")
    A.printed("history.met_london", "1.1")
    A.printed("history.met_homicides", "97")
    A.printed("history.nypd_q3_murders", f"{A.results['history']['nypd_q3_murders']}")
    A.printed("history.nypd_q3_record", f"{A.results['history']['nypd_q3_record']}")
    A.printed("paris_city_2025.count", f"{A.results['paris_city_2025']['count']}")
    A.printed("paris_city_2025.rate", f"{A.results['paris_city_2025']['rate']:.1f}")
    A.printed("met_check.nyc_rate", f"{A.results['met_check']['nyc_rate']:.1f}")
    A.printed("met_check.toronto_city", f"{A.results['met_check']['toronto_city']:.1f}")
    A.printed("met_check.toronto_cma", f"{A.results['met_check']['toronto_cma']:.1f}")

    # ---- charts
    first = A.results["long"]["first_year"]
    years = list(range(first, LAST + 1))
    cells = {"": {"new_york": {str(t): r(nyr[t], 2) for t in years},
                  "toronto_cma": {str(t): r(cma.loc[t, "rate"], 2) for t in years},
                  "london": {str(t): r(rates["london"][t], 2) for t in years if t in rates["london"].index}}}
    A.chart("long_view", lines(
        title=f"New York's murder rate fell by nearly nine-tenths from {first} and is still the highest of the three",
        subtitle="Homicides per 100,000 residents, each city's own count, as far back as its records go",
        x=[[str(t), str(t)] for t in years],
        series=[["new_york", "New York City", "New York"], ["toronto_cma", "Toronto metropolitan area", "Toronto area"],
                ["london", "London (Metropolitan Police area, from 2003)", "London"]],
        cells=cells, format="dec1", axis="Homicides per 100,000 residents",
        x_ticks=[str(t) for t in years if t % 5 == 0 or t == LAST],
        note="New York: murder and non-negligent manslaughter (the State's count to 2000, the NYPD's from 2001). "
             "Toronto: the metropolitan area's victims by the year reported (Statistics Canada's rate). London: the "
             "Metropolitan Police's homicide victims by the month recorded; no consistent series before 2003 is published. "
             "Paris's and Tokyo's comparable counts start in 2016.",
        provenance="NY State DCJS; NYPD; Statistics Canada; Metropolitan Police; Census Bureau, ONS population estimates"))

    rr = sorted(ratio.index)
    A.chart("ratio_london", lines(
        title="New York's rate was about twice London's in 2017 to 2019, and is more than three times it now",
        subtitle="New York's homicides per resident as a multiple of London's",
        x=[[str(t), str(t)] for t in rr],
        series=[["ratio", "New York's rate divided by London's", "Multiple"]],
        cells={"": {"ratio": {str(t): r(ratio[t], 2) for t in rr}}}, format="dec1", axis="Times London's rate",
        x_ticks=[str(t) for t in rr if t % 5 == 0 or t in (2003, LAST)],
        note="Each city's own count on its own population: New York's murders and London's homicide victims (which also "
             "count manslaughter by gross negligence). London's series starts in 2003.",
        provenance="NYPD; Metropolitan Police; Census Bureau and ONS population estimates"))

    A.chart("five_cities", strip(
        title="New York's rate was the highest of the five in 2016 and in 2025",
        subtitle="Homicides per 100,000 residents, each city's own count",
        x=[["2016", "2016"], [str(LAST), str(LAST)]],
        series=[[k, label, label] for k, label in CITIES],
        cells={"": {k: {"2016": A.results["y2016"][k], str(LAST): A.results["y2025"][k]["rate"]} for k, _ in CITIES}},
        format="dec1", axis="Homicides per 100,000 residents",
        note="New York: murder and non-negligent manslaughter. London: homicide victims (Metropolitan Police area). Paris "
             "and inner ring: homicide victims, including assault resulting in death, by place (départements 75, 92, 93, "
             "94). Toronto: the city's deceased victims by date. Tokyo: victims who died (murder, infanticide, robbery-"
             "murder); its recognised murder cases, which include attempts, run about three times higher.",
        provenance="NYPD; Metropolitan Police; SSMSI; Toronto Police Service; Tokyo Metropolitan Police Department"))

    rows_out = []
    for k, label in CITIES:
        for t, v in ct[k].items():
            if t in pops[k].index:
                rows_out.append({"city": label, "year": int(t), "homicides": int(v), "population": int(pops[k][t]),
                                 "per_100k": r(1e5 * v / pops[k][t], 3)})
    for t, row in cma.iterrows():
        rows_out.append({"city": "Toronto metropolitan area (Statistics Canada)", "year": int(t), "homicides": int(row["victims"]),
                         "population": None, "per_100k": float(row["rate"])})
    A.write(data=pd.DataFrame(rows_out))
    print(f"{SLUG}: wrote {A.out}")
    return 0


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