This is an automated email from the ASF dual-hosted git repository.

timsaucer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-python.git


The following commit(s) were added to refs/heads/main by this push:
     new fd8224e  Update Aggregate functions to take builder parameters (#859)
fd8224e is described below

commit fd8224e1d32282c1664575f32a2eb5d39ffaabba
Author: Tim Saucer <[email protected]>
AuthorDate: Mon Sep 9 14:13:21 2024 -0400

    Update Aggregate functions to take builder parameters (#859)
    
    * Add NullTreatment enum wrapper and add filter option to approx_distinct
    
    * Small usability on aggregate
    
    * Adding documentation and additional unit test for approx_median
    
    * Update approx_percentil_cont with builder parameters it uses, which is 
filter but not distinct
    
    * Update approx_percentil_cont_with_weight with builder parameters it uses, 
which is filter but not distinct
    
    * Update array_agg to use aggregate options
    
    * Update builder options for avg aggregate function
    
    * move bit_and bit_or to use macro to generaty python fn
    
    * Update builder arguments for bitwise operators
    
    * Use macro for bool_and and bool_or
    
    * Update python wrapper for arguments appropriate to bool operators
    
    * Set corr to use macro for pyfunction
    
    * Update unit test to make it easier to debug
    
    * Update corr python wrapper to expose only builder parameters used
    
    * Update count and count_star to use macro for exposing
    
    * Update count and count_star with approprate aggregation options
    
    * Move covar_pop and covar_samp to use macro for aggregates
    
    * Updateing covar_pop and covar_samp with builder option
    
    * Use macro for last_value and move first_value to be near it
    
    * Update first_value and last_value with the builder parameters that are 
relevant
    
    * Remove grouping since it is not actually implemented upstream
    
    * Move median to use macro
    
    * Expose builder options for median
    
    * Expose nth value
    
    * Updating linear regression functions to use filter and macro
    
    * Update stddev and stddev_pop to use filter and macro
    
    * Expose string_agg
    
    * Add string_agg to python wrappers and add unit test
    
    * Switch sum to use macro in rust side and expose correct options in python 
wrapper
    
    * Use macro for exposing var_pop and var_samp
    
    * Add unit tests for filtering on var_pop and var_samp
    
    * Move approximation functions to use macro when possible
    
    * Update user documentation to explain in detail the options for aggregate 
functions
    
    * Update unit test to handle Python 3.10
    
    * Clean up commented code
---
 .../user-guide/common-operations/aggregations.rst  | 206 ++++-
 python/datafusion/common.py                        |  15 +-
 python/datafusion/dataframe.py                     |   7 +-
 python/datafusion/expr.py                          |   4 +-
 python/datafusion/functions.py                     | 867 +++++++++++++++++----
 python/datafusion/tests/test_aggregation.py        | 359 ++++++++-
 python/datafusion/tests/test_functions.py          |  99 ++-
 python/datafusion/tests/test_wrapper_coverage.py   |  11 +
 src/functions.rs                                   | 521 ++++---------
 9 files changed, 1470 insertions(+), 619 deletions(-)

diff --git a/docs/source/user-guide/common-operations/aggregations.rst 
b/docs/source/user-guide/common-operations/aggregations.rst
index 7ad4022..8fee26a 100644
--- a/docs/source/user-guide/common-operations/aggregations.rst
+++ b/docs/source/user-guide/common-operations/aggregations.rst
@@ -20,43 +20,205 @@
 Aggregation
 ============
 
-An aggregate or aggregation is a function where the values of multiple rows 
are processed together to form a single summary value.
-For performing an aggregation, DataFusion provides the 
:py:func:`~datafusion.dataframe.DataFrame.aggregate`
+An aggregate or aggregation is a function where the values of multiple rows 
are processed together
+to form a single summary value. For performing an aggregation, DataFusion 
provides the
+:py:func:`~datafusion.dataframe.DataFrame.aggregate`
 
 .. ipython:: python
 
+    import urllib.request
     from datafusion import SessionContext
-    from datafusion import column, lit
+    from datafusion import col, lit
     from datafusion import functions as f
-    import random
 
-    ctx = SessionContext()
-    df = ctx.from_pydict(
-        {
-            "a": ["foo", "bar", "foo", "bar", "foo", "bar", "foo", "foo"],
-            "b": ["one", "one", "two", "three", "two", "two", "one", "three"],
-            "c": [random.randint(0, 100) for _ in range(8)],
-            "d": [random.random() for _ in range(8)],
-        },
-        name="foo_bar"
+    urllib.request.urlretrieve(
+        
"https://gist.githubusercontent.com/ritchie46/cac6b337ea52281aa23c049250a4ff03/raw/89a957ff3919d90e6ef2d34235e6bf22304f3366/pokemon.csv";,
+        "pokemon.csv",
     )
 
-    col_a = column("a")
-    col_b = column("b")
-    col_c = column("c")
-    col_d = column("d")
+    ctx = SessionContext()
+    df = ctx.read_csv("pokemon.csv")
+
+    col_type_1 = col('"Type 1"')
+    col_type_2 = col('"Type 2"')
+    col_speed = col('"Speed"')
+    col_attack = col('"Attack"')
 
-    df.aggregate([], [f.approx_distinct(col_c), f.approx_median(col_d), 
f.approx_percentile_cont(col_d, lit(0.5))])
+    df.aggregate([col_type_1], [
+        f.approx_distinct(col_speed).alias("Count"),
+        f.approx_median(col_speed).alias("Median Speed"),
+        f.approx_percentile_cont(col_speed, 0.9).alias("90% Speed")])
 
-When the :code:`group_by` list is empty the aggregation is done over the whole 
:class:`.DataFrame`. For grouping
-the :code:`group_by` list must contain at least one column
+When the :code:`group_by` list is empty the aggregation is done over the whole 
:class:`.DataFrame`.
+For grouping the :code:`group_by` list must contain at least one column.
 
 .. ipython:: python
 
-    df.aggregate([col_a], [f.sum(col_c), f.max(col_d), f.min(col_d)])
+    df.aggregate([col_type_1], [
+        f.max(col_speed).alias("Max Speed"),
+        f.avg(col_speed).alias("Avg Speed"),
+        f.min(col_speed).alias("Min Speed")])
 
 More than one column can be used for grouping
 
 .. ipython:: python
 
-    df.aggregate([col_a, col_b], [f.sum(col_c), f.max(col_d), f.min(col_d)])
+    df.aggregate([col_type_1, col_type_2], [
+        f.max(col_speed).alias("Max Speed"),
+        f.avg(col_speed).alias("Avg Speed"),
+        f.min(col_speed).alias("Min Speed")])
+
+
+
+Setting Parameters
+------------------
+
+Each of the built in aggregate functions provides arguments for the parameters 
that affect their
+operation. These can also be overridden using the builder approach to setting 
any of the following
+parameters. When you use the builder, you must call ``build()`` to finish. For 
example, these two
+expressions are equivalent.
+
+.. ipython:: python
+
+    first_1 = f.first_value(col("a"), order_by=[col("a")])
+    first_2 = f.first_value(col("a")).order_by(col("a")).build()
+
+Ordering
+^^^^^^^^
+
+You can control the order in which rows are processed by window functions by 
providing
+a list of ``order_by`` functions for the ``order_by`` parameter. In the 
following example, we
+sort the Pokemon by their attack in increasing order and take the first value, 
which gives us the
+Pokemon with the smallest attack value in each ``Type 1``.
+
+.. ipython:: python
+
+    df.aggregate(
+        [col('"Type 1"')],
+        [f.first_value(
+            col('"Name"'),
+            order_by=[col('"Attack"').sort(ascending=True)]
+            ).alias("Smallest Attack")
+        ])
+
+Distinct
+^^^^^^^^
+
+When you set the parameter ``distinct`` to ``True``, then unique values will 
only be evaluated one
+time each. Suppose we want to create an array of all of the ``Type 2`` for 
each ``Type 1`` of our
+Pokemon set. Since there will be many entries of ``Type 2`` we only one each 
distinct value.
+
+.. ipython:: python
+
+    df.aggregate([col_type_1], [f.array_agg(col_type_2, 
distinct=True).alias("Type 2 List")])
+
+In the output of the above we can see that there are some ``Type 1`` for which 
the ``Type 2`` entry
+is ``null``. In reality, we probably want to filter those out. We can do this 
in two ways. First,
+we can filter DataFrame rows that have no ``Type 2``. If we do this, we might 
have some ``Type 1``
+entries entirely removed. The second is we can use the ``filter`` argument 
described below.
+
+.. ipython:: python
+
+    df.filter(col_type_2.is_not_null()).aggregate([col_type_1], 
[f.array_agg(col_type_2, distinct=True).alias("Type 2 List")])
+
+    df.aggregate([col_type_1], [f.array_agg(col_type_2, distinct=True, 
filter=col_type_2.is_not_null()).alias("Type 2 List")])
+
+Which approach you take should depend on your use case.
+
+Null Treatment
+^^^^^^^^^^^^^^
+
+This option allows you to either respect or ignore null values.
+
+One common usage for handling nulls is the case where you want to find the 
first value within a
+partition. By setting the null treatment to ignore nulls, we can find the 
first non-null value
+in our partition.
+
+
+.. ipython:: python
+
+    from datafusion.common import NullTreatment
+
+    df.aggregate([col_type_1], [
+        f.first_value(
+            col_type_2,
+            order_by=[col_attack],
+            null_treatment=NullTreatment.RESPECT_NULLS
+        ).alias("Lowest Attack Type 2")])
+
+    df.aggregate([col_type_1], [
+        f.first_value(
+            col_type_2,
+            order_by=[col_attack],
+            null_treatment=NullTreatment.IGNORE_NULLS
+        ).alias("Lowest Attack Type 2")])
+
+Filter
+^^^^^^
+
+Using the filter option is useful for filtering results to include in the 
aggregate function. It can
+be seen in the example above on how this can be useful to only filter rows 
evaluated by the
+aggregate function without filtering rows from the entire DataFrame.
+
+Filter takes a single expression.
+
+Suppose we want to find the speed values for only Pokemon that have low Attack 
values.
+
+.. ipython:: python
+
+    df.aggregate([col_type_1], [
+        f.avg(col_speed).alias("Avg Speed All"),
+        f.avg(col_speed, filter=col_attack < lit(50)).alias("Avg Speed Low 
Attack")])
+
+
+Aggregate Functions
+-------------------
+
+The available aggregate functions are:
+
+1. Comparison Functions
+    - :py:func:`datafusion.functions.min`
+    - :py:func:`datafusion.functions.max`
+2. Math Functions
+    - :py:func:`datafusion.functions.sum`
+    - :py:func:`datafusion.functions.avg`
+    - :py:func:`datafusion.functions.median`
+3. Array Functions
+    - :py:func:`datafusion.functions.array_agg`
+4. Logical Functions
+    - :py:func:`datafusion.functions.bit_and`
+    - :py:func:`datafusion.functions.bit_or`
+    - :py:func:`datafusion.functions.bit_xor`
+    - :py:func:`datafusion.functions.bool_and`
+    - :py:func:`datafusion.functions.bool_or`
+5. Statistical Functions
+    - :py:func:`datafusion.functions.count`
+    - :py:func:`datafusion.functions.corr`
+    - :py:func:`datafusion.functions.covar_samp`
+    - :py:func:`datafusion.functions.covar_pop`
+    - :py:func:`datafusion.functions.stddev`
+    - :py:func:`datafusion.functions.stddev_pop`
+    - :py:func:`datafusion.functions.var_samp`
+    - :py:func:`datafusion.functions.var_pop`
+6. Linear Regression Functions
+    - :py:func:`datafusion.functions.regr_count`
+    - :py:func:`datafusion.functions.regr_slope`
+    - :py:func:`datafusion.functions.regr_intercept`
+    - :py:func:`datafusion.functions.regr_r2`
+    - :py:func:`datafusion.functions.regr_avgx`
+    - :py:func:`datafusion.functions.regr_avgy`
+    - :py:func:`datafusion.functions.regr_sxx`
+    - :py:func:`datafusion.functions.regr_syy`
+    - :py:func:`datafusion.functions.regr_slope`
+7. Positional Functions
+    - :py:func:`datafusion.functions.first_value`
+    - :py:func:`datafusion.functions.last_value`
+    - :py:func:`datafusion.functions.nth_value`
+8. String Functions
+    - :py:func:`datafusion.functions.string_agg`
+9. Approximation Functions
+    - :py:func:`datafusion.functions.approx_distinct`
+    - :py:func:`datafusion.functions.approx_median`
+    - :py:func:`datafusion.functions.approx_percentile_cont`
+    - :py:func:`datafusion.functions.approx_percentile_cont_with_weight`
+
diff --git a/python/datafusion/common.py b/python/datafusion/common.py
index 225e333..7db8333 100644
--- a/python/datafusion/common.py
+++ b/python/datafusion/common.py
@@ -17,13 +17,13 @@
 """Common data types used throughout the DataFusion project."""
 
 from ._internal import common as common_internal
+from enum import Enum
 
 # TODO these should all have proper wrapper classes
 
 DFSchema = common_internal.DFSchema
 DataType = common_internal.DataType
 DataTypeMap = common_internal.DataTypeMap
-NullTreatment = common_internal.NullTreatment
 PythonType = common_internal.PythonType
 RexType = common_internal.RexType
 SqlFunction = common_internal.SqlFunction
@@ -47,3 +47,16 @@ __all__ = [
     "SqlStatistics",
     "SqlFunction",
 ]
+
+
+class NullTreatment(Enum):
+    """Describe how null values are to be treated by functions.
+
+    This is used primarily by aggregate and window functions. It can be set on
+    these functions using the builder approach described in
+    ref:`_window_functions` and ref:`_aggregation` in the online documentation.
+
+    """
+
+    RESPECT_NULLS = common_internal.NullTreatment.RESPECT_NULLS
+    IGNORE_NULLS = common_internal.NullTreatment.IGNORE_NULLS
diff --git a/python/datafusion/dataframe.py b/python/datafusion/dataframe.py
index 46b8fa1..56dff22 100644
--- a/python/datafusion/dataframe.py
+++ b/python/datafusion/dataframe.py
@@ -180,7 +180,9 @@ class DataFrame:
         """
         return DataFrame(self.df.with_column_renamed(old_name, new_name))
 
-    def aggregate(self, group_by: list[Expr], aggs: list[Expr]) -> DataFrame:
+    def aggregate(
+        self, group_by: list[Expr] | Expr, aggs: list[Expr] | Expr
+    ) -> DataFrame:
         """Aggregates the rows of the current DataFrame.
 
         Args:
@@ -190,6 +192,9 @@ class DataFrame:
         Returns:
             DataFrame after aggregation.
         """
+        group_by = group_by if isinstance(group_by, list) else [group_by]
+        aggs = aggs if isinstance(aggs, list) else [aggs]
+
         group_by = [e.expr for e in group_by]
         aggs = [e.expr for e in aggs]
         return DataFrame(self.df.aggregate(group_by, aggs))
diff --git a/python/datafusion/expr.py b/python/datafusion/expr.py
index 7fa6080..bd6a86f 100644
--- a/python/datafusion/expr.py
+++ b/python/datafusion/expr.py
@@ -473,7 +473,7 @@ class Expr:
         set parameters for either window or aggregate functions. If used on 
any other
         type of expression, an error will be generated when ``build()`` is 
called.
         """
-        return ExprFuncBuilder(self.expr.null_treatment(null_treatment))
+        return ExprFuncBuilder(self.expr.null_treatment(null_treatment.value))
 
     def partition_by(self, *partition_by: Expr) -> ExprFuncBuilder:
         """Set the partitioning for a window function.
@@ -518,7 +518,7 @@ class ExprFuncBuilder:
 
     def null_treatment(self, null_treatment: NullTreatment) -> ExprFuncBuilder:
         """Set how nulls are treated for either window or aggregate 
functions."""
-        return ExprFuncBuilder(self.builder.null_treatment(null_treatment))
+        return 
ExprFuncBuilder(self.builder.null_treatment(null_treatment.value))
 
     def partition_by(self, *partition_by: Expr) -> ExprFuncBuilder:
         """Set partitioning for window functions."""
diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py
index 97b4fe1..163ff04 100644
--- a/python/datafusion/functions.py
+++ b/python/datafusion/functions.py
@@ -18,9 +18,10 @@
 
 from __future__ import annotations
 
-from datafusion._internal import functions as f, common
+from datafusion._internal import functions as f, expr as expr_internal
 from datafusion.expr import CaseBuilder, Expr, WindowFrame
 from datafusion.context import SessionContext
+from datafusion.common import NullTreatment
 
 from typing import Any, Optional
 
@@ -126,7 +127,6 @@ __all__ = [
     "floor",
     "from_unixtime",
     "gcd",
-    "grouping",
     "in_list",
     "initcap",
     "isnan",
@@ -180,6 +180,7 @@ __all__ = [
     "named_struct",
     "nanvl",
     "now",
+    "nth_value",
     "nullif",
     "octet_length",
     "order_by",
@@ -222,6 +223,7 @@ __all__ = [
     "stddev",
     "stddev_pop",
     "stddev_samp",
+    "string_agg",
     "strpos",
     "struct",
     "substr",
@@ -244,6 +246,7 @@ __all__ = [
     "var",
     "var_pop",
     "var_samp",
+    "var_sample",
     "when",
     # Window Functions
     "window",
@@ -258,6 +261,12 @@ __all__ = [
 ]
 
 
+def expr_list_to_raw_expr_list(
+    expr_list: Optional[list[Expr]],
+) -> Optional[list[expr_internal.Expr]]:
+    return [e.expr for e in expr_list] if expr_list is not None else None
+
+
 def isnan(expr: Expr) -> Expr:
     """Returns true if a given number is +NaN or -NaN otherwise returns 
false."""
     return Expr(f.isnan(expr.expr))
@@ -358,9 +367,18 @@ def col(name: str) -> Expr:
     return Expr(f.col(name))
 
 
-def count_star() -> Expr:
-    """Create a COUNT(1) aggregate expression."""
-    return Expr(f.count_star())
+def count_star(filter: Optional[Expr] = None) -> Expr:
+    """Create a COUNT(1) aggregate expression.
+
+    This aggregate function will count all of the rows in the partition.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``distinct``, and ``null_treatment``.
+
+    Args:
+        filter: If provided, only count rows for which the filter is True
+    """
+    return count(Expr.literal(1), filter=filter)
 
 
 def case(expr: Expr) -> CaseBuilder:
@@ -400,8 +418,8 @@ def window(
         df.select(functions.lag(col("a")).partition_by(col("b")).build())
     """
     args = [a.expr for a in args]
-    partition_by = [e.expr for e in partition_by] if partition_by is not None 
else None
-    order_by = [o.expr for o in order_by] if order_by is not None else None
+    partition_by = expr_list_to_raw_expr_list(partition_by)
+    order_by = expr_list_to_raw_expr_list(order_by)
     window_frame = window_frame.window_frame if window_frame is not None else 
None
     return Expr(f.window(name, args, partition_by, order_by, window_frame, 
ctx))
 
@@ -1486,291 +1504,788 @@ def flatten(array: Expr) -> Expr:
 
 
 # aggregate functions
-def approx_distinct(expression: Expr) -> Expr:
-    """Returns the approximate number of distinct values."""
-    return Expr(f.approx_distinct(expression.expr))
+def approx_distinct(
+    expression: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Returns the approximate number of distinct values.
+
+    This aggregate function is similar to :py:func:`count` with distinct set, 
but it
+    will approximate the number of distinct entries. It may return 
significantly faster
+    than :py:func:`count` for some DataFrames.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        expression: Values to check for distinct entries
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+
+    return Expr(f.approx_distinct(expression.expr, filter=filter_raw))
+
 
+def approx_median(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Returns the approximate median value.
 
-def approx_median(arg: Expr, distinct: bool = False) -> Expr:
-    """Returns the approximate median value."""
-    return Expr(f.approx_median(arg.expr, distinct=distinct))
+    This aggregate function is similar to :py:func:`median`, but it will only
+    approximate the median. It may return significantly faster for some 
DataFrames.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by`` and ``null_treatment``, and ``distinct``.
+
+    Args:
+        expression: Values to find the median for
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.approx_median(expression.expr, filter=filter_raw))
 
 
 def approx_percentile_cont(
     expression: Expr,
-    percentile: Expr,
-    num_centroids: Expr | None = None,
-    distinct: bool = False,
+    percentile: float,
+    num_centroids: Optional[int] = None,
+    filter: Optional[Expr] = None,
 ) -> Expr:
-    """Returns the value that is approximately at a given percentile of 
``expr``."""
-    if num_centroids is None:
-        return Expr(
-            f.approx_percentile_cont(
-                expression.expr, percentile.expr, distinct=distinct, 
num_centroids=None
-            )
-        )
+    """Returns the value that is approximately at a given percentile of 
``expr``.
+
+    This aggregate function assumes the input values form a continuous 
distribution.
+    Suppose you have a DataFrame which consists of 100 different test scores. 
If you
+    called this function with a percentile of 0.9, it would return the value 
of the
+    test score that is above 90% of the other test scores. The returned value 
may be
+    between two of the values.
+
+    This function uses the [t-digest](https://arxiv.org/abs/1902.04023) 
algorithm to
+    compute the percentil. You can limit the number of bins used in this 
algorithm by
+    setting the ``num_centroids`` parameter.
 
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        expression: Values for which to find the approximate percentile
+        percentile: This must be between 0.0 and 1.0, inclusive
+        num_centroids: Max bin size for the t-digest algorithm
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
     return Expr(
         f.approx_percentile_cont(
-            expression.expr,
-            percentile.expr,
-            distinct=distinct,
-            num_centroids=num_centroids.expr,
+            expression.expr, percentile, num_centroids=num_centroids, 
filter=filter_raw
         )
     )
 
 
 def approx_percentile_cont_with_weight(
-    arg: Expr, weight: Expr, percentile: Expr, distinct: bool = False
+    expression: Expr, weight: Expr, percentile: float, filter: Optional[Expr] 
= None
 ) -> Expr:
-    """Returns the value of the approximate percentile.
+    """Returns the value of the weighted approximate percentile.
+
+    This aggregate function is similar to :py:func:`approx_percentile_cont` 
except that
+    it uses the associated associated weights.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        expression: Values for which to find the approximate percentile
+        weight: Relative weight for each of the values in ``expression``
+        percentile: This must be between 0.0 and 1.0, inclusive
+        filter: If provided, only compute against rows for which the filter is 
True
 
-    This function is similar to :py:func:`approx_percentile_cont` except that 
it uses
-    the associated associated weights.
     """
+    filter_raw = filter.expr if filter is not None else None
     return Expr(
         f.approx_percentile_cont_with_weight(
-            arg.expr, weight.expr, percentile.expr, distinct=distinct
+            expression.expr, weight.expr, percentile, filter=filter_raw
         )
     )
 
 
-def array_agg(arg: Expr, distinct: bool = False) -> Expr:
-    """Aggregate values into an array."""
-    return Expr(f.array_agg(arg.expr, distinct=distinct))
+def array_agg(
+    expression: Expr,
+    distinct: bool = False,
+    filter: Optional[Expr] = None,
+    order_by: Optional[list[Expr]] = None,
+) -> Expr:
+    """Aggregate values into an array.
 
+    Currently ``distinct`` and ``order_by`` cannot be used together. As a work 
around,
+    consider :py:func:`array_sort` after aggregation.
+    [Issue Tracker](https://github.com/apache/datafusion/issues/12371)
 
-def avg(arg: Expr, distinct: bool = False) -> Expr:
-    """Returns the average value."""
-    return Expr(f.avg(arg.expr, distinct=distinct))
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the option ``null_treatment``.
 
+    Args:
+        expression: Values to combine into an array
+        distinct: If True, a single entry for each distinct value will be in 
the result
+        filter: If provided, only compute against rows for which the filter is 
True
+        order_by: Order the resultant array values
+    """
+    order_by_raw = expr_list_to_raw_expr_list(order_by)
+    filter_raw = filter.expr if filter is not None else None
+
+    return Expr(
+        f.array_agg(
+            expression.expr, distinct=distinct, filter=filter_raw, 
order_by=order_by_raw
+        )
+    )
 
-def corr(value1: Expr, value2: Expr, distinct: bool = False) -> Expr:
-    """Returns the correlation coefficient between ``value1`` and 
``value2``."""
-    return Expr(f.corr(value1.expr, value2.expr, distinct=distinct))
 
+def avg(
+    expression: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Returns the average value.
 
-def count(args: Expr | list[Expr] | None = None, distinct: bool = False) -> 
Expr:
-    """Returns the number of rows that match the given arguments."""
-    if args is None:
-        return count(Expr.literal(1), distinct=distinct)
-    if isinstance(args, list):
-        args = [arg.expr for arg in args]
-    elif isinstance(args, Expr):
-        args = [args.expr]
-    return Expr(f.count(*args, distinct=distinct))
+    This aggregate function expects a numeric expression and will return a 
float.
 
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
 
-def covar(y: Expr, x: Expr) -> Expr:
-    """Computes the sample covariance.
+    Args:
+        expression: Values to combine into an array
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.avg(expression.expr, filter=filter_raw))
 
-    This is an alias for :py:func:`covar_samp`.
+
+def corr(value_y: Expr, value_x: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Returns the correlation coefficient between ``value1`` and ``value2``.
+
+    This aggregate function expects both values to be numeric and will return 
a float.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        value_y: The dependent variable for correlation
+        value_x: The independent variable for correlation
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.corr(value_y.expr, value_x.expr, filter=filter_raw))
+
+
+def count(
+    expressions: Expr | list[Expr] | None = None,
+    distinct: bool = False,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Returns the number of rows that match the given arguments.
+
+    This aggregate function will count the non-null rows provided in the 
expression.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by`` and ``null_treatment``.
+
+    Args:
+        expressions: Argument to perform bitwise calculation on
+        distinct: If True, a single entry for each distinct value will be in 
the result
+        filter: If provided, only compute against rows for which the filter is 
True
     """
-    return covar_samp(y, x)
+    filter_raw = filter.expr if filter is not None else None
 
+    if expressions is None:
+        args = [Expr.literal(1).expr]
+    elif isinstance(expressions, list):
+        args = [arg.expr for arg in expressions]
+    else:
+        args = [expressions.expr]
 
-def covar_pop(y: Expr, x: Expr) -> Expr:
-    """Computes the population covariance."""
-    return Expr(f.covar_pop(y.expr, x.expr))
+    return Expr(f.count(*args, distinct=distinct, filter=filter_raw))
 
 
-def covar_samp(y: Expr, x: Expr) -> Expr:
-    """Computes the sample covariance."""
-    return Expr(f.covar_samp(y.expr, x.expr))
+def covar_pop(value_y: Expr, value_x: Expr, filter: Optional[Expr] = None) -> 
Expr:
+    """Computes the population covariance.
 
+    This aggregate function expects both values to be numeric and will return 
a float.
 
-def grouping(arg: Expr, distinct: bool = False) -> Expr:
-    """Indicates if the expression is aggregated or not.
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
 
-    Returns 1 if the value of the argument is aggregated, 0 if not.
+    Args:
+        value_y: The dependent variable for covariance
+        value_x: The independent variable for covariance
+        filter: If provided, only compute against rows for which the filter is 
True
     """
-    return Expr(f.grouping(arg.expr, distinct=distinct))
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.covar_pop(value_y.expr, value_x.expr, filter=filter_raw))
 
 
-def max(arg: Expr, distinct: bool = False) -> Expr:
-    """Returns the maximum value of the argument."""
-    return Expr(f.max(arg.expr, distinct=distinct))
+def covar_samp(value_y: Expr, value_x: Expr, filter: Optional[Expr] = None) -> 
Expr:
+    """Computes the sample covariance.
 
+    This aggregate function expects both values to be numeric and will return 
a float.
 
-def mean(arg: Expr, distinct: bool = False) -> Expr:
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        value_y: The dependent variable for covariance
+        value_x: The independent variable for covariance
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.covar_samp(value_y.expr, value_x.expr, filter=filter_raw))
+
+
+def covar(value_y: Expr, value_x: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Computes the sample covariance.
+
+    This is an alias for :py:func:`covar_samp`.
+    """
+    return covar_samp(value_y, value_x, filter)
+
+
+def max(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Aggregate function that returns the maximum value of the argument.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        expression: The value to find the maximum of
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.max(expression.expr, filter=filter_raw))
+
+
+def mean(expression: Expr, filter: Optional[Expr] = None) -> Expr:
     """Returns the average (mean) value of the argument.
 
     This is an alias for :py:func:`avg`.
     """
-    return avg(arg, distinct)
+    return avg(expression, filter)
+
+
+def median(
+    expression: Expr, distinct: bool = False, filter: Optional[Expr] = None
+) -> Expr:
+    """Computes the median of a set of numbers.
+
+    This aggregate function returns the median value of the expression for the 
given
+    aggregate function.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by`` and ``null_treatment``.
+
+    Args:
+        expression: The value to compute the median of
+        distinct: If True, a single entry for each distinct value will be in 
the result
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.median(expression.expr, distinct=distinct, 
filter=filter_raw))
+
 
+def min(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Returns the minimum value of the argument.
 
-def median(arg: Expr) -> Expr:
-    """Computes the median of a set of numbers."""
-    return Expr(f.median(arg.expr))
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
 
+    Args:
+        expression: The value to find the minimum of
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.min(expression.expr, filter=filter_raw))
 
-def min(arg: Expr, distinct: bool = False) -> Expr:
-    """Returns the minimum value of the argument."""
-    return Expr(f.min(arg.expr, distinct=distinct))
 
+def sum(
+    expression: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Computes the sum of a set of numbers.
 
-def sum(arg: Expr) -> Expr:
-    """Computes the sum of a set of numbers."""
-    return Expr(f.sum(arg.expr))
+    This aggregate function expects a numeric expression.
 
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
 
-def stddev(arg: Expr, distinct: bool = False) -> Expr:
-    """Computes the standard deviation of the argument."""
-    return Expr(f.stddev(arg.expr, distinct=distinct))
+    Args:
+        expression: Values to combine into an array
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.sum(expression.expr, filter=filter_raw))
 
 
-def stddev_pop(arg: Expr, distinct: bool = False) -> Expr:
-    """Computes the population standard deviation of the argument."""
-    return Expr(f.stddev_pop(arg.expr, distinct=distinct))
+def stddev(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Computes the standard deviation of the argument.
 
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
 
-def stddev_samp(arg: Expr, distinct: bool = False) -> Expr:
+    Args:
+        expression: The value to find the minimum of
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.stddev(expression.expr, filter=filter_raw))
+
+
+def stddev_pop(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Computes the population standard deviation of the argument.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        expression: The value to find the minimum of
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.stddev_pop(expression.expr, filter=filter_raw))
+
+
+def stddev_samp(arg: Expr, filter: Optional[Expr] = None) -> Expr:
     """Computes the sample standard deviation of the argument.
 
     This is an alias for :py:func:`stddev`.
     """
-    return stddev(arg, distinct)
+    return stddev(arg, filter=filter)
 
 
-def var(arg: Expr) -> Expr:
+def var(expression: Expr, filter: Optional[Expr] = None) -> Expr:
     """Computes the sample variance of the argument.
 
     This is an alias for :py:func:`var_samp`.
     """
-    return var_samp(arg)
+    return var_samp(expression, filter)
+
+
+def var_pop(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Computes the population variance of the argument.
 
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
 
-def var_pop(arg: Expr, distinct: bool = False) -> Expr:
-    """Computes the population variance of the argument."""
-    return Expr(f.var_pop(arg.expr, distinct=distinct))
+    Args:
+        expression: The variable to compute the variance for
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.var_pop(expression.expr, filter=filter_raw))
 
 
-def var_samp(arg: Expr) -> Expr:
-    """Computes the sample variance of the argument."""
-    return Expr(f.var_samp(arg.expr))
+def var_samp(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Computes the sample variance of the argument.
 
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
 
-def regr_avgx(y: Expr, x: Expr, distinct: bool = False) -> Expr:
+    Args:
+        expression: The variable to compute the variance for
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.var_sample(expression.expr, filter=filter_raw))
+
+
+def var_sample(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Computes the sample variance of the argument.
+
+    This is an alias for :py:func:`var_samp`.
+    """
+    return var_samp(expression, filter)
+
+
+def regr_avgx(
+    y: Expr,
+    x: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
     """Computes the average of the independent variable ``x``.
 
-    Only non-null pairs of the inputs are evaluated.
+    This is a linear regression aggregate function. Only non-null pairs of the 
inputs
+    are evaluated.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        y: The linear regression dependent variable
+        x: The linear regression independent variable
+        filter: If provided, only compute against rows for which the filter is 
True
     """
-    return Expr(f.regr_avgx(y.expr, x.expr, distinct))
+    filter_raw = filter.expr if filter is not None else None
+
+    return Expr(f.regr_avgx(y.expr, x.expr, filter=filter_raw))
 
 
-def regr_avgy(y: Expr, x: Expr, distinct: bool = False) -> Expr:
+def regr_avgy(
+    y: Expr,
+    x: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
     """Computes the average of the dependent variable ``y``.
 
-    Only non-null pairs of the inputs are evaluated.
+    This is a linear regression aggregate function. Only non-null pairs of the 
inputs
+    are evaluated.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        y: The linear regression dependent variable
+        x: The linear regression independent variable
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+
+    return Expr(f.regr_avgy(y.expr, x.expr, filter=filter_raw))
+
+
+def regr_count(
+    y: Expr,
+    x: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Counts the number of rows in which both expressions are not null.
+
+    This is a linear regression aggregate function. Only non-null pairs of the 
inputs
+    are evaluated.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        y: The linear regression dependent variable
+        x: The linear regression independent variable
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+
+    return Expr(f.regr_count(y.expr, x.expr, filter=filter_raw))
+
+
+def regr_intercept(
+    y: Expr,
+    x: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Computes the intercept from the linear regression.
+
+    This is a linear regression aggregate function. Only non-null pairs of the 
inputs
+    are evaluated.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        y: The linear regression dependent variable
+        x: The linear regression independent variable
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+
+    return Expr(f.regr_intercept(y.expr, x.expr, filter=filter_raw))
+
+
+def regr_r2(
+    y: Expr,
+    x: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Computes the R-squared value from linear regression.
+
+    This is a linear regression aggregate function. Only non-null pairs of the 
inputs
+    are evaluated.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        y: The linear regression dependent variable
+        x: The linear regression independent variable
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+
+    return Expr(f.regr_r2(y.expr, x.expr, filter=filter_raw))
+
+
+def regr_slope(
+    y: Expr,
+    x: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Computes the slope from linear regression.
+
+    This is a linear regression aggregate function. Only non-null pairs of the 
inputs
+    are evaluated.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        y: The linear regression dependent variable
+        x: The linear regression independent variable
+        filter: If provided, only compute against rows for which the filter is 
True
     """
-    return Expr(f.regr_avgy(y.expr, x.expr, distinct))
+    filter_raw = filter.expr if filter is not None else None
+
+    return Expr(f.regr_slope(y.expr, x.expr, filter=filter_raw))
 
 
-def regr_count(y: Expr, x: Expr, distinct: bool = False) -> Expr:
-    """Counts the number of rows in which both expressions are not null."""
-    return Expr(f.regr_count(y.expr, x.expr, distinct))
+def regr_sxx(
+    y: Expr,
+    x: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Computes the sum of squares of the independent variable ``x``.
+
+    This is a linear regression aggregate function. Only non-null pairs of the 
inputs
+    are evaluated.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        y: The linear regression dependent variable
+        x: The linear regression independent variable
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
 
+    return Expr(f.regr_sxx(y.expr, x.expr, filter=filter_raw))
 
-def regr_intercept(y: Expr, x: Expr, distinct: bool = False) -> Expr:
-    """Computes the intercept from the linear regression."""
-    return Expr(f.regr_intercept(y.expr, x.expr, distinct))
 
+def regr_sxy(
+    y: Expr,
+    x: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Computes the sum of products of pairs of numbers.
+
+    This is a linear regression aggregate function. Only non-null pairs of the 
inputs
+    are evaluated.
 
-def regr_r2(y: Expr, x: Expr, distinct: bool = False) -> Expr:
-    """Computes the R-squared value from linear regression."""
-    return Expr(f.regr_r2(y.expr, x.expr, distinct))
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
 
+    Args:
+        y: The linear regression dependent variable
+        x: The linear regression independent variable
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
 
-def regr_slope(y: Expr, x: Expr, distinct: bool = False) -> Expr:
-    """Computes the slope from linear regression."""
-    return Expr(f.regr_slope(y.expr, x.expr, distinct))
+    return Expr(f.regr_sxy(y.expr, x.expr, filter=filter_raw))
 
 
-def regr_sxx(y: Expr, x: Expr, distinct: bool = False) -> Expr:
-    """Computes the sum of squares of the independent variable ``x``."""
-    return Expr(f.regr_sxx(y.expr, x.expr, distinct))
+def regr_syy(
+    y: Expr,
+    x: Expr,
+    filter: Optional[Expr] = None,
+) -> Expr:
+    """Computes the sum of squares of the dependent variable ``y``.
 
+    This is a linear regression aggregate function. Only non-null pairs of the 
inputs
+    are evaluated.
 
-def regr_sxy(y: Expr, x: Expr, distinct: bool = False) -> Expr:
-    """Computes the sum of products of pairs of numbers."""
-    return Expr(f.regr_sxy(y.expr, x.expr, distinct))
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
 
+    Args:
+        y: The linear regression dependent variable
+        x: The linear regression independent variable
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
 
-def regr_syy(y: Expr, x: Expr, distinct: bool = False) -> Expr:
-    """Computes the sum of squares of the dependent variable ``y``."""
-    return Expr(f.regr_syy(y.expr, x.expr, distinct))
+    return Expr(f.regr_syy(y.expr, x.expr, filter=filter_raw))
 
 
 def first_value(
-    arg: Expr,
-    distinct: bool = False,
-    filter: Optional[bool] = None,
+    expression: Expr,
+    filter: Optional[Expr] = None,
     order_by: Optional[list[Expr]] = None,
-    null_treatment: Optional[common.NullTreatment] = None,
+    null_treatment: NullTreatment = NullTreatment.RESPECT_NULLS,
 ) -> Expr:
-    """Returns the first value in a group of values."""
-    order_by_cols = [e.expr for e in order_by] if order_by is not None else 
None
+    """Returns the first value in a group of values.
+
+    This aggregate function will return the first value in the partition.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the option ``distinct``.
+
+    Args:
+        expression: Argument to perform bitwise calculation on
+        filter: If provided, only compute against rows for which the filter is 
True
+        order_by: Set the ordering of the expression to evaluate
+        null_treatment: Assign whether to respect or ignull null values.
+    """
+    order_by_raw = expr_list_to_raw_expr_list(order_by)
+    filter_raw = filter.expr if filter is not None else None
 
     return Expr(
         f.first_value(
-            arg.expr,
-            distinct=distinct,
-            filter=filter,
-            order_by=order_by_cols,
-            null_treatment=null_treatment,
+            expression.expr,
+            filter=filter_raw,
+            order_by=order_by_raw,
+            null_treatment=null_treatment.value,
         )
     )
 
 
 def last_value(
-    arg: Expr,
-    distinct: bool = False,
-    filter: Optional[bool] = None,
+    expression: Expr,
+    filter: Optional[Expr] = None,
     order_by: Optional[list[Expr]] = None,
-    null_treatment: Optional[common.NullTreatment] = None,
+    null_treatment: NullTreatment = NullTreatment.RESPECT_NULLS,
 ) -> Expr:
     """Returns the last value in a group of values.
 
-    To set parameters on this expression, use ``.order_by()``, ``.distinct()``,
-    ``.filter()``, or ``.null_treatment()``.
+    This aggregate function will return the last value in the partition.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the option ``distinct``.
+
+    Args:
+        expression: Argument to perform bitwise calculation on
+        filter: If provided, only compute against rows for which the filter is 
True
+        order_by: Set the ordering of the expression to evaluate
+        null_treatment: Assign whether to respect or ignull null values.
     """
-    order_by_cols = [e.expr for e in order_by] if order_by is not None else 
None
+    order_by_raw = expr_list_to_raw_expr_list(order_by)
+    filter_raw = filter.expr if filter is not None else None
 
     return Expr(
         f.last_value(
-            arg.expr,
-            distinct=distinct,
-            filter=filter,
-            order_by=order_by_cols,
-            null_treatment=null_treatment,
+            expression.expr,
+            filter=filter_raw,
+            order_by=order_by_raw,
+            null_treatment=null_treatment.value,
+        )
+    )
+
+
+def nth_value(
+    expression: Expr,
+    n: int,
+    filter: Optional[Expr] = None,
+    order_by: Optional[list[Expr]] = None,
+    null_treatment: NullTreatment = NullTreatment.RESPECT_NULLS,
+) -> Expr:
+    """Returns the n-th value in a group of values.
+
+    This aggregate function will return the n-th value in the partition.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the option ``distinct``.
+
+    Args:
+        expression: Argument to perform bitwise calculation on
+        n: Index of value to return. Starts at 1.
+        filter: If provided, only compute against rows for which the filter is 
True
+        order_by: Set the ordering of the expression to evaluate
+        null_treatment: Assign whether to respect or ignull null values.
+    """
+    order_by_raw = expr_list_to_raw_expr_list(order_by)
+    filter_raw = filter.expr if filter is not None else None
+
+    return Expr(
+        f.nth_value(
+            expression.expr,
+            n,
+            filter=filter_raw,
+            order_by=order_by_raw,
+            null_treatment=null_treatment.value,
         )
     )
 
 
-def bit_and(arg: Expr, distinct: bool = False) -> Expr:
-    """Computes the bitwise AND of the argument."""
-    return Expr(f.bit_and(arg.expr, distinct=distinct))
+def bit_and(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Computes the bitwise AND of the argument.
+
+    This aggregate function will bitwise compare every value in the input 
partition.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
 
+    Args:
+        expression: Argument to perform bitwise calculation on
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.bit_and(expression.expr, filter=filter_raw))
 
-def bit_or(arg: Expr, distinct: bool = False) -> Expr:
-    """Computes the bitwise OR of the argument."""
-    return Expr(f.bit_or(arg.expr, distinct=distinct))
 
+def bit_or(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Computes the bitwise OR of the argument.
 
-def bit_xor(arg: Expr, distinct: bool = False) -> Expr:
-    """Computes the bitwise XOR of the argument."""
-    return Expr(f.bit_xor(arg.expr, distinct=distinct))
+    This aggregate function will bitwise compare every value in the input 
partition.
 
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        expression: Argument to perform bitwise calculation on
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.bit_or(expression.expr, filter=filter_raw))
 
-def bool_and(arg: Expr, distinct: bool = False) -> Expr:
-    """Computes the boolean AND of the argument."""
-    return Expr(f.bool_and(arg.expr, distinct=distinct))
 
+def bit_xor(
+    expression: Expr, distinct: bool = False, filter: Optional[Expr] = None
+) -> Expr:
+    """Computes the bitwise XOR of the argument.
 
-def bool_or(arg: Expr, distinct: bool = False) -> Expr:
-    """Computes the boolean OR of the argument."""
-    return Expr(f.bool_or(arg.expr, distinct=distinct))
+    This aggregate function will bitwise compare every value in the input 
partition.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by`` and ``null_treatment``.
+
+    Args:
+        expression: Argument to perform bitwise calculation on
+        distinct: If True, evaluate each unique value of expression only once
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.bit_xor(expression.expr, distinct=distinct, 
filter=filter_raw))
+
+
+def bool_and(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Computes the boolean AND of the argument.
+
+    This aggregate function will compare every value in the input partition. 
These are
+    expected to be boolean values.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        expression: Argument to perform calculation on
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.bool_and(expression.expr, filter=filter_raw))
+
+
+def bool_or(expression: Expr, filter: Optional[Expr] = None) -> Expr:
+    """Computes the boolean OR of the argument.
+
+    This aggregate function will compare every value in the input partition. 
These are
+    expected to be boolean values.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``order_by``, ``null_treatment``, and ``distinct``.
+
+    Args:
+        expression: Argument to perform calculation on
+        filter: If provided, only compute against rows for which the filter is 
True
+    """
+    filter_raw = filter.expr if filter is not None else None
+    return Expr(f.bool_or(expression.expr, filter=filter_raw))
 
 
 def lead(
@@ -2107,3 +2622,37 @@ def ntile(
             order_by=order_cols,
         )
     )
+
+
+def string_agg(
+    expression: Expr,
+    delimiter: str,
+    filter: Optional[Expr] = None,
+    order_by: Optional[list[Expr]] = None,
+) -> Expr:
+    """Concatenates the input strings.
+
+    This aggregate function will concatenate input strings, ignoring null 
values, and
+    seperating them with the specified delimiter. Non-string values will be 
converted to
+    their string equivalents.
+
+    If using the builder functions described in ref:`_aggregation` this 
function ignores
+    the options ``distinct`` and ``null_treatment``.
+
+    Args:
+        expression: Argument to perform bitwise calculation on
+        delimiter: Text to place between each value of expression
+        filter: If provided, only compute against rows for which the filter is 
True
+        order_by: Set the ordering of the expression to evaluate
+    """
+    order_by_raw = expr_list_to_raw_expr_list(order_by)
+    filter_raw = filter.expr if filter is not None else None
+
+    return Expr(
+        f.string_agg(
+            expression.expr,
+            delimiter,
+            filter=filter_raw,
+            order_by=order_by_raw,
+        )
+    )
diff --git a/python/datafusion/tests/test_aggregation.py 
b/python/datafusion/tests/test_aggregation.py
index ab653c4..243a8c3 100644
--- a/python/datafusion/tests/test_aggregation.py
+++ b/python/datafusion/tests/test_aggregation.py
@@ -21,6 +21,7 @@ import pytest
 
 from datafusion import SessionContext, column, lit
 from datafusion import functions as f
+from datafusion.common import NullTreatment
 
 
 @pytest.fixture
@@ -34,12 +35,30 @@ def df():
             pa.array([4, 4, 6]),
             pa.array([9, 8, 5]),
             pa.array([True, True, False]),
+            pa.array([1, 2, None]),
         ],
-        names=["a", "b", "c", "d"],
+        names=["a", "b", "c", "d", "e"],
     )
     return ctx.create_dataframe([[batch]])
 
 
[email protected]
+def df_partitioned():
+    ctx = SessionContext()
+
+    # create a RecordBatch and a new DataFrame from it
+    batch = pa.RecordBatch.from_arrays(
+        [
+            pa.array([0, 1, 2, 3, 4, 5, 6]),
+            pa.array([7, None, 7, 8, 9, None, 9]),
+            pa.array(["A", "A", "A", "A", "B", "B", "B"]),
+        ],
+        names=["a", "b", "c"],
+    )
+
+    return ctx.create_dataframe([[batch]])
+
+
 @pytest.fixture
 def df_aggregate_100():
     ctx = SessionContext()
@@ -87,6 +106,7 @@ def df_aggregate_100():
     ],
 )
 def test_aggregation_stats(df, agg_expr, calc_expected):
+    df = df.select("a", "b", "c", "d")
     agg_df = df.aggregate([], [agg_expr])
     result = agg_df.collect()[0]
     values_a, values_b, values_c, values_d = df.collect()[0]
@@ -95,68 +115,323 @@ def test_aggregation_stats(df, agg_expr, calc_expected):
 
 
 @pytest.mark.parametrize(
-    "agg_expr, expected",
+    "agg_expr, expected, array_sort",
     [
-        (f.approx_distinct(column("b")), pa.array([2], type=pa.uint64())),
-        (f.approx_median(column("b")), pa.array([4])),
-        (f.approx_percentile_cont(column("b"), lit(0.5)), pa.array([4])),
+        (f.approx_distinct(column("b")), pa.array([2], type=pa.uint64()), 
False),
+        (
+            f.approx_distinct(
+                column("b"),
+                filter=column("a") != lit(3),
+            ),
+            pa.array([1], type=pa.uint64()),
+            False,
+        ),
+        (f.approx_median(column("b")), pa.array([4]), False),
+        (f.median(column("b"), distinct=True), pa.array([5]), False),
+        (f.median(column("b"), filter=column("a") != 2), pa.array([5]), False),
+        (f.approx_median(column("b"), filter=column("a") != 2), pa.array([5]), 
False),
+        (f.approx_percentile_cont(column("b"), 0.5), pa.array([4]), False),
         (
-            f.approx_percentile_cont_with_weight(column("b"), lit(0.6), 
lit(0.5)),
+            f.approx_percentile_cont_with_weight(column("b"), lit(0.6), 0.5),
             pa.array([6], type=pa.float64()),
+            False,
+        ),
+        (
+            f.approx_percentile_cont_with_weight(
+                column("b"), lit(0.6), 0.5, filter=column("a") != lit(3)
+            ),
+            pa.array([4], type=pa.float64()),
+            False,
+        ),
+        (f.array_agg(column("b")), pa.array([[4, 4, 6]]), False),
+        (f.array_agg(column("b"), distinct=True), pa.array([[4, 6]]), True),
+        (
+            f.array_agg(column("e"), filter=column("e").is_not_null()),
+            pa.array([[1, 2]]),
+            False,
+        ),
+        (
+            f.array_agg(column("b"), order_by=[column("c")]),
+            pa.array([[6, 4, 4]]),
+            False,
+        ),
+        (f.avg(column("b"), filter=column("a") != lit(1)), pa.array([5.0]), 
False),
+        (f.sum(column("b"), filter=column("a") != lit(1)), pa.array([10]), 
False),
+        (f.count(column("b"), distinct=True), pa.array([2]), False),
+        (f.count(column("b"), filter=column("a") != 3), pa.array([2]), False),
+        (f.count(), pa.array([3]), False),
+        (f.count(column("e")), pa.array([2]), False),
+        (f.count_star(filter=column("a") != 3), pa.array([2]), False),
+        (f.max(column("a"), filter=column("a") != lit(3)), pa.array([2]), 
False),
+        (f.min(column("a"), filter=column("a") != lit(1)), pa.array([2]), 
False),
+        (
+            f.stddev(column("a"), filter=column("a") != lit(2)),
+            pa.array([np.sqrt(2)]),
+            False,
+        ),
+        (
+            f.stddev_pop(column("a"), filter=column("a") != lit(2)),
+            pa.array([1.0]),
+            False,
         ),
-        (f.array_agg(column("b")), pa.array([[4, 4, 6]])),
     ],
 )
-def test_aggregation(df, agg_expr, expected):
-    agg_df = df.aggregate([], [agg_expr])
+def test_aggregation(df, agg_expr, expected, array_sort):
+    agg_df = df.aggregate([], [agg_expr.alias("agg_expr")])
+    if array_sort:
+        agg_df = agg_df.select(f.array_sort(column("agg_expr")))
+    agg_df.show()
     result = agg_df.collect()[0]
+
+    print(result)
     assert result.column(0) == expected
 
 
-def test_aggregate_100(df_aggregate_100):
[email protected](
+    "name,expr,expected",
+    [
+        (
+            "approx_percentile_cont",
+            f.approx_percentile_cont(column("c3"), 0.95, num_centroids=200),
+            [73, 68, 122, 124, 115],
+        ),
+        (
+            "approx_perc_cont_few_centroids",
+            f.approx_percentile_cont(column("c3"), 0.95, num_centroids=5),
+            [72, 68, 119, 124, 115],
+        ),
+        (
+            "approx_perc_cont_filtered",
+            f.approx_percentile_cont(
+                column("c3"), 0.95, num_centroids=200, filter=column("c3") > 
lit(0)
+            ),
+            [83, 68, 122, 124, 117],
+        ),
+        (
+            "corr",
+            f.corr(column("c3"), column("c2")),
+            [-0.1056, -0.2808, 0.0023, 0.0022, -0.2473],
+        ),
+        (
+            "corr_w_filter",
+            f.corr(column("c3"), column("c2"), filter=column("c3") > lit(0)),
+            [-0.3298, 0.2925, 0.2467, -0.2269, 0.0358],
+        ),
+        (
+            "covar_pop",
+            f.covar_pop(column("c3"), column("c2")),
+            [-7.2857, -25.6731, 0.2222, 0.2469, -20.2857],
+        ),
+        (
+            "covar_pop_w_filter",
+            f.covar_pop(column("c3"), column("c2"), filter=column("c3") > 
lit(0)),
+            [-9.25, 9.0579, 13.7521, -9.9669, 1.1641],
+        ),
+        (
+            "covar_samp",
+            f.covar_samp(column("c3"), column("c2")),
+            [-7.65, -27.0994, 0.2333, 0.2614, -21.3],
+        ),
+        (
+            "covar_samp_w_filter",
+            f.covar_samp(column("c3"), column("c2"), filter=column("c3") > 
lit(0)),
+            [-10.5714, 9.9636, 15.1273, -10.9636, 1.2417],
+        ),
+        (
+            "var_samp",
+            f.var_samp(column("c2")),
+            [1.9286, 2.2047, 1.6333, 2.1438, 1.6],
+        ),
+        (
+            "var_samp_w_filter",
+            f.var_samp(column("c2"), filter=column("c3") > lit(0)),
+            [1.4286, 2.4182, 1.8545, 1.4727, 1.6292],
+        ),
+        (
+            "var_pop",
+            f.var_pop(column("c2")),
+            [1.8367, 2.0886, 1.5556, 2.0247, 1.5238],
+        ),
+        (
+            "var_pop_w_filter",
+            f.var_pop(column("c2"), filter=column("c3") > lit(0)),
+            [1.25, 2.1983, 1.686, 1.3388, 1.5273],
+        ),
+    ],
+)
+def test_aggregate_100(df_aggregate_100, name, expr, expected):
     # 
https://github.com/apache/datafusion/blob/bddb6415a50746d2803dd908d19c3758952d74f9/datafusion/sqllogictest/test_files/aggregate.slt#L1490-L1498
 
-    result = (
+    df = (
         df_aggregate_100.aggregate(
             [column("c1")],
-            [f.approx_percentile_cont(column("c3"), lit(0.95), 
lit(200)).alias("c3")],
+            [expr.alias(name)],
         )
+        .select("c1", f.round(column(name), lit(4)).alias(name))
         .sort(column("c1").sort(ascending=True))
-        .collect()
     )
+    df.show()
 
-    assert len(result) == 1
-    result = result[0]
-    assert result.column("c1") == pa.array(["a", "b", "c", "d", "e"])
-    assert result.column("c3") == pa.array([73, 68, 122, 124, 115])
+    expected_dict = {
+        "c1": ["a", "b", "c", "d", "e"],
+        name: expected,
+    }
 
+    assert df.collect()[0].to_pydict() == expected_dict
 
-def test_bit_add_or_xor(df):
-    df = df.aggregate(
-        [],
-        [
-            f.bit_and(column("a")),
-            f.bit_or(column("b")),
-            f.bit_xor(column("c")),
-        ],
-    )
 
-    result = df.collect()
-    result = result[0]
-    assert result.column(0) == pa.array([0])
-    assert result.column(1) == pa.array([6])
-    assert result.column(2) == pa.array([4])
+data_test_bitwise_and_boolean_functions = [
+    ("bit_and", f.bit_and(column("a")), [0]),
+    ("bit_and_filter", f.bit_and(column("a"), filter=column("a") != lit(2)), 
[1]),
+    ("bit_or", f.bit_or(column("b")), [6]),
+    ("bit_or_filter", f.bit_or(column("b"), filter=column("a") != lit(3)), 
[4]),
+    ("bit_xor", f.bit_xor(column("c")), [4]),
+    ("bit_xor_distinct", f.bit_xor(column("b"), distinct=True), [2]),
+    ("bit_xor_filter", f.bit_xor(column("b"), filter=column("a") != lit(3)), 
[0]),
+    (
+        "bit_xor_filter_distinct",
+        f.bit_xor(column("b"), distinct=True, filter=column("a") != lit(3)),
+        [4],
+    ),
+    ("bool_and", f.bool_and(column("d")), [False]),
+    ("bool_and_filter", f.bool_and(column("d"), filter=column("a") != lit(3)), 
[True]),
+    ("bool_or", f.bool_or(column("d")), [True]),
+    ("bool_or_filter", f.bool_or(column("d"), filter=column("a") == lit(3)), 
[False]),
+]
 
 
-def test_bool_and_or(df):
-    df = df.aggregate(
-        [],
-        [
-            f.bool_and(column("d")),
-            f.bool_or(column("d")),
-        ],
[email protected]("name,expr,result", 
data_test_bitwise_and_boolean_functions)
+def test_bit_and_bool_fns(df, name, expr, result):
+    df = df.aggregate([], [expr.alias(name)])
+
+    expected = {
+        name: result,
+    }
+
+    assert df.collect()[0].to_pydict() == expected
+
+
[email protected](
+    "name,expr,result",
+    [
+        ("first_value", f.first_value(column("a")), [0, 4]),
+        (
+            "first_value_ordered",
+            f.first_value(column("a"), 
order_by=[column("a").sort(ascending=False)]),
+            [3, 6],
+        ),
+        (
+            "first_value_with_null",
+            f.first_value(
+                column("b"),
+                order_by=[column("b").sort(ascending=True)],
+                null_treatment=NullTreatment.RESPECT_NULLS,
+            ),
+            [None, None],
+        ),
+        (
+            "first_value_ignore_null",
+            f.first_value(
+                column("b"),
+                order_by=[column("b").sort(ascending=True)],
+                null_treatment=NullTreatment.IGNORE_NULLS,
+            ),
+            [7, 9],
+        ),
+        ("last_value", f.last_value(column("a")), [3, 6]),
+        (
+            "last_value_ordered",
+            f.last_value(column("a"), 
order_by=[column("a").sort(ascending=False)]),
+            [0, 4],
+        ),
+        (
+            "last_value_with_null",
+            f.last_value(
+                column("b"),
+                order_by=[column("b").sort(ascending=True, nulls_first=False)],
+                null_treatment=NullTreatment.RESPECT_NULLS,
+            ),
+            [None, None],
+        ),
+        (
+            "last_value_ignore_null",
+            f.last_value(
+                column("b"),
+                order_by=[column("b").sort(ascending=True)],
+                null_treatment=NullTreatment.IGNORE_NULLS,
+            ),
+            [8, 9],
+        ),
+        ("first_value", f.first_value(column("a")), [0, 4]),
+        (
+            "nth_value_ordered",
+            f.nth_value(column("a"), 2, 
order_by=[column("a").sort(ascending=False)]),
+            [2, 5],
+        ),
+        (
+            "nth_value_with_null",
+            f.nth_value(
+                column("b"),
+                3,
+                order_by=[column("b").sort(ascending=True, nulls_first=False)],
+                null_treatment=NullTreatment.RESPECT_NULLS,
+            ),
+            [8, None],
+        ),
+        (
+            "nth_value_ignore_null",
+            f.nth_value(
+                column("b"),
+                2,
+                order_by=[column("b").sort(ascending=True)],
+                null_treatment=NullTreatment.IGNORE_NULLS,
+            ),
+            [7, 9],
+        ),
+    ],
+)
+def test_first_last_value(df_partitioned, name, expr, result) -> None:
+    df = df_partitioned.aggregate([column("c")], 
[expr.alias(name)]).sort(column("c"))
+
+    expected = {
+        "c": ["A", "B"],
+        name: result,
+    }
+
+    assert df.collect()[0].to_pydict() == expected
+
+
[email protected](
+    "name,expr,result",
+    [
+        ("string_agg", f.string_agg(column("a"), ","), "one,two,three,two"),
+        ("string_agg", f.string_agg(column("b"), ""), "03124"),
+        (
+            "string_agg",
+            f.string_agg(column("a"), ",", filter=column("b") != lit(3)),
+            "one,three,two",
+        ),
+        (
+            "string_agg",
+            f.string_agg(column("a"), ",", order_by=[column("b")]),
+            "one,three,two,two",
+        ),
+    ],
+)
+def test_string_agg(name, expr, result) -> None:
+    ctx = SessionContext()
+
+    df = ctx.from_pydict(
+        {
+            "a": ["one", "two", None, "three", "two"],
+            "b": [0, 3, 1, 2, 4],
+        }
     )
-    result = df.collect()
-    result = result[0]
-    assert result.column(0) == pa.array([False])
-    assert result.column(1) == pa.array([True])
+
+    df = df.aggregate([], [expr.alias(name)])
+
+    expected = {
+        name: [result],
+    }
+    df.show()
+    assert df.collect()[0].to_pydict() == expected
diff --git a/python/datafusion/tests/test_functions.py 
b/python/datafusion/tests/test_functions.py
index e7e6d79..8e3c513 100644
--- a/python/datafusion/tests/test_functions.py
+++ b/python/datafusion/tests/test_functions.py
@@ -912,17 +912,64 @@ def test_regr_funcs_sql_2():
 @pytest.mark.parametrize(
     "func, expected",
     [
-        pytest.param(f.regr_slope, pa.array([2], type=pa.float64()), 
id="regr_slope"),
+        pytest.param(f.regr_slope(column("c2"), column("c1")), [4.6], 
id="regr_slope"),
         pytest.param(
-            f.regr_intercept, pa.array([0], type=pa.float64()), 
id="regr_intercept"
+            f.regr_slope(column("c2"), column("c1"), filter=column("c1") > 
literal(2)),
+            [8],
+            id="regr_slope_filter",
+        ),
+        pytest.param(
+            f.regr_intercept(column("c2"), column("c1")), [-4], 
id="regr_intercept"
+        ),
+        pytest.param(
+            f.regr_intercept(
+                column("c2"), column("c1"), filter=column("c1") > literal(2)
+            ),
+            [-16],
+            id="regr_intercept_filter",
+        ),
+        pytest.param(f.regr_count(column("c2"), column("c1")), [4], 
id="regr_count"),
+        pytest.param(
+            f.regr_count(column("c2"), column("c1"), filter=column("c1") > 
literal(2)),
+            [2],
+            id="regr_count_filter",
+        ),
+        pytest.param(f.regr_r2(column("c2"), column("c1")), [0.92], 
id="regr_r2"),
+        pytest.param(
+            f.regr_r2(column("c2"), column("c1"), filter=column("c1") > 
literal(2)),
+            [1.0],
+            id="regr_r2_filter",
+        ),
+        pytest.param(f.regr_avgx(column("c2"), column("c1")), [2.5], 
id="regr_avgx"),
+        pytest.param(
+            f.regr_avgx(column("c2"), column("c1"), filter=column("c1") > 
literal(2)),
+            [3.5],
+            id="regr_avgx_filter",
+        ),
+        pytest.param(f.regr_avgy(column("c2"), column("c1")), [7.5], 
id="regr_avgy"),
+        pytest.param(
+            f.regr_avgy(column("c2"), column("c1"), filter=column("c1") > 
literal(2)),
+            [12.0],
+            id="regr_avgy_filter",
+        ),
+        pytest.param(f.regr_sxx(column("c2"), column("c1")), [5.0], 
id="regr_sxx"),
+        pytest.param(
+            f.regr_sxx(column("c2"), column("c1"), filter=column("c1") > 
literal(2)),
+            [0.5],
+            id="regr_sxx_filter",
+        ),
+        pytest.param(f.regr_syy(column("c2"), column("c1")), [115.0], 
id="regr_syy"),
+        pytest.param(
+            f.regr_syy(column("c2"), column("c1"), filter=column("c1") > 
literal(2)),
+            [32.0],
+            id="regr_syy_filter",
+        ),
+        pytest.param(f.regr_sxy(column("c2"), column("c1")), [23.0], 
id="regr_sxy"),
+        pytest.param(
+            f.regr_sxy(column("c2"), column("c1"), filter=column("c1") > 
literal(2)),
+            [4.0],
+            id="regr_sxy_filter",
         ),
-        pytest.param(f.regr_count, pa.array([3], type=pa.uint64()), 
id="regr_count"),
-        pytest.param(f.regr_r2, pa.array([1], type=pa.float64()), 
id="regr_r2"),
-        pytest.param(f.regr_avgx, pa.array([2], type=pa.float64()), 
id="regr_avgx"),
-        pytest.param(f.regr_avgy, pa.array([4], type=pa.float64()), 
id="regr_avgy"),
-        pytest.param(f.regr_sxx, pa.array([2], type=pa.float64()), 
id="regr_sxx"),
-        pytest.param(f.regr_syy, pa.array([8], type=pa.float64()), 
id="regr_syy"),
-        pytest.param(f.regr_sxy, pa.array([4], type=pa.float64()), 
id="regr_sxy"),
     ],
 )
 def test_regr_funcs_df(func, expected):
@@ -932,38 +979,18 @@ def test_regr_funcs_df(func, expected):
     ctx = SessionContext()
 
     # Create a DataFrame
-    data = {"column1": [1, 2, 3], "column2": [2, 4, 6]}
+    data = {"c1": [1, 2, 3, 4, 5, None], "c2": [2, 4, 8, 16, None, 64]}
     df = ctx.from_pydict(data, name="test_table")
 
     # Perform the regression function using DataFrame API
-    result_df = df.aggregate([], [func(f.col("column2"), 
f.col("column1"))]).collect()
-
-    # Assertion for DataFrame API result
-    assert result_df[0].column(0) == expected
-
+    df = df.aggregate([], [func.alias("result")])
+    df.show()
 
-def test_first_last_value(df):
-    df = df.aggregate(
-        [],
-        [
-            f.first_value(column("a")),
-            f.first_value(column("b")),
-            f.first_value(column("d")),
-            f.last_value(column("a")),
-            f.last_value(column("b")),
-            f.last_value(column("d")),
-        ],
-    )
+    expected_dict = {
+        "result": expected,
+    }
 
-    result = df.collect()
-    result = result[0]
-    assert result.column(0) == pa.array(["Hello"])
-    assert result.column(1) == pa.array([4])
-    assert result.column(2) == pa.array([datetime(2022, 12, 31)])
-    assert result.column(3) == pa.array(["!"])
-    assert result.column(4) == pa.array([6])
-    assert result.column(5) == pa.array([datetime(2020, 7, 2)])
-    df.show()
+    assert df.collect()[0].to_pydict() == expected_dict
 
 
 def test_binary_string_functions(df):
diff --git a/python/datafusion/tests/test_wrapper_coverage.py 
b/python/datafusion/tests/test_wrapper_coverage.py
index 44b9ca8..4a47de2 100644
--- a/python/datafusion/tests/test_wrapper_coverage.py
+++ b/python/datafusion/tests/test_wrapper_coverage.py
@@ -20,8 +20,19 @@ import datafusion.functions
 import datafusion.object_store
 import datafusion.substrait
 
+# EnumType introduced in 3.11. 3.10 and prior it was called EnumMeta.
+try:
+    from enum import EnumType
+except ImportError:
+    from enum import EnumMeta as EnumType
+
 
 def missing_exports(internal_obj, wrapped_obj) -> None:
+    # Special case enums - just make sure they exist since dir()
+    # and other functions get overridden.
+    if isinstance(wrapped_obj, EnumType):
+        return
+
     for attr in dir(internal_obj):
         assert attr in dir(wrapped_obj)
 
diff --git a/src/functions.rs b/src/functions.rs
index b5b003d..b9ca630 100644
--- a/src/functions.rs
+++ b/src/functions.rs
@@ -35,305 +35,27 @@ use datafusion::functions_aggregate;
 use datafusion::logical_expr::expr::Alias;
 use datafusion::logical_expr::sqlparser::ast::NullTreatment as DFNullTreatment;
 use datafusion::logical_expr::{
-    expr::{find_df_window_func, AggregateFunction, Sort, WindowFunction},
+    expr::{find_df_window_func, Sort, WindowFunction},
     lit, Expr, WindowFunctionDefinition,
 };
 
-#[pyfunction]
-pub fn approx_distinct(expression: PyExpr) -> PyExpr {
-    functions_aggregate::expr_fn::approx_distinct(expression.expr).into()
-}
-
-#[pyfunction]
-pub fn approx_median(expression: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::approx_median(expression.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn approx_percentile_cont(
-    expression: PyExpr,
-    percentile: PyExpr,
-    distinct: bool,
-    num_centroids: Option<PyExpr>, // enforces optional arguments at the end, 
currently
-) -> PyResult<PyExpr> {
-    let args = if let Some(num_centroids) = num_centroids {
-        vec![expression.expr, percentile.expr, num_centroids.expr]
-    } else {
-        vec![expression.expr, percentile.expr]
-    };
-    let udaf = 
functions_aggregate::approx_percentile_cont::approx_percentile_cont_udaf();
-    let expr = udaf.call(args);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn approx_percentile_cont_with_weight(
-    expression: PyExpr,
-    weight: PyExpr,
-    percentile: PyExpr,
-    distinct: bool,
-) -> PyResult<PyExpr> {
-    let expr = 
functions_aggregate::expr_fn::approx_percentile_cont_with_weight(
-        expression.expr,
-        weight.expr,
-        percentile.expr,
-    );
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn avg(expression: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::avg(expression.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn bit_and(expr_x: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::bit_and(expr_x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn bit_or(expression: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::bit_or(expression.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn bit_xor(expression: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::bit_xor(expression.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn bool_and(expression: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::bool_and(expression.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn bool_or(expression: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::bool_or(expression.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn corr(y: PyExpr, x: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::corr(y.expr, x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn grouping(expression: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::grouping(expression.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn sum(args: PyExpr) -> PyExpr {
-    functions_aggregate::expr_fn::sum(args.expr).into()
-}
-
-#[pyfunction]
-pub fn covar_samp(y: PyExpr, x: PyExpr) -> PyExpr {
-    functions_aggregate::expr_fn::covar_samp(y.expr, x.expr).into()
-}
-
-#[pyfunction]
-pub fn covar_pop(y: PyExpr, x: PyExpr) -> PyExpr {
-    functions_aggregate::expr_fn::covar_pop(y.expr, x.expr).into()
-}
-
-#[pyfunction]
-pub fn median(arg: PyExpr) -> PyExpr {
-    functions_aggregate::expr_fn::median(arg.expr).into()
-}
-
-#[pyfunction]
-pub fn stddev(expression: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::stddev(expression.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn stddev_pop(expression: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::stddev_pop(expression.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn var_samp(expression: PyExpr) -> PyExpr {
-    functions_aggregate::expr_fn::var_sample(expression.expr).into()
-}
-
-#[pyfunction]
-pub fn var_pop(expression: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::var_pop(expression.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn regr_avgx(expr_y: PyExpr, expr_x: PyExpr, distinct: bool) -> 
PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::regr_avgx(expr_y.expr, 
expr_x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn regr_avgy(expr_y: PyExpr, expr_x: PyExpr, distinct: bool) -> 
PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::regr_avgy(expr_y.expr, 
expr_x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn regr_count(expr_y: PyExpr, expr_x: PyExpr, distinct: bool) -> 
PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::regr_count(expr_y.expr, 
expr_x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn regr_intercept(expr_y: PyExpr, expr_x: PyExpr, distinct: bool) -> 
PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::regr_intercept(expr_y.expr, 
expr_x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn regr_r2(expr_y: PyExpr, expr_x: PyExpr, distinct: bool) -> 
PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::regr_r2(expr_y.expr, expr_x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn regr_slope(expr_y: PyExpr, expr_x: PyExpr, distinct: bool) -> 
PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::regr_slope(expr_y.expr, 
expr_x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn regr_sxx(expr_y: PyExpr, expr_x: PyExpr, distinct: bool) -> 
PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::regr_sxx(expr_y.expr, 
expr_x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn regr_sxy(expr_y: PyExpr, expr_x: PyExpr, distinct: bool) -> 
PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::regr_sxy(expr_y.expr, 
expr_x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
-#[pyfunction]
-pub fn regr_syy(expr_y: PyExpr, expr_x: PyExpr, distinct: bool) -> 
PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::regr_syy(expr_y.expr, 
expr_x.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
 fn add_builder_fns_to_aggregate(
     agg_fn: Expr,
-    distinct: bool,
+    distinct: Option<bool>,
     filter: Option<PyExpr>,
     order_by: Option<Vec<PyExpr>>,
     null_treatment: Option<NullTreatment>,
 ) -> PyResult<PyExpr> {
     // Since ExprFuncBuilder::new() is private, we can guarantee initializing
-    // a builder with an `order_by` default of empty vec
-    let order_by = order_by
-        .map(|x| x.into_iter().map(|x| x.expr).collect::<Vec<_>>())
-        .unwrap_or_default();
-    let mut builder = agg_fn.order_by(order_by);
+    // a builder with an `null_treatment` with option None
+    let mut builder = agg_fn.null_treatment(None);
 
-    if distinct {
+    if let Some(order_by_cols) = order_by {
+        let order_by_cols = to_sort_expressions(order_by_cols);
+        builder = builder.order_by(order_by_cols);
+    }
+
+    if let Some(true) = distinct {
         builder = builder.distinct();
     }
 
@@ -341,39 +63,11 @@ fn add_builder_fns_to_aggregate(
         builder = builder.filter(filter.expr);
     }
 
-    // would be nice if all the options builder methods accepted Option<T> ...
     builder = 
builder.null_treatment(null_treatment.map(DFNullTreatment::from));
 
     Ok(builder.build()?.into())
 }
 
-#[pyfunction]
-pub fn first_value(
-    expr: PyExpr,
-    distinct: bool,
-    filter: Option<PyExpr>,
-    order_by: Option<Vec<PyExpr>>,
-    null_treatment: Option<NullTreatment>,
-) -> PyResult<PyExpr> {
-    // If we initialize the UDAF with order_by directly, then it gets 
over-written by the builder
-    let agg_fn = functions_aggregate::expr_fn::first_value(expr.expr, None);
-
-    add_builder_fns_to_aggregate(agg_fn, distinct, filter, order_by, 
null_treatment)
-}
-
-#[pyfunction]
-pub fn last_value(
-    expr: PyExpr,
-    distinct: bool,
-    filter: Option<PyExpr>,
-    order_by: Option<Vec<PyExpr>>,
-    null_treatment: Option<NullTreatment>,
-) -> PyResult<PyExpr> {
-    let agg_fn = functions_aggregate::expr_fn::last_value(vec![expr.expr]);
-
-    add_builder_fns_to_aggregate(agg_fn, distinct, filter, order_by, 
null_treatment)
-}
-
 #[pyfunction]
 fn in_list(expr: PyExpr, value: Vec<PyExpr>, negated: bool) -> PyExpr {
     datafusion::logical_expr::in_list(
@@ -505,25 +199,6 @@ fn col(name: &str) -> PyResult<PyExpr> {
     })
 }
 
-// TODO: should we just expose this in python?
-/// Create a COUNT(1) aggregate expression
-#[pyfunction]
-fn count_star() -> PyExpr {
-    functions_aggregate::expr_fn::count(lit(1)).into()
-}
-
-/// Wrapper for [`functions_aggregate::expr_fn::count`]
-/// Count the number of non-null values in the column
-#[pyfunction]
-fn count(expr: PyExpr, distinct: bool) -> PyResult<PyExpr> {
-    let expr = functions_aggregate::expr_fn::count(expr.expr);
-    if distinct {
-        Ok(expr.distinct().build()?.into())
-    } else {
-        Ok(expr.into())
-    }
-}
-
 /// Create a CASE WHEN statement with literal WHEN expressions for comparison 
to the base expression.
 #[pyfunction]
 fn case(expr: PyExpr) -> PyResult<PyCaseBuilder> {
@@ -646,24 +321,46 @@ fn window(
     })
 }
 
+// Generates a [pyo3] wrapper for associated aggregate functions.
+// All of the builder options are exposed to the python internal
+// function and we rely on the wrappers to only use those that
+// are appropriate.
 macro_rules! aggregate_function {
-    ($NAME: ident, $FUNC: path) => {
-        aggregate_function!($NAME, $FUNC, stringify!($NAME));
+    ($NAME: ident) => {
+        aggregate_function!($NAME, expr);
     };
-    ($NAME: ident, $FUNC: path, $DOC: expr) => {
-        #[doc = $DOC]
+    ($NAME: ident, $($arg:ident)*) => {
         #[pyfunction]
-        #[pyo3(signature = (*args, distinct=false))]
-        fn $NAME(args: Vec<PyExpr>, distinct: bool) -> PyExpr {
-            let expr = 
datafusion::logical_expr::Expr::AggregateFunction(AggregateFunction {
-                func: $FUNC(),
-                args: args.into_iter().map(|e| e.into()).collect(),
-                distinct,
-                filter: None,
-                order_by: None,
-                null_treatment: None,
-            });
-            expr.into()
+        fn $NAME(
+            $($arg: PyExpr),*,
+            distinct: Option<bool>,
+            filter: Option<PyExpr>,
+            order_by: Option<Vec<PyExpr>>,
+            null_treatment: Option<NullTreatment>
+        ) -> PyResult<PyExpr> {
+            let agg_fn = functions_aggregate::expr_fn::$NAME($($arg.into()),*);
+
+            add_builder_fns_to_aggregate(agg_fn, distinct, filter, order_by, 
null_treatment)
+        }
+    };
+}
+
+macro_rules! aggregate_function_vec_args {
+    ($NAME: ident) => {
+        aggregate_function_vec_args!($NAME, expr);
+    };
+    ($NAME: ident, $($arg:ident)*) => {
+        #[pyfunction]
+        fn $NAME(
+            $($arg: PyExpr),*,
+            distinct: Option<bool>,
+            filter: Option<PyExpr>,
+            order_by: Option<Vec<PyExpr>>,
+            null_treatment: Option<NullTreatment>
+        ) -> PyResult<PyExpr> {
+            let agg_fn = 
functions_aggregate::expr_fn::$NAME(vec![$($arg.into()),*]);
+
+            add_builder_fns_to_aggregate(agg_fn, distinct, filter, order_by, 
null_treatment)
         }
     };
 }
@@ -891,9 +588,120 @@ array_fn!(array_resize, array size value);
 array_fn!(flatten, array);
 array_fn!(range, start stop step);
 
-aggregate_function!(array_agg, functions_aggregate::array_agg::array_agg_udaf);
-aggregate_function!(max, functions_aggregate::min_max::max_udaf);
-aggregate_function!(min, functions_aggregate::min_max::min_udaf);
+aggregate_function!(array_agg);
+aggregate_function!(max);
+aggregate_function!(min);
+aggregate_function!(avg);
+aggregate_function!(sum);
+aggregate_function!(bit_and);
+aggregate_function!(bit_or);
+aggregate_function!(bit_xor);
+aggregate_function!(bool_and);
+aggregate_function!(bool_or);
+aggregate_function!(corr, y x);
+aggregate_function!(count);
+aggregate_function!(covar_samp, y x);
+aggregate_function!(covar_pop, y x);
+aggregate_function!(median);
+aggregate_function!(regr_slope, y x);
+aggregate_function!(regr_intercept, y x);
+aggregate_function!(regr_count, y x);
+aggregate_function!(regr_r2, y x);
+aggregate_function!(regr_avgx, y x);
+aggregate_function!(regr_avgy, y x);
+aggregate_function!(regr_sxx, y x);
+aggregate_function!(regr_syy, y x);
+aggregate_function!(regr_sxy, y x);
+aggregate_function!(stddev);
+aggregate_function!(stddev_pop);
+aggregate_function!(var_sample);
+aggregate_function!(var_pop);
+aggregate_function!(approx_distinct);
+aggregate_function!(approx_median);
+
+// Code is commented out since grouping is not yet implemented
+// https://github.com/apache/datafusion-python/issues/861
+// aggregate_function!(grouping);
+
+#[pyfunction]
+pub fn approx_percentile_cont(
+    expression: PyExpr,
+    percentile: f64,
+    num_centroids: Option<i64>, // enforces optional arguments at the end, 
currently
+    filter: Option<PyExpr>,
+) -> PyResult<PyExpr> {
+    let args = if let Some(num_centroids) = num_centroids {
+        vec![expression.expr, lit(percentile), lit(num_centroids)]
+    } else {
+        vec![expression.expr, lit(percentile)]
+    };
+    let udaf = 
functions_aggregate::approx_percentile_cont::approx_percentile_cont_udaf();
+    let agg_fn = udaf.call(args);
+
+    add_builder_fns_to_aggregate(agg_fn, None, filter, None, None)
+}
+
+#[pyfunction]
+pub fn approx_percentile_cont_with_weight(
+    expression: PyExpr,
+    weight: PyExpr,
+    percentile: f64,
+    filter: Option<PyExpr>,
+) -> PyResult<PyExpr> {
+    let agg_fn = 
functions_aggregate::expr_fn::approx_percentile_cont_with_weight(
+        expression.expr,
+        weight.expr,
+        lit(percentile),
+    );
+
+    add_builder_fns_to_aggregate(agg_fn, None, filter, None, None)
+}
+
+aggregate_function_vec_args!(last_value);
+
+// We handle first_value explicitly because the signature expects an order_by
+// https://github.com/apache/datafusion/issues/12376
+#[pyfunction]
+pub fn first_value(
+    expr: PyExpr,
+    distinct: Option<bool>,
+    filter: Option<PyExpr>,
+    order_by: Option<Vec<PyExpr>>,
+    null_treatment: Option<NullTreatment>,
+) -> PyResult<PyExpr> {
+    // If we initialize the UDAF with order_by directly, then it gets 
over-written by the builder
+    let agg_fn = functions_aggregate::expr_fn::first_value(expr.expr, None);
+
+    add_builder_fns_to_aggregate(agg_fn, distinct, filter, order_by, 
null_treatment)
+}
+
+// nth_value requires a non-expr argument
+#[pyfunction]
+pub fn nth_value(
+    expr: PyExpr,
+    n: i64,
+    distinct: Option<bool>,
+    filter: Option<PyExpr>,
+    order_by: Option<Vec<PyExpr>>,
+    null_treatment: Option<NullTreatment>,
+) -> PyResult<PyExpr> {
+    let agg_fn = 
datafusion::functions_aggregate::nth_value::nth_value(vec![expr.expr, lit(n)]);
+    add_builder_fns_to_aggregate(agg_fn, distinct, filter, order_by, 
null_treatment)
+}
+
+// string_agg requires a non-expr argument
+#[pyfunction]
+pub fn string_agg(
+    expr: PyExpr,
+    delimiter: String,
+    distinct: Option<bool>,
+    filter: Option<PyExpr>,
+    order_by: Option<Vec<PyExpr>>,
+    null_treatment: Option<NullTreatment>,
+) -> PyResult<PyExpr> {
+    let agg_fn = 
datafusion::functions_aggregate::string_agg::string_agg(expr.expr, 
lit(delimiter));
+    add_builder_fns_to_aggregate(agg_fn, distinct, filter, order_by, 
null_treatment)
+}
 
 fn add_builder_fns_to_window(
     window_fn: Expr,
@@ -1042,7 +850,6 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> 
PyResult<()> {
     m.add_wrapped(wrap_pyfunction!(cosh))?;
     m.add_wrapped(wrap_pyfunction!(cot))?;
     m.add_wrapped(wrap_pyfunction!(count))?;
-    m.add_wrapped(wrap_pyfunction!(count_star))?;
     m.add_wrapped(wrap_pyfunction!(covar_pop))?;
     m.add_wrapped(wrap_pyfunction!(covar_samp))?;
     m.add_wrapped(wrap_pyfunction!(current_date))?;
@@ -1059,7 +866,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> 
PyResult<()> {
     m.add_wrapped(wrap_pyfunction!(floor))?;
     m.add_wrapped(wrap_pyfunction!(from_unixtime))?;
     m.add_wrapped(wrap_pyfunction!(gcd))?;
-    m.add_wrapped(wrap_pyfunction!(grouping))?;
+    // m.add_wrapped(wrap_pyfunction!(grouping))?;
     m.add_wrapped(wrap_pyfunction!(in_list))?;
     m.add_wrapped(wrap_pyfunction!(initcap))?;
     m.add_wrapped(wrap_pyfunction!(isnan))?;
@@ -1113,6 +920,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> 
PyResult<()> {
     m.add_wrapped(wrap_pyfunction!(starts_with))?;
     m.add_wrapped(wrap_pyfunction!(stddev))?;
     m.add_wrapped(wrap_pyfunction!(stddev_pop))?;
+    m.add_wrapped(wrap_pyfunction!(string_agg))?;
     m.add_wrapped(wrap_pyfunction!(strpos))?;
     m.add_wrapped(wrap_pyfunction!(r#struct))?; // Use raw identifier since 
struct is a keyword
     m.add_wrapped(wrap_pyfunction!(substr))?;
@@ -1134,7 +942,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> 
PyResult<()> {
     m.add_wrapped(wrap_pyfunction!(upper))?;
     m.add_wrapped(wrap_pyfunction!(self::uuid))?; // Use self to avoid name 
collision
     m.add_wrapped(wrap_pyfunction!(var_pop))?;
-    m.add_wrapped(wrap_pyfunction!(var_samp))?;
+    m.add_wrapped(wrap_pyfunction!(var_sample))?;
     m.add_wrapped(wrap_pyfunction!(window))?;
     m.add_wrapped(wrap_pyfunction!(regr_avgx))?;
     m.add_wrapped(wrap_pyfunction!(regr_avgy))?;
@@ -1147,6 +955,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> 
PyResult<()> {
     m.add_wrapped(wrap_pyfunction!(regr_syy))?;
     m.add_wrapped(wrap_pyfunction!(first_value))?;
     m.add_wrapped(wrap_pyfunction!(last_value))?;
+    m.add_wrapped(wrap_pyfunction!(nth_value))?;
     m.add_wrapped(wrap_pyfunction!(bit_and))?;
     m.add_wrapped(wrap_pyfunction!(bit_or))?;
     m.add_wrapped(wrap_pyfunction!(bit_xor))?;


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


Reply via email to