@@ -350,6 +350,39 @@ def test_covariates_with_survey(self, sdid_survey_data, survey_design_weights):
350350 assert np .isfinite (result .att )
351351 assert result .survey_metadata is not None
352352
353+ def test_effective_weights_returned (self , sdid_survey_data , survey_design_weights ):
354+ """unit_weights returns composed ω_eff (not raw ω) under survey weighting."""
355+ est = SyntheticDiD (variance_method = "placebo" , n_bootstrap = 50 , seed = 42 )
356+ result = est .fit (
357+ sdid_survey_data ,
358+ outcome = "outcome" ,
359+ treatment = "treated" ,
360+ unit = "unit" ,
361+ time = "time" ,
362+ post_periods = [6 , 7 , 8 , 9 ],
363+ survey_design = survey_design_weights ,
364+ )
365+ weights = result .unit_weights
366+ # Effective weights should sum to 1 (renormalized)
367+ assert sum (weights .values ()) == pytest .approx (1.0 , abs = 1e-10 )
368+ # With non-uniform survey weights, effective weights should differ
369+ # from what uniform survey weights would produce
370+ sdid_survey_data_u = sdid_survey_data .copy ()
371+ sdid_survey_data_u ["uniform_w" ] = 1.0
372+ result_u = est .fit (
373+ sdid_survey_data_u ,
374+ outcome = "outcome" ,
375+ treatment = "treated" ,
376+ unit = "unit" ,
377+ time = "time" ,
378+ post_periods = [6 , 7 , 8 , 9 ],
379+ survey_design = SurveyDesign (weights = "uniform_w" ),
380+ )
381+ # Non-uniform weights should change the returned weight distribution
382+ eff_vals = sorted (weights .values (), reverse = True )
383+ uni_vals = sorted (result_u .unit_weights .values (), reverse = True )
384+ assert eff_vals != pytest .approx (uni_vals , abs = 1e-6 )
385+
353386
354387# =============================================================================
355388# TROP Survey Tests
@@ -577,3 +610,41 @@ def test_to_dict_includes_survey(self, trop_survey_data, survey_design_weights):
577610 d = result .to_dict ()
578611 assert "weight_type" in d
579612 assert d ["weight_type" ] == "pweight"
613+
614+ def test_local_bootstrap_nan_treated_outcomes (self , trop_survey_data ):
615+ """Bootstrap handles NaN treated outcomes without poisoning SE."""
616+ trop_survey_data = trop_survey_data .copy ()
617+ # Set some treated post-treatment outcomes to NaN
618+ mask = (trop_survey_data ["D" ] == 1 ) & (trop_survey_data ["time" ] == 7 )
619+ trop_survey_data .loc [mask , "outcome" ] = np .nan
620+
621+ est = TROP (method = "local" , n_bootstrap = 10 , seed = 42 , max_iter = 5 )
622+ result = est .fit (
623+ trop_survey_data ,
624+ outcome = "outcome" ,
625+ treatment = "D" ,
626+ unit = "unit" ,
627+ time = "time" ,
628+ )
629+ # Point estimate should use finite cells only
630+ assert np .isfinite (result .att )
631+ # SE should remain finite (not poisoned by NaN)
632+ assert np .isfinite (result .se )
633+
634+ def test_local_bootstrap_nan_with_survey (self , trop_survey_data , survey_design_weights ):
635+ """Bootstrap + survey handles NaN treated outcomes correctly."""
636+ trop_survey_data = trop_survey_data .copy ()
637+ mask = (trop_survey_data ["D" ] == 1 ) & (trop_survey_data ["time" ] == 8 )
638+ trop_survey_data .loc [mask , "outcome" ] = np .nan
639+
640+ est = TROP (method = "local" , n_bootstrap = 10 , seed = 42 , max_iter = 5 )
641+ result = est .fit (
642+ trop_survey_data ,
643+ outcome = "outcome" ,
644+ treatment = "D" ,
645+ unit = "unit" ,
646+ time = "time" ,
647+ survey_design = survey_design_weights ,
648+ )
649+ assert np .isfinite (result .att )
650+ assert np .isfinite (result .se )
0 commit comments