Skip to content

Commit a0f2033

Browse files
test(coverage): expand b1000 Monte Carlo audit (CS, ebalance, DML)
Promotes the Callaway-Sant'Anna staggered ATT, entropy-balancing CIA ATT, and DML IRM ATE coverage rows from the B=200 pytest cap to the full B=1000 materialised audit, alongside refreshed wall-times for the existing rows. Nominal 95% coverage holds (0.946-1.000) across the added estimators. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 618bd83 commit a0f2033

2 files changed

Lines changed: 135 additions & 4 deletions

File tree

tests/coverage_monte_carlo/results_b1000/coverage_b1000.json

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,48 @@
44
"B": 1000,
55
"covered": 952,
66
"rate": 0.952,
7-
"wall_s": 3.2
7+
"wall_s": 2.8
88
},
99
{
1010
"name": "sp.regress 2x2 DiD",
1111
"B": 1000,
1212
"covered": 955,
1313
"rate": 0.955,
14-
"wall_s": 3.6
14+
"wall_s": 2.9
1515
},
1616
{
1717
"name": "sp.ivreg (HC1) on strong-Z IV",
1818
"B": 1000,
1919
"covered": 962,
2020
"rate": 0.962,
21-
"wall_s": 2.6
21+
"wall_s": 2.5
22+
},
23+
{
24+
"name": "sp.callaway_santanna simple ATT (staggered)",
25+
"B": 1000,
26+
"covered": 946,
27+
"rate": 0.946,
28+
"wall_s": 10.9
29+
},
30+
{
31+
"name": "sp.ebalance (CIA, ATT)",
32+
"B": 1000,
33+
"covered": 1000,
34+
"rate": 1.0,
35+
"wall_s": 0.7
36+
},
37+
{
38+
"name": "sp.causal_question(design='dml') IRM ATE",
39+
"B": 1000,
40+
"covered": 969,
41+
"rate": 0.969,
42+
"wall_s": 426.1
43+
},
44+
{
45+
"name": "sp.causal_question(design='causal_forest') AIPW ATE",
46+
"B": 1000,
47+
"covered": 977,
48+
"rate": 0.977,
49+
"wall_s": 1404.3
2250
}
2351
]

tests/coverage_monte_carlo/run_b1000.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,112 @@ def coverage_iv() -> dict:
114114
"covered": covered, "rate": covered / B}
115115

116116

