HyukjinKwon opened a new pull request, #57952:
URL: https://github.com/apache/spark/pull/57952

   ### What changes were proposed in this pull request?
   
   This is a **draft / WIP** that adds a Python analog of the Scala typed
   `org.apache.spark.sql.expressions.Aggregator[IN, BUF, OUT]` with **true 
incremental (partial)
   aggregation** — i.e. map-side combine, not whole-group materialization.
   
   Users subclass a new `Aggregator` base class (`zero` / `reduce` / `merge` / 
`finish` +
   `bufferSchema`) and wrap it with `arrow_udaf(...)` for use in 
`groupBy().agg(...)`:
   
   ```python
   from pyspark.sql.pandas.aggregator import Aggregator, arrow_udaf
   from pyspark.sql.types import StructType, StructField, DoubleType, LongType
   
   class Mean(Aggregator):
       @property
       def bufferSchema(self):
           return StructType([StructField("sum", DoubleType()), 
StructField("count", LongType())])
       @property
       def outputType(self):
           return DoubleType()
       def zero(self):           return (0.0, 0)
       def reduce(self, buf, v): return (buf[0] + v[0], buf[1] + 1)
       def merge(self, a, b):    return (a[0] + b[0], a[1] + b[1])
       def finish(self, buf):    return buf[0] / buf[1] if buf[1] else None
   
   df.groupBy("k").agg(arrow_udaf(Mean())(df.v))
   ```
   
   Unlike grouped-agg pandas/arrow UDFs (`PythonUDAF` + 
`ArrowAggregatePythonExec`), which collect the
   whole group and call Python once, this is planned as a **two-stage 
aggregation**:
   
   - a map-side **PARTIAL** stage folds each group's input rows into a 
per-group buffer via `reduce`;
   - the buffers are shuffled by the grouping key (as an Arrow struct column);
   - a **FINAL** stage merges the partial buffers via `merge` and produces the 
output via `finish`.
   
   Because `merge` is associative/commutative, the result is independent of 
partition count.
   
   Class hierarchy / trace:
   
   - `PythonEvalType`: new `SQL_GROUPED_AGG_ARROW_INCREMENTAL_PARTIAL_UDF` 
(255) and `..._FINAL_UDF`
     (256), added on both the Python (`pyspark.util`) and JVM 
(`api.python.PythonEvalType`) sides.
   - Catalyst: new `PythonAggregate` expression (an `UnevaluableAggregateFunc`, 
like `PythonUDAF`)
     carrying the intermediate `bufferSchema`.
   - Planning: `SparkStrategies.Aggregation` routes an all-`PythonAggregate` 
aggregate to
     `PythonIncrementalAggregateExec.plan(...)`, which builds
     `PythonIncrementalAggregatePartialExec` -> (Exchange, inserted by 
`EnsureRequirements`) ->
     `PythonIncrementalAggregateFinalExec`. Both operators reuse 
`ArrowPythonWithNamedArgumentRunner`
     + `GroupedPythonArrowInput`.
   - Worker (`worker.py`): a PARTIAL handler that folds input batches into a 
buffer via `reduce`, and
     a FINAL handler that merges partial-buffer rows via `merge` then `finish`.
   - The buffer schema is threaded to the JVM via a new nullable `bufferType` on
     `UserDefinedPythonFunction` (an auxiliary constructor preserves the 
existing Py4J arity).
   
   Out of scope for this draft (planned follow-ups): `DISTINCT`, mixing with 
SQL aggregate functions in
   one `Aggregate`, window/streaming, Spark Connect parity, SQL 
`spark.udf.register`, real disk spill
   (currently the map side bounds memory by per-partition grouping; 
associativity makes early partial
   emission safe), and a typed-columnar vs. pickled buffer performance variant.
   
   ### Why are the changes needed?
   
   PySpark has no incremental user-defined aggregator: every custom-aggregation 
path (grouped-agg
   `pandas_udf`/`arrow_udf`, `applyInPandas`) materializes the whole group and 
invokes Python once,
   with no map-side combine or partial/merge across the shuffle. This adds the 
missing
   `Aggregator`-style abstraction with genuine partial aggregation, matching 
the Scala typed
   `Aggregator`.
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes — a new public API: `pyspark.sql.pandas.aggregator.Aggregator` and 
`arrow_udaf(...)`, usable in
   `groupBy().agg(...)`. No existing behavior changes.
   
   ### How was this patch tested?
   
   - Compilation verified: `sql/compile` (catalyst + core + sql) builds cleanly 
with the new
     expression, operators, and planner routing.
   - Added `python/pyspark/sql/tests/arrow/test_arrow_python_aggregator.py`
     (`ArrowPythonAggregatorTests`): checks the incremental aggregator matches 
built-in `avg`/`sum`,
     a no-group case, a custom buffer, and that results are independent of 
partition count (exercising
     partial + merge).
   
   NOTE (draft): a full end-to-end test run in the author's local environment 
is still pending due to
   an offline-build constraint (fresh-`master` dependency 
`at.yawk.lz4:lz4-java:1.11.2` is not in the
   local mirror, and the only prebuilt assembly available is from a different 
branch). CI on this PR is
   the intended validation.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code (Opus 4.8)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to