@@ -230,16 +230,12 @@ def tabulate(
230230 psu_col = f"psu{ _INTERNAL_CONCAT_SUFFIX } " if design .psu is not None else None
231231 ssu_col = f"ssu{ _INTERNAL_CONCAT_SUFFIX } " if design .ssu is not None else None
232232
233- # Cast columns to correct types
234- concat_data = concat_data .with_columns (pl .col (rowvar ).cast (pl .String ))
235- if colvar :
236- concat_data = concat_data .with_columns (pl .col (colvar ).cast (pl .String ))
237- if strata_col and strata_col in concat_data .columns :
238- concat_data = concat_data .with_columns (pl .col (strata_col ).cast (pl .String ))
239- if psu_col and psu_col in concat_data .columns :
240- concat_data = concat_data .with_columns (pl .col (psu_col ).cast (pl .String ))
241- if ssu_col and ssu_col in concat_data .columns :
242- concat_data = concat_data .with_columns (pl .col (ssu_col ).cast (pl .String ))
233+ # Cast all design columns to String in a single with_columns call
234+ _cast_cols = [rowvar ] + ([colvar ] if colvar else [])
235+ for _c in [strata_col , psu_col , ssu_col ]:
236+ if _c and _c in concat_data .columns :
237+ _cast_cols .append (_c )
238+ concat_data = concat_data .with_columns ([pl .col (c ).cast (pl .String ) for c in _cast_cols ])
243239
244240 # Determine whether to compute totals (weights not normalized)
245241 compute_totals = abs (float (wgt_arr .sum ()) - 1.0 ) > 1e-6
@@ -256,12 +252,11 @@ def tabulate(
256252 compute_totals = compute_totals ,
257253 )
258254
259- # Unpack cells DataFrame into CellEst objects
255+ # Unpack cells DataFrame into CellEst objects — vectorized CI computation
260256 df_val = int (cells_df ["df" ][0 ]) if cells_df .height > 0 else 1
261257 t_crit = float (t_dist .ppf (1 - alpha / 2 , df_val ))
262258 z_crit = float (norm_dist .ppf (1 - alpha / 2 ))
263259
264- import math
265260 import re
266261
267262 _NUMERIC_RE = re .compile (r"^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$" )
@@ -277,53 +272,64 @@ def _norm_label(v: str) -> str:
277272 pass
278273 return s
279274
280- cell_rows = []
281- for row in cells_df .iter_rows (named = True ):
282- est = row ["est" ]
283- se = row ["se" ]
275+ # Extract all columns as numpy arrays — one pass, no iter_rows
276+ est_arr = cells_df ["est" ].to_numpy ()
277+ se_arr = cells_df ["se" ].to_numpy ()
278+ cv_arr = cells_df ["cv" ].to_numpy ()
279+ rowvar_arr = cells_df ["rowvar" ].to_list ()
280+ colvar_arr = cells_df ["colvar" ].to_list ()
284281
285- if compute_totals :
286- lci = est - z_crit * se
287- uci = est + z_crit * se
288- else :
289- if est <= 0 or est >= 1 or se <= 0 :
290- lci , uci = est , est
291- else :
292- scale = se / (est * (1.0 - est ))
293- logit = math .log (est / (1 - est ))
294- lci = 1.0 / (1.0 + math .exp (- (logit - t_crit * scale )))
295- uci = 1.0 / (1.0 + math .exp (- (logit + t_crit * scale )))
296-
297- cell_rows .append (
298- CellEst (
299- rowvar = _norm_label (row ["rowvar" ]),
300- colvar = _norm_label (row ["colvar" ]),
301- est = est ,
302- se = se ,
303- cv = row ["cv" ],
304- lci = lci ,
305- uci = uci ,
306- )
282+ if compute_totals :
283+ lci_arr = est_arr - z_crit * se_arr
284+ uci_arr = est_arr + z_crit * se_arr
285+ else :
286+ # Logit CI — fully vectorized with masked ops
287+ valid = (est_arr > 0 ) & (est_arr < 1 ) & (se_arr > 0 )
288+ # Safe denominators for masked positions
289+ _p = np .where (valid , est_arr , 0.5 )
290+ scale = np .where (valid , se_arr / (_p * (1.0 - _p )), 0.0 )
291+ logit = np .where (valid , np .log (_p / (1.0 - _p )), 0.0 )
292+ lci_arr = np .where (
293+ valid ,
294+ 1.0 / (1.0 + np .exp (- (logit - t_crit * scale ))),
295+ est_arr ,
296+ )
297+ uci_arr = np .where (
298+ valid ,
299+ 1.0 / (1.0 + np .exp (- (logit + t_crit * scale ))),
300+ est_arr ,
301+ )
302+
303+ cell_rows = [
304+ CellEst (
305+ rowvar = _norm_label (rowvar_arr [i ]),
306+ colvar = _norm_label (colvar_arr [i ]),
307+ est = float (est_arr [i ]),
308+ se = float (se_arr [i ]),
309+ cv = float (cv_arr [i ]),
310+ lci = float (lci_arr [i ]),
311+ uci = float (uci_arr [i ]),
307312 )
313+ for i in range (len (est_arr ))
314+ ]
308315
309316 # Unpack stats DataFrame into TableStats (two-way only)
310317 tbl_stats = None
311318 if colvar is not None and stats_df .height > 0 :
312- stats_map = {row ["stat" ]: row for row in stats_df .iter_rows (named = True )}
313- cs = stats_map .get ("chisq" , {})
314- fs = stats_map .get ("f" , {})
319+ cs = stats_df .filter (pl .col ("stat" ) == "chisq" ).row (0 , named = True )
320+ fs = stats_df .filter (pl .col ("stat" ) == "f" ).row (0 , named = True )
315321
316322 tbl_stats = TableStats (
317323 chisq = ChiSquare (
318- df = int (cs . get ( "df" , 0 ) ),
319- value = cs . get ( "value" , 0.0 ) ,
320- p_value = cs . get ( "p_value" , 1.0 ) ,
324+ df = int (cs [ "df" ] ),
325+ value = cs [ "value" ] ,
326+ p_value = cs [ "p_value" ] ,
321327 ),
322328 f = FDist (
323- df_num = fs . get ( "df" , 0.0 ) ,
324- df_den = fs . get ( "df2" , 0.0 ) ,
325- value = fs . get ( "value" , 0.0 ) ,
326- p_value = fs . get ( "p_value" , 1.0 ) ,
329+ df_num = fs [ "df" ] ,
330+ df_den = fs [ "df2" ] ,
331+ value = fs [ "value" ] ,
332+ p_value = fs [ "p_value" ] ,
327333 ),
328334 )
329335
@@ -844,16 +850,10 @@ def _compute_midranks(y: np.ndarray, w: np.ndarray) -> tuple[np.ndarray, float]:
844850 cumw = np .cumsum (w_sorted )
845851 midrank_sorted = cumw - w_sorted / 2.0
846852
847- # Average over ties
848- rankhat_sorted = np .empty (n , dtype = np .float64 )
849- i = 0
850- while i < n :
851- j = i + 1
852- while j < n and y_sorted [j ] == y_sorted [i ]:
853- j += 1
854- avg_midrank = np .mean (midrank_sorted [i :j ])
855- rankhat_sorted [i :j ] = avg_midrank
856- i = j
853+ # Average over ties — vectorized with np.unique + np.bincount
854+ _ , inverse , counts = np .unique (y_sorted , return_inverse = True , return_counts = True )
855+ group_sums = np .bincount (inverse , weights = midrank_sorted , minlength = len (counts ))
856+ rankhat_sorted = (group_sums / counts )[inverse ]
857857
858858 rankhat = np .empty (n , dtype = np .float64 )
859859 rankhat [ii ] = rankhat_sorted
0 commit comments