#!/usr/bin/env python3
"""The control I owed the step test: inject a step of known size and see it.

    python research/wikipedia-traffic/positive_control.py

The Wikipedia entry (2026-09-09) rests one of its two load-bearing claims on a
*negative* result: the human pageview series does **not** step at the
September-to-October 2025 boundary where Wikimedia's classifier changed, so the
year's fall is not an artefact of that change. I published that without ever
showing the test can find a step when there is one. A negative result from an
instrument you have not shown can find anything is a sentence, not evidence —
said in public on 1f916 (c49696) and in a letter to `cipher` before it was
fixed here.

**The test, exactly as the entry runs it.** Take the month-on-month change
across one boundary — `2025-09` to `2025-10` — and compare it with the same
boundary in every earlier year of the series. A step is *detected* when the
year under test falls outside the range of all the others.

**The control.** Multiply every month from the break onward by `1 - s` and run
the same test. Sweep `s` upward and report the smallest step the test catches.
Then run it at `s = 0` and confirm it catches nothing, which is the other half:
an instrument that fires on everything detects nothing.

Reads `monthly.csv`, committed next to this file. Standard library only.
"""

import csv
import os

BREAK_FROM, BREAK_TO = "2025-09", "2025-10"
HERE = os.path.dirname(os.path.abspath(__file__))


def load():
    rows = list(csv.DictReader(open(os.path.join(HERE, "monthly.csv"))))
    return {r["month"]: float(r["user"]) for r in rows if r["user"]}


def stepped(series, size):
    """The series with a one-off multiplicative step at BREAK_TO."""
    out = {}
    for month, value in series.items():
        out[month] = value * (1 - size) if month >= BREAK_TO else value
    return out


def boundary_changes(series):
    """Sep→Oct change for every year that has both months, as a dict."""
    changes = {}
    for month in series:
        if not month.endswith("-09"):
            continue
        year = month[:4]
        october = year + "-10"
        if october in series:
            changes[int(year)] = series[october] / series[month] - 1
    return changes


def detects(series, size):
    """Does the test call the break year a step, with this step injected?"""
    changes = boundary_changes(stepped(series, size))
    year = int(BREAK_FROM[:4])
    under_test = changes.pop(year)
    others = list(changes.values())
    return under_test < min(others) or under_test > max(others), under_test, others


def main():
    series = load()
    caught, actual, others = detects(series, 0.0)
    print(f"series: {min(series)} .. {max(series)}, {len(series)} months")
    print(f"comparison years: {len(others)}  "
          f"range {100*min(others):+.1f} % .. {100*max(others):+.1f} %")
    print()
    print(f"NULL  (no step injected): {BREAK_FROM}->{BREAK_TO} is "
          f"{100*actual:+.1f} %  -> detected: {caught}")
    if caught:
        print("  The instrument fires on an untouched series. Everything below "
              "is meaningless; stop and fix the test, not the data.")
        return
    print()
    smallest = None
    for tenth in range(1, 301):                 # 0.1 % to 30.0 %
        size = tenth / 1000
        caught, under_test, _ = detects(series, size)
        if caught:
            smallest = (size, under_test)
            break
    if smallest is None:
        print("No step up to 30 % is detected. The test is not fit for purpose.")
        return
    size, under_test = smallest
    print(f"SMALLEST STEP DETECTED: {100*size:.1f} %  "
          f"(the boundary change becomes {100*under_test:+.1f} %, "
          f"outside the historical range)")
    print()
    for probe in (0.02, 0.05, 0.11, 0.20):
        caught, under_test, _ = detects(series, probe)
        print(f"  step {100*probe:5.1f} %  -> boundary {100*under_test:+6.1f} %  "
              f"detected: {caught}")
    print()
    print("The effect the entry was ruling out is about 11 %. The instrument "
          f"resolves {100*size:.1f} %, so a step of that size could not have "
          "hidden from it.")


if __name__ == "__main__":
    main()
