2727import bigframes .core .bytecode as bytecode
2828import bigframes .core .expression as ex
2929import bigframes .core .ordering as ordering
30- import bigframes .core .window_spec as windows
30+ import bigframes .core .window_spec as window_specs
3131import bigframes .dtypes as dtypes
32+ import bigframes .functions
3233import bigframes .operations as ops
3334import bigframes .operations .aggregations as agg_ops
35+ from bigframes ._config import options
3436from bigframes .core import agg_expressions , py_expressions
3537
3638
37- def apply_to_block_rows (
38- func : Callable , block : blocks .Block , * args , ** kwargs
39- ) -> blocks .Block :
40- """
41- Apply the given function to each row of the block.
42-
43- The function is applied to each row of the block, and the result is returned
44- as a new block with the same index.
45- """
39+ def compile_udf (
40+ block : blocks .Block ,
41+ func : Callable ,
42+ args : tuple = (),
43+ kwargs : dict | None = None ,
44+ col_series_args : typing .Mapping [str , str ] | None = None ,
45+ window_spec : Optional [window_specs .WindowSpec ] = None ,
46+ ) -> ex .Expression :
47+ """Compile a python function to a BigFrames expression in the context of a block."""
48+ if kwargs is None :
49+ kwargs = {}
4650 expr = bytecode ._compile_bytecode_to_py_expr (func )
4751 sig = inspect .signature (func )
4852
@@ -54,17 +58,77 @@ def apply_to_block_rows(
5458 for name , value in bound_params .items ():
5559 bindings [name ] = ex .const (value )
5660
57- expr = py_expressions .resolve_py_exprs (
58- expr ,
59- series_arg = next (iter (sig .parameters .keys ())),
60- series_attrs = {
61- label : col_id
62- for label in block .column_labels
63- if (col_id := block .resolve_label_exact (label )) is not None
64- },
65- )
61+ series_arg = next (iter (sig .parameters .keys ()))
62+
63+ if col_series_args is not None :
64+ expr = py_expressions .resolve_py_exprs (
65+ expr ,
66+ series_arg = series_arg ,
67+ col_series_args = col_series_args ,
68+ window_spec = window_spec ,
69+ )
70+ else :
71+ series_attrs : dict = {}
72+ for i , (col_id , label ) in enumerate (
73+ zip (block .value_columns , block .column_labels )
74+ ):
75+ series_attrs [i ] = col_id
76+ if label is not None :
77+ series_attrs [label ] = col_id
78+
79+ expr = py_expressions .resolve_py_exprs (
80+ expr ,
81+ series_arg = series_arg ,
82+ series_attrs = series_attrs ,
83+ window_spec = window_spec ,
84+ )
85+
6686 expr = expr .bind_variables (bindings )
87+ return expr
88+
6789
90+ def is_transpiler_eligible (func : typing .Any ) -> bool :
91+ """Return True if func is eligible for Python transpilation."""
92+ return (
93+ options .experiments .enable_python_transpiler
94+ and callable (func )
95+ and not isinstance (func , bigframes .functions .Udf )
96+ )
97+
98+
99+ def compile_column_udf (
100+ block : blocks .Block ,
101+ func : Callable ,
102+ column_id : str ,
103+ args : tuple = (),
104+ kwargs : dict | None = None ,
105+ window_spec : Optional [window_specs .WindowSpec ] = None ,
106+ ) -> tuple [ex .Expression , str ]:
107+ """Compile a column-wise python UDF in block context and return (expr, name)."""
108+ sig = inspect .signature (func )
109+ series_arg = next (iter (sig .parameters .keys ()))
110+ expr = compile_udf (
111+ block ,
112+ func ,
113+ args = args ,
114+ kwargs = kwargs ,
115+ col_series_args = {series_arg : column_id },
116+ window_spec = window_spec ,
117+ )
118+ name = getattr (func , "__name__" , "<lambda>" )
119+ return expr , name
120+
121+
122+ def apply_to_block_rows (
123+ func : Callable , block : blocks .Block , * args , ** kwargs
124+ ) -> blocks .Block :
125+ """
126+ Apply the given function to each row of the block.
127+
128+ The function is applied to each row of the block, and the result is returned
129+ as a new block with the same index.
130+ """
131+ expr = compile_udf (block , func , args , kwargs )
68132 return block .project_exprs ([expr ], labels = [None ], drop = True )
69133
70134
@@ -107,13 +171,13 @@ def indicate_duplicates(
107171 agg_expressions .NullaryAggregation (
108172 agg_ops .RowNumberOp (),
109173 ),
110- window = windows .unbound (grouping_keys = tuple (columns )),
174+ window = window_specs .unbound (grouping_keys = tuple (columns )),
111175 )
112176 count = agg_expressions .WindowExpression (
113177 agg_expressions .NullaryAggregation (
114178 agg_ops .SizeOp (),
115179 ),
116- window = windows .unbound (grouping_keys = tuple (columns )),
180+ window = window_specs .unbound (grouping_keys = tuple (columns )),
117181 )
118182
119183 if keep == "first" :
@@ -147,7 +211,7 @@ def quantile(
147211 dropna : bool = False ,
148212) -> blocks .Block :
149213 # TODO: handle windowing and more interpolation methods
150- window = windows .unbound (
214+ window = window_specs .unbound (
151215 grouping_keys = tuple (grouping_column_ids ),
152216 )
153217 quantile_cols = []
@@ -248,8 +312,8 @@ def _interpolate_column(
248312 if interpolate_method not in ["linear" , "nearest" , "ffill" ]:
249313 raise ValueError ("interpolate method not supported" )
250314 window_ordering = (ordering .OrderingExpression (ex .deref (x_values )),)
251- backwards_window = windows .rows (end = 0 , ordering = window_ordering )
252- forwards_window = windows .rows (start = 0 , ordering = window_ordering )
315+ backwards_window = window_specs .rows (end = 0 , ordering = window_ordering )
316+ forwards_window = window_specs .rows (start = 0 , ordering = window_ordering )
253317
254318 # Note, this method may
255319 block , notnull = block .apply_unary_op (column , ops .notnull_op )
@@ -401,7 +465,7 @@ def value_counts(
401465 )
402466 count_id = block .value_columns [0 ]
403467 if normalize :
404- unbound_window = windows .unbound (grouping_keys = tuple (grouping_keys ))
468+ unbound_window = window_specs .unbound (grouping_keys = tuple (grouping_keys ))
405469 block , total_count_id = block .apply_window_op (
406470 count_id , agg_ops .sum_op , unbound_window
407471 )
@@ -429,7 +493,7 @@ def pct_change(block: blocks.Block, periods: int = 1) -> blocks.Block:
429493 column_labels = block .column_labels
430494
431495 # Window framing clause is not allowed for analytic function lag.
432- window_spec = windows .unbound ()
496+ window_spec = window_specs .unbound ()
433497
434498 original_columns = block .value_columns
435499 exprs = []
@@ -484,9 +548,9 @@ def rank(
484548 )
485549 window_op = agg_ops .dense_rank_op if method == "dense" else agg_ops .count_op
486550 window_spec = (
487- windows .unbound (grouping_keys = grouping_cols , ordering = window_ordering )
551+ window_specs .unbound (grouping_keys = grouping_cols , ordering = window_ordering )
488552 if method == "dense"
489- else windows .rows (
553+ else window_specs .rows (
490554 end = 0 , ordering = window_ordering , grouping_keys = grouping_cols
491555 )
492556 )
@@ -498,7 +562,7 @@ def rank(
498562 result_expr ,
499563 agg_expressions .WindowExpression (
500564 agg_expressions .UnaryAggregation (agg_ops .max_op , result_expr ),
501- windows .unbound (grouping_keys = grouping_cols ),
565+ window_specs .unbound (grouping_keys = grouping_cols ),
502566 ),
503567 )
504568 # Step 2: Apply aggregate to groups of like input values.
@@ -511,7 +575,7 @@ def rank(
511575 }[method ]
512576 result_expr = agg_expressions .WindowExpression (
513577 agg_expressions .UnaryAggregation (agg_op , result_expr ),
514- windows .unbound (grouping_keys = (col , * grouping_cols )),
578+ window_specs .unbound (grouping_keys = (col , * grouping_cols )),
515579 )
516580 # Pandas masks all values where any grouping column is null
517581 # Note: we use pd.NA instead of float('nan')
@@ -612,7 +676,7 @@ def nsmallest(
612676 block , counter = block .apply_window_op (
613677 column_ids [0 ],
614678 agg_ops .rank_op ,
615- window_spec = windows .unbound (ordering = tuple (order_refs )),
679+ window_spec = window_specs .unbound (ordering = tuple (order_refs )),
616680 )
617681 block , condition = block .project_expr (ops .le_op .as_expr (counter , ex .const (n )))
618682 block = block .filter_by_id (condition )
@@ -642,7 +706,7 @@ def nlargest(
642706 block , counter = block .apply_window_op (
643707 column_ids [0 ],
644708 agg_ops .rank_op ,
645- window_spec = windows .unbound (ordering = tuple (order_refs )),
709+ window_spec = window_specs .unbound (ordering = tuple (order_refs )),
646710 )
647711 block , condition = block .project_expr (ops .le_op .as_expr (counter , ex .const (n )))
648712 block = block .filter_by_id (condition )
@@ -913,7 +977,7 @@ def _idx_extrema(
913977 for idx_col in original_block .index_columns
914978 ],
915979 ]
916- window_spec = windows .unbound (ordering = tuple (order_refs ))
980+ window_spec = window_specs .unbound (ordering = tuple (order_refs ))
917981 idx_col = original_block .index_columns [0 ]
918982 block , result_col = block .apply_window_op (
919983 idx_col , agg_ops .first_op , window_spec
0 commit comments