117+
def coverage_cs() -> dict:
118+
"""Callaway--Sant'Anna simple ATT on a homogeneous staggered DGP.
119+
120+
Same DGP as ``test_cs_staggered_ci_coverage`` (n_units=200, 8 periods,
121+
cohorts {3,5,7,never}); promoted from the B=200 pytest cap to a full
122+
B=1000 materialised audit.
123+
"""
124+
truth = 1.5
125+
covered = 0
126+
cohorts = [3, 5, 7, 0]
127+
for seed in range(B):
128+
rng = np.random.default_rng(seed)
129+
n_units = 200
130+
rows = []
131+
for i in range(n_units):
132+
g = cohorts[i % 4]
133+
ui = rng.normal(scale=0.5)
134+
for t in range(1, 9):
135+
post = 1 if (g > 0 and t >= g) else 0
136+
y = 0.2 * t + truth * post + ui + rng.normal(scale=0.8)
137+
rows.append({"i": i, "t": t, "g": g, "y": y})
138+
df = pd.DataFrame(rows)
139+
r = sp.callaway_santanna(df, y="y", g="g", t="t", i="i",
140+
estimator="reg")
141+
if r.ci[0] <= truth <= r.ci[1]:
142+
covered += 1
143+
return {"name": "sp.callaway_santanna simple ATT (staggered)", "B": B,
144+
"covered": covered, "rate": covered / B}
145+
146+
147+
def coverage_ebalance() -> dict:
148+
"""Entropy balancing on a CIA DGP (same DGP as the pytest row)."""
149+
truth = 2.0
150+
covered = 0
151+
for seed in range(B):
152+
rng = np.random.default_rng(seed)
153+
n = 500
154+
X1 = rng.normal(size=n)
155+
X2 = rng.normal(size=n)
156+
p = 1 / (1 + np.exp(-(-0.3 + 0.5 * X1 - 0.3 * X2)))
157+
d = (rng.uniform(0, 1, n) < p).astype(int)
158+
y = 1.0 + 1.5 * X1 - 0.8 * X2 + truth * d + rng.normal(scale=0.8, size=n)
159+
df = pd.DataFrame({"y": y, "d": d, "X1": X1, "X2": X2})
160+
r = sp.ebalance(df, y="y", treat="d", covariates=["X1", "X2"])
161+
if r.ci[0] <= truth <= r.ci[1]:
162+
covered += 1
163+
return {"name": "sp.ebalance (CIA, ATT)", "B": B,
164+
"covered": covered, "rate": covered / B}
165+
166+
167+
def coverage_dml() -> dict:
168+
"""DML IRM ATE via ``sp.causal_question(design='dml')`` (binary D)."""
169+
truth = 1.0
170+
covered = 0
171+
for seed in range(B):
172+
rng = np.random.default_rng(seed)
173+
n = 500
174+
x1 = rng.normal(size=n)
175+
x2 = rng.normal(size=n)
176+
p = 1 / (1 + np.exp(-(0.4 * x1 - 0.2 * x2)))
177+
d = rng.binomial(1, p)
178+
y = 0.5 + truth * d + 0.6 * x1 + 0.3 * x2 + rng.normal(size=n)
179+
df = pd.DataFrame({"y": y, "d": d, "x1": x1, "x2": x2})
180+
q = sp.causal_question(treatment="d", outcome="y", design="dml",
181+
covariates=["x1", "x2"], data=df)
182+
r = q.estimate()
183+
if r.ci[0] <= truth <= r.ci[1]:
184+
covered += 1
185+
return {"name": "sp.causal_question(design='dml') IRM ATE", "B": B,
186+
"covered": covered, "rate": covered / B}
187+
188+
189+
def coverage_causal_forest() -> dict:
190+
"""Causal-forest population ATE via cross-fit AIPW-IF (binary D).
191+
192+
Same DGP as ``test_causal_forest_aipw_ci_coverage``; the ATE summary
193+
is the doubly-robust AIPW influence-function mean -- the same
194+
estimator grf::average_treatment_effect reports.
195+
"""
196+
truth = 1.0
197+
covered = 0
198+
for seed in range(B):
199+
rng = np.random.default_rng(seed)
200+
n = 500
201+
x1 = rng.normal(size=n)
202+
x2 = rng.normal(size=n)
203+
p = 1 / (1 + np.exp(-(0.5 * x1)))
204+
d = rng.binomial(1, p)
205+
y = 0.5 + truth * d + 0.7 * x1 + 0.3 * x2 + rng.normal(size=n)
206+
df = pd.DataFrame({"y": y, "d": d, "x1": x1, "x2": x2})
207+
q = sp.causal_question(treatment="d", outcome="y",
208+
design="causal_forest",
209+
covariates=["x1", "x2"], data=df)
210+
r = q.estimate(n_estimators=30, random_state=seed)
211+
if r.ci[0] <= truth <= r.ci[1]:
212+
covered += 1
213+
return {"name": "sp.causal_question(design='causal_forest') AIPW ATE",
214+
"B": B, "covered": covered, "rate": covered / B}
215+
216+
117217
def main() -> None:
118218
out: list[dict] = []
119-
for fn in [coverage_ols, coverage_did_2x2, coverage_iv]:
219+
fns = [coverage_ols, coverage_did_2x2, coverage_iv,
220+
coverage_cs, coverage_ebalance, coverage_dml,
221+
coverage_causal_forest]
222+
for fn in fns:
120223
t0 = time.time()
121224
rec = fn()
122225
rec["wall_s"] = round(time.time() - t0, 1)

0 commit comments

Comments
 (0)