pfs.event_study: pre/post windows around an exposure year (the effective date is 1 January, so the exposure year is the first treated year), log ratio of window means, difference in log changes against the never-treated control set from pfs.exposure.control_codes (same RVU status and decile band, no exposure within ±2 years), 95% percentile-bootstrap interval over the controls with a fixed seed. A code with no pre-period of its own (a new code or a successor) is measured through its family's summed series (scope="family"), so G2058 → 99439 is one continuous line. pre_post() gives single-series window means for outcomes without a code-level control set (occupation wages, employment). utilization_panel() ties an exposure row, its pfs.utilization series, its controls and the estimate. notebooks/event_study.py: pick one of nine fixture exposures, see the treated services series with the exposure rule, the indexed control band (10th–90th pct), the contrast table with its interval and FR anchor, clinical-staff wages and health-care employment around the same year, and a table of every fixture contrast with anchors. A 2025 exposure has no post-period until the 2025 PUF ships — stated, never filled. Descriptive contrasts only; the notebook says so and the chat only links here. Market/industry series are deferred: no keyless quote source is reachable from the rack.
73 lines
2.4 KiB
Python
73 lines
2.4 KiB
Python
"""notebooks/event_study.py — structure, headless degradation, live fixture panels (#696)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import importlib.util
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
NB = Path(__file__).resolve().parents[2] / "notebooks" / "event_study.py"
|
|
_ROOT = Path(__file__).resolve().parents[2]
|
|
_HAS_DATA = (_ROOT / "data" / "replica" / "aco.ro.duckdb").exists() and (
|
|
_ROOT / "data" / "bib.sqlite"
|
|
).exists()
|
|
_live = pytest.mark.skipif(not _HAS_DATA, reason="needs the live replica and bib")
|
|
|
|
|
|
def _load():
|
|
spec = importlib.util.spec_from_file_location("event_study_nb", NB)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
def test_notebook_is_a_marimo_app():
|
|
assert _load().app.__class__.__name__ == "App"
|
|
|
|
|
|
def test_cells_are_anonymous_and_banners_present():
|
|
src = NB.read_text()
|
|
names = [n.name for n in ast.parse(src).body if isinstance(n, ast.FunctionDef)]
|
|
assert names and set(names) == {"_"}
|
|
assert re.findall(r"# ── (\S+)\. ", src) == ["0", "1", "2", "3", "4", "5"]
|
|
|
|
|
|
def test_no_causal_language():
|
|
src = NB.read_text().lower()
|
|
assert (
|
|
"caused" not in src and "causal estimate" in src
|
|
) # the disclaimer, nothing else
|
|
|
|
|
|
def test_headless_run_degrades_without_data(monkeypatch):
|
|
monkeypatch.setenv("STACK_DUCKDB_REPLICA", "1")
|
|
mod = _load()
|
|
import conf.connect as cc
|
|
|
|
monkeypatch.setattr(
|
|
cc,
|
|
"duckdb",
|
|
lambda *a, **k: (_ for _ in ()).throw(FileNotFoundError("no replica")),
|
|
)
|
|
monkeypatch.setattr(
|
|
cc, "bib", lambda *a, **k: (_ for _ in ()).throw(FileNotFoundError("no bib"))
|
|
)
|
|
outputs, _ = mod.app.run()
|
|
rendered = "\n".join(o._repr_html_() for o in outputs if hasattr(o, "_repr_html_"))
|
|
assert "Not built yet" in rendered and "Traceback" not in rendered
|
|
|
|
|
|
@_live
|
|
def test_ccm_2015_and_apcm_2025_panels_render_with_anchors():
|
|
mod = _load()
|
|
outputs, _ = mod.app.run()
|
|
rendered = "\n".join(o._repr_html_() for o in outputs if hasattr(o, "_repr_html_"))
|
|
assert "Traceback" not in rendered
|
|
# section 4 lists every fixture with its anchor: CCM 2015 (DE2VH9PD) and APCM 2025 (JJ6AM5HJ)
|
|
assert "99490 becomes-payable 2015" in rendered and "DE2VH9PD" in rendered
|
|
assert "G0556 becomes-payable 2025" in rendered and "JJ6AM5HJ" in rendered
|
|
assert "no post-period on file yet" in rendered
|