@@ -892,30 +892,30 @@ def _fit_staggered(
892892 # - not_yet_treated (cohort_i > g): keep only t < cohort_i
893893 if self .control_group == "not_yet_treated" :
894894 cohort_g_set = set (cohort_g_units )
895- post_mask_g = sub_df [time ].isin (post_periods_g ) & (
896- sub_df [unit ].isin (cohort_g_set ) # treated cohort: all post
897- | (sub_df [cohort ] == 0 )
898- | sub_df [cohort ].isna () # never-treated: all post
899- | (sub_df [time ] < sub_df [cohort ]) # not-yet-treated: only before own treatment
895+ post_mask_g = sub_df [time ].isin (post_periods_g ) & ( # type: ignore[union-attr, call-overload]
896+ sub_df [unit ].isin (cohort_g_set ) # type: ignore[union-attr, call-overload]
897+ | (sub_df [cohort ] == 0 ) # type: ignore[call-overload]
898+ | sub_df [cohort ].isna () # type: ignore[union-attr, call-overload]
899+ | (sub_df [time ] < sub_df [cohort ]) # type: ignore[operator, call-overload]
900900 )
901901 else :
902- post_mask_g = sub_df [time ].isin (post_periods_g )
902+ post_mask_g = sub_df [time ].isin (post_periods_g ) # type: ignore[union-attr, call-overload]
903903
904- post_sub = sub_df .loc [post_mask_g ]
904+ post_sub = sub_df .loc [post_mask_g ] # type: ignore[union-attr]
905905
906906 unit_post_avg_g = post_sub .groupby (unit )["_ydot" ].mean ().reset_index ()
907907 unit_post_avg_g .columns = [unit , "_ydot_avg" ]
908908
909909 # Build cross-sectional sample
910910 # Treatment indicator: 1 if unit is in cohort g
911- cs_g = sub_df .drop_duplicates (subset = [unit ], keep = "first" )[[unit ] + controls ].copy ()
911+ cs_g = sub_df .drop_duplicates (subset = [unit ], keep = "first" )[[unit ] + controls ].copy () # type: ignore[union-attr]
912912 cs_g ["_treat_g" ] = cs_g [unit ].isin (cohort_g_units ).astype (float )
913913
914914 if cluster is not None :
915915 if cluster == unit :
916916 cs_g [cluster ] = cs_g [unit ]
917917 else :
918- cluster_map_g = sub_df .drop_duplicates (subset = [unit ], keep = "first" ).set_index (
918+ cluster_map_g = sub_df .drop_duplicates (subset = [unit ], keep = "first" ).set_index ( # type: ignore[union-attr]
919919 unit
920920 )[cluster ]
921921 cs_g [cluster ] = cs_g [unit ].map (cluster_map_g )
@@ -1245,6 +1245,11 @@ def _fit_event_study(
12451245
12461246 cohort_data_cache [g ] = cache_g
12471247
1248+ # Precompute unit-level controls lookup (time-invariant)
1249+ _unit_controls_df = None
1250+ if controls :
1251+ _unit_controls_df = df .drop_duplicates (subset = [unit ], keep = "first" ).set_index (unit )
1252+
12481253 # Compute WATT(r) and influence functions
12491254 event_study_effects = {}
12501255 if_matrix = {} # r -> IF vector of shape (n_total_units,)
@@ -1313,15 +1318,30 @@ def _fit_event_study(
13131318 cs_units = [cs_units [i ] for i in range (len (valid_mask )) if valid_mask [i ]]
13141319
13151320 controls_matrix_g = None
1316- if controls :
1317- ctrl_df = sub_df .drop_duplicates (subset = [unit ], keep = "first" ).set_index (unit )
1321+ if controls and _unit_controls_df is not None :
13181322 ctrl_vals = []
1323+ valid_ctrl_mask = []
13191324 for u in cs_units :
1320- if u in ctrl_df .index :
1321- ctrl_vals .append (ctrl_df .loc [u , controls ].values .astype (np .float64 ))
1325+ if u in _unit_controls_df .index :
1326+ row = _unit_controls_df .loc [u , controls ]
1327+ vals = row .values .astype (np .float64 ) if hasattr (row , 'values' ) else np .array ([float (row )])
1328+ if np .all (np .isfinite (vals )):
1329+ ctrl_vals .append (vals )
1330+ valid_ctrl_mask .append (True )
1331+ else :
1332+ valid_ctrl_mask .append (False )
13221333 else :
1323- ctrl_vals .append (np .full (len (controls ), np .nan ))
1324- controls_matrix_g = np .array (ctrl_vals )
1334+ valid_ctrl_mask .append (False )
1335+ # Filter out units with missing controls
1336+ if len (ctrl_vals ) < len (cs_units ):
1337+ valid_ctrl_mask = np .array (valid_ctrl_mask )
1338+ y_vec = y_vec [valid_ctrl_mask ]
1339+ treat_vec = treat_vec [valid_ctrl_mask ]
1340+ cs_units = [cs_units [i ] for i in range (len (valid_ctrl_mask )) if valid_ctrl_mask [i ]]
1341+ if len (cs_units ) < 3 or treat_vec .sum () == 0 or treat_vec .sum () == len (treat_vec ):
1342+ continue
1343+ if ctrl_vals :
1344+ controls_matrix_g = np .array (ctrl_vals )
13251345
13261346 att_g_r , se_g_r , coefs_g_r , vcov_g_r , n_params = self ._dispatch_estimator (
13271347 y_vec , treat_vec , controls_matrix_g , None , len (y_vec )
@@ -1669,7 +1689,7 @@ def _composite_regression_aggregation(
16691689 df_transformed = self ._transform_demean (df , outcome , unit , pre_mask_g )
16701690
16711691 # Per-unit average of transformed outcome in post-periods (>= g)
1672- post_data = df_transformed .loc [post_mask_g ]
1692+ post_data = df_transformed .loc [post_mask_g ] # type: ignore[union-attr]
16731693 unit_avg_g = post_data .groupby (unit )["_ydot" ].mean ()
16741694 ydot_by_cohort [g ] = unit_avg_g
16751695
@@ -3348,8 +3368,8 @@ def _bootstrap(
33483368 else :
33493369 df_t = self ._transform_detrend (df , outcome , unit , time , pre_mask )
33503370
3351- post_mask = df_t [time ].isin (post_periods )
3352- post_df = df_t .loc [post_mask ]
3371+ post_mask = df_t [time ].isin (post_periods ) # type: ignore[union-attr, call-overload]
3372+ post_df = df_t .loc [post_mask ] # type: ignore[union-attr]
33533373 unit_post_avg = post_df .groupby (unit )["_ydot" ].mean ()
33543374
33553375 cs_df = df .drop_duplicates (subset = [unit ], keep = "first" )[[unit ] + controls ].copy ()
@@ -3419,16 +3439,16 @@ def _bootstrap(
34193439 )
34203440
34213441 # Cross-sectional estimate
3422- post_mask_b = boot_df [time ].isin (post_periods )
3423- post_b = boot_df .loc [post_mask_b ]
3442+ post_mask_b = boot_df [time ].isin (post_periods ) # type: ignore[union-attr, call-overload]
3443+ post_b = boot_df .loc [post_mask_b ] # type: ignore[union-attr]
34243444 unit_avg_b = post_b .groupby ("_boot_unit" )["_ydot" ].mean ()
34253445
3426- cs_b = boot_df .drop_duplicates (subset = ["_boot_unit" ], keep = "first" )[
3446+ cs_b = boot_df .drop_duplicates (subset = ["_boot_unit" ], keep = "first" )[ # type: ignore[union-attr]
34273447 ["_boot_unit" ]
34283448 ].copy ()
34293449 if controls :
34303450 for c in controls :
3431- cs_b [c ] = boot_df .drop_duplicates (subset = ["_boot_unit" ], keep = "first" )[
3451+ cs_b [c ] = boot_df .drop_duplicates (subset = ["_boot_unit" ], keep = "first" )[ # type: ignore[union-attr]
34323452 c
34333453 ].values
34343454
@@ -3468,7 +3488,7 @@ def _bootstrap(
34683488 # Pre-generate all bootstrap unit samples with deterministic seeds
34693489 boot_unit_samples = []
34703490 for b in range (self .n_bootstrap ):
3471- rng_b = np .random .default_rng (seed = self .bootstrap_seed + b )
3491+ rng_b = np .random .default_rng (seed = ( self .bootstrap_seed or 0 ) + b )
34723492 boot_treated = rng_b .choice (treated_arr , size = n_treated , replace = True )
34733493 boot_control = rng_b .choice (control_arr , size = n_control , replace = True )
34743494 boot_unit_samples .append (np .concatenate ([boot_treated , boot_control ]))
@@ -3511,16 +3531,16 @@ def _run_replicate(b: int) -> float:
35113531 )
35123532
35133533 # Cross-sectional estimate
3514- post_mask_b = boot_df [time ].isin (post_periods )
3515- post_b = boot_df .loc [post_mask_b ]
3534+ post_mask_b = boot_df [time ].isin (post_periods ) # type: ignore[union-attr, call-overload]
3535+ post_b = boot_df .loc [post_mask_b ] # type: ignore[union-attr]
35163536 unit_avg_b = post_b .groupby ("_boot_unit" )["_ydot" ].mean ()
35173537
3518- cs_b = boot_df .drop_duplicates (subset = ["_boot_unit" ], keep = "first" )[
3538+ cs_b = boot_df .drop_duplicates (subset = ["_boot_unit" ], keep = "first" )[ # type: ignore[union-attr]
35193539 ["_boot_unit" ]
35203540 ].copy ()
35213541 if controls :
35223542 for c in controls :
3523- cs_b [c ] = boot_df .drop_duplicates (subset = ["_boot_unit" ], keep = "first" )[
3543+ cs_b [c ] = boot_df .drop_duplicates (subset = ["_boot_unit" ], keep = "first" )[ # type: ignore[union-attr]
35243544 c
35253545 ].values
35263546
@@ -3957,7 +3977,7 @@ def validate_staggered_data(data, unit, time, cohort) -> Dict[str, Any]:
39573977
39583978 df = data .copy ()
39593979
3960- results = {"valid" : True , "warnings" : [], "errors" : []}
3980+ results : dict [ str , Any ] = {"valid" : True , "warnings" : [], "errors" : []}
39613981
39623982 # Check required columns exist
39633983 for col in [unit , time , cohort ]:
0 commit comments