@@ -168,6 +168,86 @@ class TestMultipleEstimationExpansion:
168168 "Y2 ~ X2 | f2" ,
169169 ],
170170 ),
171+ # Multiple dep vars with sw in covariates and csw in fixed effects
172+ (
173+ "sw(Y1, Y2) ~ sw(X1, X2) | csw(f1, f2)" ,
174+ [
175+ "Y1 ~ X1 | f1" ,
176+ "Y1 ~ X1 | f1 + f2" ,
177+ "Y1 ~ X2 | f1" ,
178+ "Y1 ~ X2 | f1 + f2" ,
179+ "Y2 ~ X1 | f1" ,
180+ "Y2 ~ X1 | f1 + f2" ,
181+ "Y2 ~ X2 | f1" ,
182+ "Y2 ~ X2 | f1 + f2" ,
183+ ],
184+ ),
185+ # sw0 in covariates + sw in fixed effects
186+ (
187+ "Y ~ sw0(X1, X2) | sw(f1, f2)" ,
188+ [
189+ "Y ~ 1 | f1" ,
190+ "Y ~ 1 | f2" ,
191+ "Y ~ X1 | f1" ,
192+ "Y ~ X1 | f2" ,
193+ "Y ~ X2 | f1" ,
194+ "Y ~ X2 | f2" ,
195+ ],
196+ ),
197+ # csw in covariates + csw0 in fixed effects
198+ (
199+ "Y ~ csw(X1, X2) | csw0(f1, f2)" ,
200+ [
201+ "Y ~ X1 | 1" ,
202+ "Y ~ X1 | f1" ,
203+ "Y ~ X1 | f1 + f2" ,
204+ "Y ~ X1 + X2 | 1" ,
205+ "Y ~ X1 + X2 | f1" ,
206+ "Y ~ X1 + X2 | f1 + f2" ,
207+ ],
208+ ),
209+ # sw(dep vars) + csw(covariates) + sw(fixed effects)
210+ (
211+ "sw(Y1, Y2) ~ csw(X1, X2) | sw(f1, f2)" ,
212+ [
213+ "Y1 ~ X1 | f1" ,
214+ "Y1 ~ X1 | f2" ,
215+ "Y1 ~ X1 + X2 | f1" ,
216+ "Y1 ~ X1 + X2 | f2" ,
217+ "Y2 ~ X1 | f1" ,
218+ "Y2 ~ X1 | f2" ,
219+ "Y2 ~ X1 + X2 | f1" ,
220+ "Y2 ~ X1 + X2 | f2" ,
221+ ],
222+ ),
223+ # mvsw in covariates + sw in fixed effects
224+ (
225+ "Y ~ mvsw(X1, X2) | sw(f1, f2)" ,
226+ [
227+ "Y ~ 1 | f1" ,
228+ "Y ~ 1 | f2" ,
229+ "Y ~ X1 | f1" ,
230+ "Y ~ X1 | f2" ,
231+ "Y ~ X2 | f1" ,
232+ "Y ~ X2 | f2" ,
233+ "Y ~ X1 + X2 | f1" ,
234+ "Y ~ X1 + X2 | f2" ,
235+ ],
236+ ),
237+ # mvsw in covariates + csw in fixed effects
238+ (
239+ "Y ~ mvsw(X1, X2) | csw(f1, f2)" ,
240+ [
241+ "Y ~ 1 | f1" ,
242+ "Y ~ 1 | f1 + f2" ,
243+ "Y ~ X1 | f1" ,
244+ "Y ~ X1 | f1 + f2" ,
245+ "Y ~ X2 | f1" ,
246+ "Y ~ X2 | f1 + f2" ,
247+ "Y ~ X1 + X2 | f1" ,
248+ "Y ~ X1 + X2 | f1 + f2" ,
249+ ],
250+ ),
171251 ],
172252 )
173253 def test_expand_all_multiple_estimation (self , formula , expected ):
@@ -269,6 +349,63 @@ def test_parse_sw_in_fe_and_independent(self):
269349 result = Formula .parse ("Y ~ sw(X1, X2) | sw(f1, f2)" )
270350 assert len (result ) == 4 # 2 x 2
271351
352+ def test_parse_multiple_dep_vars_with_sw_and_csw (self ):
353+ """Y1 + Y2 (preprocessed to sw) + sw in covars + csw in FE."""
354+ result = Formula .parse ("Y1 + Y2 ~ sw(X1, X2) | csw(f1, f2)" )
355+ assert len (result ) == 8 # 2 dep * 2 covars * 2 FE
356+ second_stages = [f .second_stage for f in result ]
357+ fixed_effects = [f .fixed_effects for f in result ]
358+ assert second_stages == [
359+ "Y1 ~ X1" , "Y1 ~ X1" , "Y1 ~ X2" , "Y1 ~ X2" ,
360+ "Y2 ~ X1" , "Y2 ~ X1" , "Y2 ~ X2" , "Y2 ~ X2" ,
361+ ]
362+ assert fixed_effects == [
363+ "f1" , "f1 + f2" , "f1" , "f1 + f2" ,
364+ "f1" , "f1 + f2" , "f1" , "f1 + f2" ,
365+ ]
366+
367+ def test_parse_csw0_in_fe_maps_to_none (self ):
368+ """csw0 in FE produces a '1' zero-step which maps to fixed_effects=None."""
369+ result = Formula .parse ("Y ~ X1 | csw0(f1, f2)" )
370+ assert len (result ) == 3
371+ assert result [0 ].fixed_effects is None # zero step: "1" -> None
372+ assert result [1 ].fixed_effects == "f1"
373+ assert result [2 ].fixed_effects == "f1 + f2"
374+
375+ def test_parse_sw0_in_fe_maps_to_none (self ):
376+ """sw0 in FE produces a '1' zero-step which maps to fixed_effects=None."""
377+ result = Formula .parse ("Y ~ X1 | sw0(f1, f2)" )
378+ assert len (result ) == 3
379+ assert result [0 ].fixed_effects is None # zero step: "1" -> None
380+ assert result [1 ].fixed_effects == "f1"
381+ assert result [2 ].fixed_effects == "f2"
382+
383+ def test_parse_mvsw_covars_with_csw_fe (self ):
384+ """mvsw in covariates combined with csw in fixed effects."""
385+ result = Formula .parse ("Y ~ mvsw(X1, X2) | csw(f1, f2)" )
386+ assert len (result ) == 8 # 4 mvsw * 2 csw
387+ second_stages = [f .second_stage for f in result ]
388+ fixed_effects = [f .fixed_effects for f in result ]
389+ assert second_stages == [
390+ "Y ~ 1" , "Y ~ 1" ,
391+ "Y ~ X1" , "Y ~ X1" ,
392+ "Y ~ X2" , "Y ~ X2" ,
393+ "Y ~ X1 + X2" , "Y ~ X1 + X2" ,
394+ ]
395+ assert fixed_effects == [
396+ "f1" , "f1 + f2" ,
397+ "f1" , "f1 + f2" ,
398+ "f1" , "f1 + f2" ,
399+ "f1" , "f1 + f2" ,
400+ ]
401+
402+ def test_parse_to_dict_csw0_fe_groups (self ):
403+ """parse_to_dict should group csw0 FE correctly, with None for zero-step."""
404+ result = Formula .parse_to_dict ("Y ~ X1 | csw0(f1, f2)" )
405+ assert None in result # zero step
406+ assert "f1" in result
407+ assert "f1 + f2" in result
408+
272409
273410class TestValidation :
274411 """Tests for formula validation / error handling."""
0 commit comments