#!/usr/bin/env python3
"""Reconcile NHS England's "Chest X-ray AI - Studies Successfully Processed by NHS Trust, by Month"
(July 2026), the source the Budget 2026 cites in footnote 66.
python3 -I cxr-throughput-reconciliation.py.txt 20260707---cxr-throughput-statistics---aidf1.xlsx cxr-throughput-reconciliation.json
Standard library only. Reads the single worksheet, then reports: the national monthly totals and running
count; the sum of trust rows and of network subtotals against the national row, month by month; any
network whose subtotal differs from the sum of its trusts in a month; trusts whose Total column differs
from the sum of their monthly columns; and the three Greater Manchester trusts whose notes give a larger
"reconciled cumulative" total. Checks the sheet's own grand total and the ministerial answer to PQ 30882
(781,000 for Jan-Mar 2026), then writes the JSON named as the second argument."""
import hashlib, html, json, re, sys, zipfile
src, dest = sys.argv[1], sys.argv[2]
def colidx(c):
n = 0
for ch in c: n = n * 26 + ord(ch) - 64
return n
z = zipfile.ZipFile(src)
ss = [html.unescape(re.sub(r"<[^>]+>", "", m)) for m in re.findall(r"(.*?)", z.read("xl/sharedStrings.xml").decode(), re.S)]
rows = {}
for r in re.finditer(r']*r="(\d+)"[^>]*>(.*?)
', z.read("xl/worksheets/sheet1.xml").decode(), re.S):
cells = {}
for c in re.finditer(r']*?)(?:/>|>(.*?))', r.group(2), re.S):
col, attrs, inner = c.groups()
v = re.search(r"([^<]*)", inner or "")
if v: cells[col] = ss[int(v.group(1))] if 't="s"' in attrs else float(v.group(1))
rows[int(r.group(1))] = cells
header = next(i for i in sorted(rows) if rows[i].get("A") == "Imaging Network")
grand = next(i for i in rows if any(isinstance(v, str) and "GRAND TOTAL" in v for v in rows[i].values()))
H, G = rows[header], rows[grand]
tot_col = next(c for c in H if H[c] == "Total")
mcols = sorted((c for c in H if colidx(c) >= 4 and c != tot_col), key=colidx)
is_sub = lambda i: "subtotal" in (str(rows[i].get("A", "")) + str(rows[i].get("B", ""))).lower()
body = [i for i in range(header + 1, grand) if rows.get(i)]
sub_rows = [i for i in body if is_sub(i)]
trust_rows = [i for i in body if not is_sub(i) and rows[i].get("B")]
num = lambda i, c: rows[i].get(c, 0) if isinstance(rows[i].get(c, 0), float) else 0
msum = lambda i: sum(num(i, c) for c in mcols)
# Each network's trust rows sit directly above its subtotal row.
networks, pending = [], []
for i in body:
if is_sub(i):
networks.append((rows[i].get("A") or rows[i].get("B"), i, pending)); pending = []
elif i in trust_rows:
pending.append(i)
months, cum = [], 0
for c in mcols:
if isinstance(G.get(c), float):
cum += G[c]
months.append({"month": H[c], "studies": int(G[c]), "cumulative": int(cum)})
total = int(G[tot_col])
by = {m["month"]: m for m in months}
jan_mar_2026 = sum(by[m]["studies"] for m in ("Jan 26", "Feb 26", "Mar 26"))
assert jan_mar_2026 == 781_000, jan_mar_2026 # PQ 30882 answer, 19 Oct 2026
assert total == 4_303_064, total # sheet's own GRAND TOTAL
unassigned_note = next(s for s in ss if s.startswith("Bolton, Tameside and WWL"))
gm = {n: {"listed_total": int(a.replace(",", "")), "reconciled_total_in_notes": int(b.replace(",", ""))}
for n, a, b in re.findall(r"(Bolton|Tameside|WWL) ([\d,]+) of ([\d,]+)", unassigned_note)}
out = {
"source": "NHS England, “Chest X-ray AI – Studies Successfully Processed by NHS Trust, by Month” (July 2026), "
"file 20260707---cxr-throughput-statistics---aidf1.xlsx; retrieved 31 October 2026 from a public mirror of digital.nhs.uk.",
"licence_note": "Derived from NHS England data. Totals only; no patient-level data.",
"sha256": hashlib.sha256(open(src, "rb").read()).hexdigest(),
"metric": next(s for s in ss if s.startswith("Metric:")),
"grand_total": total,
"monthly_sum_to_may_2026": months[-1]["cumulative"],
"not_in_monthly_columns": total - months[-1]["cumulative"],
"first_month_cumulative_over_4m": next(m["month"] for m in months if m["cumulative"] >= 4_000_000),
"jan_mar_2026": jan_mar_2026,
"trust_rows": len(trust_rows),
"trust_rows_total": int(sum(num(i, tot_col) for i in trust_rows)),
"trust_rows_monthly_total": int(sum(msum(i) for i in trust_rows)),
"unexplained_difference_trust_rows_vs_national": total - int(sum(num(i, tot_col) for i in trust_rows)),
"trusts_with_total_not_equal_monthly_sum": {rows[i]["B"]: int(num(i, tot_col) - msum(i))
for i in trust_rows if abs(num(i, tot_col) - msum(i)) > 0.5},
"gm_trusts_listed_vs_reconciled": gm,
"notes_reconciled_minus_listed_totals_3_gm_trusts": sum(v["reconciled_total_in_notes"] - v["listed_total"] for v in gm.values()),
"unassigned_note": unassigned_note,
"network_subtotals": {name: int(num(s, tot_col)) for name, s, _ in networks},
"network_month_mismatches": [
{"network": name, "month": H[c], "subtotal_cell": f"{c}{s}", "subtotal": int(num(s, c)),
"sum_of_trusts": int(sum(num(t, c) for t in ts)), "trust_cells": [f"{c}{t}" for t in ts]}
for name, s, ts in networks for c in mcols if abs(num(s, c) - sum(num(t, c) for t in ts)) > 0.5],
"monthly_reconciliation": [
{"month": H[c], "national_row": int(G.get(c, 0)), "sum_of_trust_rows": int(sum(num(i, c) for i in trust_rows)),
"sum_of_network_subtotals": int(sum(num(i, c) for i in sub_rows))}
for c in mcols if isinstance(G.get(c), float)],
"months": months,
"quarter_end_cumulative_millions": {m["month"]: round(m["cumulative"] / 1e6, 2)
for m in months if m["month"][:3] in ("Mar", "Jun", "Sep", "Dec")},
}
json.dump(out, open(dest, "w"), indent=1, ensure_ascii=False)
print(json.dumps({k: out[k] for k in ("grand_total", "trust_rows_total", "network_month_mismatches",
"notes_reconciled_minus_listed_totals_3_gm_trusts")}, indent=1, ensure_ascii=False))