@@ -940,20 +940,17 @@ def test_multiperiod_with_survey_design(self, multiperiod_data):
940940 # Average ATT should be close to 2.5
941941 assert abs (result .avg_att - 2.5 ) < 1.5
942942
943- def test_fweight_warning_for_fractional (self ):
944- """Fractional fweights emit a UserWarning ."""
943+ def test_fweight_error_for_fractional (self ):
944+ """Fractional fweights raise ValueError ."""
945945 df = pd .DataFrame (
946946 {
947947 "y" : [1 , 2 , 3 ],
948948 "w" : [1.5 , 2.0 , 3.0 ], # 1.5 is fractional
949949 }
950950 )
951951 sd = SurveyDesign (weights = "w" , weight_type = "fweight" )
952- with warnings .catch_warnings (record = True ) as w :
953- warnings .simplefilter ("always" )
952+ with pytest .raises (ValueError , match = "Frequency weights.*must be positive integers" ):
954953 sd .resolve (df )
955- fweight_warnings = [x for x in w if "Frequency weights" in str (x .message )]
956- assert len (fweight_warnings ) >= 1
957954
958955 def test_lonely_psu_remove_warning (self ):
959956 """Singleton stratum with lonely_psu='remove' emits warning."""
@@ -2443,3 +2440,142 @@ def test_multiperiod_fweight_df_rounding(self):
24432440 assert np .isfinite (result .avg_att )
24442441 assert np .isfinite (result .avg_se )
24452442 assert result .avg_se > 0
2443+
2444+
2445+ class TestRound10Fixes :
2446+ """Tests for PR #218 review round 10 fixes."""
2447+
2448+ def test_zero_se_estimator_nan_inference (self ):
2449+ """Zero-SE path in LinearRegression.get_inference() returns NaN, not ±inf."""
2450+ # Build a design where all strata are certainty PSUs → zero vcov → zero SE
2451+ np .random .seed (42 )
2452+ n = 40
2453+ strata = np .repeat ([0 , 1 , 2 , 3 ], 10 )
2454+ psu = strata .copy () # 1 PSU per stratum → all certainty
2455+ df = pd .DataFrame (
2456+ {
2457+ "outcome" : np .random .randn (n ),
2458+ "treated" : np .array ([1 ] * 20 + [0 ] * 20 ),
2459+ "post" : np .tile ([0 , 1 ], 20 ),
2460+ "w" : np .ones (n ),
2461+ "strat" : strata ,
2462+ "cluster" : psu ,
2463+ }
2464+ )
2465+ sd = SurveyDesign (
2466+ weights = "w" ,
2467+ weight_type = "pweight" ,
2468+ strata = "strat" ,
2469+ psu = "cluster" ,
2470+ lonely_psu = "certainty" ,
2471+ )
2472+ did = DifferenceInDifferences ()
2473+ with warnings .catch_warnings ():
2474+ warnings .simplefilter ("ignore" )
2475+ result = did .fit (
2476+ df ,
2477+ outcome = "outcome" ,
2478+ treatment = "treated" ,
2479+ time = "post" ,
2480+ survey_design = sd ,
2481+ )
2482+ # SE should be 0 (all certainty strata), inference should be NaN
2483+ assert result .se == 0.0
2484+ assert np .isnan (result .t_stat )
2485+ assert np .isnan (result .p_value )
2486+ assert np .isnan (result .conf_int [0 ])
2487+ assert np .isnan (result .conf_int [1 ])
2488+
2489+ def test_full_census_fpc_stratified_zero_vcov (self ):
2490+ """Full-census FPC (f_h=1) returns zero vcov, not NaN."""
2491+ np .random .seed (42 )
2492+ n = 60
2493+ strata = np .repeat ([0 , 1 , 2 ], 20 )
2494+ psu = np .tile (np .arange (5 ), 12 ) # 5 PSUs per stratum
2495+
2496+ X = np .column_stack ([np .ones (n ), np .random .randn (n )])
2497+ y = np .random .randn (n )
2498+ residuals = np .random .randn (n )
2499+ weights = np .ones (n )
2500+
2501+ # FPC = n_psu per stratum (full census: f_h = 5/5 = 1)
2502+ fpc = np .array ([5.0 ] * n )
2503+
2504+ resolved = ResolvedSurveyDesign (
2505+ weights = weights ,
2506+ weight_type = "pweight" ,
2507+ strata = strata ,
2508+ psu = psu ,
2509+ fpc = fpc ,
2510+ n_strata = 3 ,
2511+ n_psu = 15 ,
2512+ lonely_psu = "remove" ,
2513+ )
2514+ vcov = compute_survey_vcov (X , residuals , resolved = resolved )
2515+ # Full census → zero variance → zero vcov
2516+ np .testing .assert_array_equal (vcov , np .zeros ((2 , 2 )))
2517+
2518+ def test_full_census_fpc_unstratified_zero_vcov (self ):
2519+ """Unstratified full-census FPC returns zero vcov, not NaN."""
2520+ np .random .seed (42 )
2521+ n = 30
2522+ psu = np .repeat (np .arange (6 ), 5 ) # 6 PSUs
2523+
2524+ X = np .column_stack ([np .ones (n ), np .random .randn (n )])
2525+ y = np .random .randn (n )
2526+ residuals = np .random .randn (n )
2527+ weights = np .ones (n )
2528+
2529+ # FPC = n_psu (full census: f_h = 6/6 = 1)
2530+ fpc = np .array ([6.0 ] * n )
2531+
2532+ resolved = ResolvedSurveyDesign (
2533+ weights = weights ,
2534+ weight_type = "pweight" ,
2535+ strata = None ,
2536+ psu = psu ,
2537+ fpc = fpc ,
2538+ n_strata = 0 ,
2539+ n_psu = 6 ,
2540+ lonely_psu = "remove" ,
2541+ )
2542+ vcov = compute_survey_vcov (X , residuals , resolved = resolved )
2543+ # Full census → (1-f_h)=0 → zero meat → zero vcov
2544+ np .testing .assert_array_equal (vcov , np .zeros ((2 , 2 )))
2545+
2546+ def test_absorbed_did_sample_counts (self ):
2547+ """n_treated/n_control reflect raw data, not demeaned values after absorb."""
2548+ np .random .seed (42 )
2549+ n_units = 20
2550+ n_times = 4
2551+ rows = []
2552+ for u in range (n_units ):
2553+ for t in range (n_times ):
2554+ rows .append (
2555+ {
2556+ "unit" : u ,
2557+ "time" : t ,
2558+ "treated" : 1 if u < 8 else 0 ,
2559+ "post" : 1 if t >= 2 else 0 ,
2560+ "outcome" : np .random .randn (),
2561+ "region" : u % 3 ,
2562+ }
2563+ )
2564+ df = pd .DataFrame (rows )
2565+
2566+ did = DifferenceInDifferences ()
2567+ with warnings .catch_warnings ():
2568+ warnings .simplefilter ("ignore" )
2569+ result = did .fit (
2570+ df ,
2571+ outcome = "outcome" ,
2572+ treatment = "treated" ,
2573+ time = "post" ,
2574+ absorb = ["region" ],
2575+ )
2576+
2577+ # Raw counts: 8 treated units * 4 times = 32 treated obs
2578+ raw_treated = int (df ["treated" ].sum ())
2579+ raw_control = len (df ) - raw_treated
2580+ assert result .n_treated == raw_treated
2581+ assert result .n_control == raw_control
0 commit comments