#!/usr/bin/env python3 """Render SM-020 from summary.json. python3 make_page.py summary.json OUT.html""" import json, sys, os, html, datetime as dt S = json.load(open(sys.argv[1])); OUT = sys.argv[2] _lv = next((sys.argv[i + 1] for i, x in enumerate(sys.argv) if x == "--live"), None) LV = json.load(open(_lv)) if _lv else None # live.py output for the newest quarter; absent = no live lead _bt = next((sys.argv[i + 1] for i, x in enumerate(sys.argv) if x == "--backtest"), None) BT = json.load(open(_bt)) if _bt else None # backtest.py output: what became of the previous quarter's missing pairs BT_DUE = "20 July 2026" HERE = os.path.dirname(os.path.abspath(__file__)); P = os.path.join(HERE, "page_parts") esc = html.escape n = lambda x: f"{x:,}" if isinstance(x, int) else str(x) pct = lambda a, b: f"{100 * a / b:.1f}%" Q, A, Y = S["quarters"], S["all"], S["y2025"] qs = list(Q) def nice(name): import re as _re keep = {"LLC", "LLP", "P.C.", "PLLC", "LP", "USA", "II", "III", "&"} def w1(w): if w in keep or (len(w) <= 2 and w.isalpha() and w not in {"AS", "OF", "OR", "AT", "IN", "ON", "TO", "BY", "AN", "A"}): return w pre = "(" if w.startswith("(") else ""; w = w.lstrip("(") return pre + _re.sub(r"(?<=')([a-z])", lambda m: m.group(1).upper(), w.capitalize()) out = " ".join(w1(w) for w in name.split()) return _re.sub(r"\bMc([a-z])", lambda m: "Mc" + m.group(1).upper(), out) def qlabel(q): return {"1": "Jan–Mar", "2": "Apr–Jun", "3": "Jul–Sep", "4": "Oct–Dec"}[q[-1]] + " " + q[:4] def dlabel(d): return dt.date.fromisoformat(d).strftime("%-d %b %Y") fetched = dt.date.fromisoformat(S["fetched_at"][:10]) _dep = os.path.expanduser("~/second-measurements/zenodo/sm-020/deposit.json") DOI = json.load(open(_dep))["metadata"]["prereserve_doi"]["doi"] if os.path.exists(_dep) else "pending" SIG = json.load(open(os.path.join(HERE, "exhibits", "signature_vs_posted_sample.json")))["summary"] TW = S["third_week"] D1 = sum(1 for v in TW["by_quarter"].values() if v["busiest_day"] == 1) PRMIN, PRMAX = min(v["peak_registrants"] for v in TW["by_quarter"].values()), max(v["peak_registrants"] for v in TW["by_quarter"].values()) ALL_LATE = [f for f in S["top_firms"] if f.get("total") and f["late"] == f["total"]] def _noact(f): return sum(v for k, v in f.get("counts", {}).items() if k.endswith("Y")) NOACT = "; ".join(f"{esc(nice(f['registrant']))}: {'all' if _noact(f) == f['total'] else n(_noact(f))} of {n(f['total'])}" for f in ALL_LATE if _noact(f) * 2 > f["total"]) NOACT = ("Some of these reports say there was no lobbying in the quarter, which still has to be reported by the deadline2: " + NOACT + ".") if NOACT else "" DUP = S["duplicates"] _res = json.load(open(os.path.join(HERE, "results_2026-10-15.json"))) TREAD = _res.get("merged", [{}])[-1].get("fetched_at", "")[11:16] _fr = sorted(f["read_at"] for f in S["top_firms"] if f.get("read_at")) FREAD = f"{_fr[0][11:16]} to {_fr[-1][11:16]}" if _fr else "" title = (f"A registered lobbyist must file each quarter’s report within 20 days. Of {n(Y['total'])} reports for 2025, " f"{n(Y['late'])} went up late, {n(Y['over_30'])} more than a month late, and the fine for a knowing violation is up to $200,000") desc = (f"Every lobbying firm and every organisation with in-house lobbyists files a quarterly report (LD-2) with the Senate and the House, " f"due 20 days after the quarter ends. The public database stamps each report with the time it was posted; on every report we checked, that was the time it was signed. " f"We counted every quarterly and termination report for January 2025 to June 2026 and read the stamp on every late one: {n(A['late'])} of {n(A['total'])} ({A['late_pct']}%) were late. " f"For 2025, {n(Y['late'])} of {n(Y['total'])} were late, by a median of {Y['median_days']:g} days.") rows = "" for q in reversed(qs): r = Q[q] rows += (f"
{desc} Who lobbies for whom is public from the day a lobbyist registers; until the quarterly report is up, what that quarter’s lobbying covered is not: the issues, the agencies, the lobbyists and the money. The law sets a fine of up to $200,000 for a knowing violation.
Disclosure: Markovian Protocol holds no financial position in any organisation named here, was paid by no one for this work, and showed it to no one before publication. How we work.
| Quarter | Due | Reports | Late | Share | Over 30 days | Regular, late | No activity, late | Termination, late |
|---|
Every LD-2 for the quarter: regular reports (Qn), reports of no activity (QnY), and termination reports (nT, nTY), which carry the same deadline; amendments are not counted. Late = posted on or after the day after the deadline, Eastern time. Deadlines from 2 U.S.C. 1604(a), matching the Senate and House filing calendars.4 Read from lda.gov on {S['fetched_at'][:10]}: regular and no-activity reports from {S['fetched_at'][11:16]} UTC, termination reports from {TREAD} UTC, the firm totals {FREAD} UTC.
The Center for Public Integrity counted late filing in 2005, under the old law, when reports were due every six months and the Senate’s database recorded only the day a form was entered: of “some 183,000” forms filed with the Senate since 1998, “36,000 of them (more than 19 percent) were late, 8,800 by more than three months.” The same article says “the Center counted all forms entered within 90 days of the deadline as ‘on time’”, so its rule and ours differ and the two shares do not compare. The nearest figure we have: {n(B['over_90'])} of our {n(A['total'])} reports ({pct(B['over_90'], A['total'])}) went up more than 90 days after the deadline.5
| Days past the deadline | Reports |
|---|
Only {S['midnight_edge']['posted_00_00_to_00_05']} late reports went up in the first five minutes after midnight, so the line between the deadline day and the next does not decide the count.
| Quarter | Posted | Days after deadline | Late reports that day | Registrants |
|---|
The days on which the most late reports were posted.
| Registrant | Late reports | Clients, late | Quarters, late | Median days late | Longest |
|---|
A lobbying firm files one report per client per quarter, so a firm that is late for a quarter is late many times. “Of” counts every report the registrant filed for the six quarters, all four types; the per-type counts are in the summary exhibit.
The firms close it by filing on time; the table above names the ones with the most to file. The Secretary of the Senate and the Clerk of the House write to registrants who do not file and refer those who do not answer within 60 days to the U.S. Attorney for the District of Columbia, which enforces the Act.2 GAO counts 3,566 such referrals for failure to file quarterly reports in 2015–2024: about 36% closed as in compliance, meaning the report was eventually filed, about 63% still pending; and one civil settlement in 2024, a $65,000 penalty and an agreement to “permanently retire from lobbying at the federal level”.3 A report filed late is not a report never filed, and nothing public says whether lateness alone is ever referred. We re-read every deadline the day after.
What we told whom, and when.
| 15 Oct 2026 | Measured. Nobody has seen the page. |
| 21 Oct 2026 | Live read of the Jul–Sep 2026 deadline, " + LV["read_started"][11:16] + "–" + LV["read_finished"][11:16] + " UTC. |
python3 lda_census.py results_2026-10-15.json 2025Q1 2025Q2 2025Q3 2025Q4 2026Q1 2026Q2 python3 lda_registrations.py 2024-12-01 registrations_2026-10-15.json python3 analyze.py results_2026-10-15.json registrations_2026-10-15.json summary_2026-10-15.json --firm-totals python3 sig_check.py results_2026-10-15.json 40 20261015 signature_vs_posted_sample.json # back-test of the live read on the previous quarter python3 lda_pairs.py 2026Q1 Q,QY pairs_2026Q1.json --posted-before 2026-07-21; python3 lda_pairs.py 2026Q1 T,TY pairs_2026Q1_term.json --posted-before 2026-07-21 python3 lda_pairs.py 2026Q2 Q,QY pairs_2026Q2.json --posted-before 2026-10-21; python3 lda_pairs.py 2026Q2 T,TY pairs_2026Q2_term.json --posted-before 2026-10-21; python3 lda_pairs.py 2026Q2 A,AY pairs_2026Q2_amend.json --posted-before 2026-10-21 python3 backtest.py pairs_2026Q1.json 2026-07-21 backtest_2026Q2.json pairs_2026Q2.json pairs_2026Q2_term.json pairs_2026Q2_amend.json --terminated pairs_2026Q1_term.json # the live read, after the July-September deadline (or recompute it from the exhibits with the second line alone) python3 lda_pairs.py 2026Q3 Q,QY,T,TY,A,AY pairs_2026Q3.json --posted-before 2026-10-21 python3 live.py pairs_2026Q2.json pairs_2026Q3.json live_2026Q3.json --terminated pairs_2026Q2_term.json
The exhibits are gzipped; gunzip -k them first. Standard library only; the API allows anonymous reads but rate-limits them, so a full run takes about an hour. Scripts and exhibits: sm-020/.
@misc{{markovian-sm020,
author = {{{{Markovian Protocol}}}},
title = {{Time from the 2 U.S.C. 1604(a) deadline to posting, every quarterly and termination
lobbying report (LD-2) for January 2025 to June 2026}},
number = {{SM-020}},
year = {{2026}},
month = oct,
url = {{https://markovianprotocol.com/measurements/sm-020.html}},
doi = {{{DOI}}}
}}
"""
open(OUT, "w").write(head + "" + top.split("", 1)[-1] + body + "\n" + foot)
print("wrote", OUT, os.path.getsize(OUT))