@@ -1232,8 +1232,8 @@ def test_fpc_nonconstant_within_stratum(self):
12321232 with pytest .raises (ValueError , match = "constant within each stratum" ):
12331233 sd .resolve (df )
12341234
1235- def test_fpc_only_without_psu_raises (self ):
1236- """FPC without psu or strata raises ValueError (P1) ."""
1235+ def test_fpc_only_without_psu_resolves (self ):
1236+ """FPC without psu or strata resolves — validated later against effective PSUs ."""
12371237 n = 10
12381238 df = pd .DataFrame (
12391239 {
@@ -1243,8 +1243,8 @@ def test_fpc_only_without_psu_raises(self):
12431243 }
12441244 )
12451245 sd = SurveyDesign (weights = "w" , fpc = "fpc" )
1246- with pytest . raises ( ValueError , match = "FPC requires either psu or strata" ):
1247- sd . resolve ( df )
1246+ resolved = sd . resolve ( df )
1247+ assert resolved . fpc is not None
12481248
12491249 def test_weighted_within_transform_matches_explicit_wls (self ):
12501250 """Weighted within_transform + OLS matches explicit WLS with dummies (P0-2).
@@ -2901,7 +2901,7 @@ def test_injected_cluster_nested_in_strata(self):
29012901 assert result .df_survey == 2
29022902
29032903 def test_fpc_with_strata_no_psu_accepted (self ):
2904- """FPC + strata (no PSU) is accepted — clusters may be injected later ."""
2904+ """FPC + strata (no PSU) resolves — FPC validated later against effective PSUs ."""
29052905 df = pd .DataFrame (
29062906 {
29072907 "y" : [1.0 , 2.0 , 3.0 , 4.0 , 5.0 , 6.0 ],
@@ -2913,6 +2913,46 @@ def test_fpc_with_strata_no_psu_accepted(self):
29132913 sd = SurveyDesign (
29142914 weights = "w" , weight_type = "pweight" , strata = "strat" , fpc = "pop"
29152915 )
2916- # Should not raise — FPC validation defers when no PSU declared
2916+ # Should not raise at resolve time — FPC >= n_PSU validated at vcov time
29172917 resolved = sd .resolve (df )
29182918 assert resolved .fpc is not None
2919+
2920+ def test_fpc_alone_no_strata_no_psu_accepted (self ):
2921+ """FPC alone (no PSU/strata) resolves — clusters may be injected later."""
2922+ df = pd .DataFrame (
2923+ {
2924+ "y" : [1.0 , 2.0 , 3.0 , 4.0 ],
2925+ "w" : [1.0 , 1.0 , 1.0 , 1.0 ],
2926+ "pop" : [100.0 , 100.0 , 100.0 , 100.0 ],
2927+ }
2928+ )
2929+ sd = SurveyDesign (weights = "w" , weight_type = "pweight" , fpc = "pop" )
2930+ resolved = sd .resolve (df )
2931+ assert resolved .fpc is not None
2932+
2933+ def test_fpc_lt_effective_npsu_rejected_at_vcov (self ):
2934+ """FPC < effective n_PSU is rejected at compute_survey_vcov time."""
2935+ np .random .seed (42 )
2936+ n = 12
2937+ strata = np .repeat ([0 , 1 ], 6 )
2938+ psu = np .tile (np .arange (3 ), 4 ) # 3 PSUs per stratum
2939+
2940+ X = np .column_stack ([np .ones (n ), np .random .randn (n )])
2941+ residuals = np .random .randn (n )
2942+ weights = np .ones (n )
2943+
2944+ # FPC = 2 per stratum, but we have 3 PSUs → invalid
2945+ fpc = np .array ([2.0 ] * n )
2946+
2947+ resolved = ResolvedSurveyDesign (
2948+ weights = weights ,
2949+ weight_type = "pweight" ,
2950+ strata = strata ,
2951+ psu = psu ,
2952+ fpc = fpc ,
2953+ n_strata = 2 ,
2954+ n_psu = 6 ,
2955+ lonely_psu = "remove" ,
2956+ )
2957+ with pytest .raises (ValueError , match = "FPC.*less than.*effective PSUs" ):
2958+ compute_survey_vcov (X , residuals , resolved = resolved )
0 commit comments