Repository: modin-project/modin

Issue:
Implement `pandas.DataFrame.nunique` at OmniSci backend
`nunique` is a distinct count in the sql notation, which is implemented in OmniSci itself, so only supporting in Modin is required.


Discussion:

Resolving patch:
diff --git a/modin/experimental/backends/omnisci/query_compiler.py b/modin/experimental/backends/omnisci/query_compiler.py
--- a/modin/experimental/backends/omnisci/query_compiler.py
+++ b/modin/experimental/backends/omnisci/query_compiler.py
@@ -296,20 +296,60 @@ def min(self, **kwargs):
         return self._agg("min", **kwargs)
 
     def sum(self, **kwargs):
+        min_count = kwargs.pop("min_count")
+        if min_count != 0:
+            return super().sum(
+                min_count=min_count,
+                **kwargs,
+            )
         return self._agg("sum", **kwargs)
 
     def mean(self, **kwargs):
         return self._agg("mean", **kwargs)
 
+    def nunique(self, axis=0, dropna=True):
+        if axis != 0 or not dropna:
+            return super().nunique(axis=axis, dropna=dropna)
+        return self._agg("count", distinct=True)
+
     def _agg(self, agg, axis=0, level=None, **kwargs):
+        """
+        Perform specified aggregation along rows/columns.
+
+        Parameters
+        ----------
+        agg : str
+            Name of the aggregation function to perform.
+        axis : {0, 1}, default: 0
+            Axis to perform aggregation along. 0 is to apply function against each column,
+            all the columns will be reduced into a single scalar. 1 is to aggregate
+            across rows.
+            *Note:* OmniSci backend supports aggregation for 0 axis only, aggregation
+            along rows will be defaulted to pandas.
+        level : None
+            Serves the compatibility purpose, always have to be None.
+        **kwargs : dict
+            Additional parameters to pass to the aggregation function.
+
+        Returns
+        -------
+        DFAlgQueryCompiler
+            New single-column (``axis=1``) or single-row (``axis=0``) query compiler containing
+            the result of aggregation.
+        """
         if level is not None or axis != 0:
             return getattr(super(), agg)(axis=axis, level=level, **kwargs)
 
-        skipna = kwargs.get("skipna", True)
-        if skipna is False:
+        # TODO: Do filtering on numeric columns if `numeric_only=True`
+        if (
+            kwargs.get("skipna", True) is not None and not kwargs.get("skipna", True)
+        ) or kwargs.get("numeric_only"):
             return getattr(super(), agg)(axis=axis, level=level, **kwargs)
+        # Processed above, so can be omitted
+        kwargs.pop("skipna", None)
+        kwargs.pop("numeric_only", None)
 
-        new_frame = self._modin_frame.agg(agg)
+        new_frame = self._modin_frame.agg(agg, **kwargs)
         new_frame = new_frame._set_index(
             pandas.Index.__new__(pandas.Index, data=["__reduced__"], dtype="O")
         )
diff --git a/modin/experimental/engines/omnisci_on_ray/frame/data.py b/modin/experimental/engines/omnisci_on_ray/frame/data.py
--- a/modin/experimental/engines/omnisci_on_ray/frame/data.py
+++ b/modin/experimental/engines/omnisci_on_ray/frame/data.py
@@ -323,12 +323,27 @@ def groupby_agg(self, by, axis, agg, groupby_args, **kwargs):
 
         return new_frame
 
-    def agg(self, agg):
+    def agg(self, agg, **kwargs):
+        """
+        Perform specified aggregation along columns.
+
+        Parameters
+        ----------
+        agg : str
+            Name of the aggregation function to perform.
+        **kwargs : dict
+            Additional parameters to pass to the aggregation expression.
+
+        Returns
+        -------
+        OmnisciOnRayFrame
+            New frame containing the result of aggregation.
+        """
         assert isinstance(agg, str)
 
         agg_exprs = OrderedDict()
         for col in self.columns:
-            agg_exprs[col] = AggregateExpr(agg, self.ref(col))
+            agg_exprs[col] = AggregateExpr(agg, self.ref(col), **kwargs)
 
         return self.__constructor__(
             columns=self.columns,


Test patch:
diff --git a/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py b/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py
--- a/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py
+++ b/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py
@@ -1081,6 +1081,7 @@ class TestAgg:
         "c": [None, 200, None, 400, 500, 600],
         "d": [11, 22, 33, 22, 33, 22],
     }
+    int_data = pandas.DataFrame(data).fillna(0).astype("int").to_dict()
 
     @pytest.mark.parametrize("agg", ["max", "min", "sum", "mean"])
     @pytest.mark.parametrize("skipna", bool_arg_values)
@@ -1113,7 +1114,9 @@ def value_counts(df, cols, dropna, sort, ascending, **kwargs):
             ascending=ascending,
         )
 
-    @pytest.mark.parametrize("method", ["sum", "mean", "max", "min", "count"])
+    @pytest.mark.parametrize(
+        "method", ["sum", "mean", "max", "min", "count", "nunique"]
+    )
     def test_simple_agg_no_default(self, method):
         def applier(df, **kwargs):
             if isinstance(df, pd.DataFrame):
@@ -1135,6 +1138,14 @@ def applier(df, **kwargs):
 
         run_and_compare(applier, data=self.data, force_lazy=False)
 
+    @pytest.mark.parametrize("data", [data, int_data])
+    @pytest.mark.parametrize("dropna", bool_arg_values)
+    def test_nunique(self, data, dropna):
+        def applier(df, **kwargs):
+            return df.nunique(dropna=dropna)
+
+        run_and_compare(applier, data=data, force_lazy=False)
+
 
 class TestMerge:
     data = {