@@ -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+
117217def 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