Repository: modin-project/modin

Issue:
ASV: Modin on Ray fillna benchmark should use column names instead of indexes
Related to #3246.

For details see https://github.com/modin-project/modin/issues/3246#issuecomment-881278686



Discussion:

Resolving patch:
diff --git a/asv_bench/benchmarks/benchmarks.py b/asv_bench/benchmarks/benchmarks.py
--- a/asv_bench/benchmarks/benchmarks.py
+++ b/asv_bench/benchmarks/benchmarks.py
@@ -24,6 +24,7 @@
 
 from .utils import (
     generate_dataframe,
+    gen_nan_data,
     RAND_LOW,
     RAND_HIGH,
     random_string,
@@ -33,6 +34,7 @@
     ASV_DATASET_SIZE,
     BINARY_OP_DATA_SIZE,
     UNARY_OP_DATA_SIZE,
+    SERIES_DATA_SIZE,
     GROUPBY_NGROUPS,
     IMPL,
     execute,
@@ -376,38 +378,61 @@ def time_head(self, shape, head_count):
         execute(self.df.head(self.head_count))
 
 
-class TimeFillna:
-    param_names = ["self_type", "value_type", "shape", "limit", "inplace"]
+class TimeFillnaSeries:
+    param_names = ["value_type", "shape", "limit"]
     params = [
-        ["DataFrame", "Series"],
-        ["scalar", "dict", "DataFrame", "Series"],
-        UNARY_OP_DATA_SIZE[ASV_DATASET_SIZE],
+        ["scalar", "dict", "Series"],
+        SERIES_DATA_SIZE[ASV_DATASET_SIZE],
         [None, 0.8],
-        [False, True],
     ]
 
-    def setup(self, self_type, value_type, shape, limit, inplace):
+    def setup(self, value_type, shape, limit):
         pd = IMPL[ASV_USE_IMPL]
-        columns = [f"col{x}" for x in range(shape[1])]
-        if self_type == "DataFrame":
-            self.dataset = pd.DataFrame(
-                np.nan, index=pd.RangeIndex(shape[0]), columns=columns
+        self.series = gen_nan_data(ASV_USE_IMPL, *shape)
+
+        if value_type == "scalar":
+            self.value = 18.19
+        elif value_type == "dict":
+            self.value = {k: k * 1.23 for k in range(shape[0])}
+        elif value_type == "Series":
+            self.value = pd.Series(
+                [k * 1.23 for k in range(shape[0])], index=pd.RangeIndex(shape[0])
             )
-        elif self_type == "Series":
-            self.dataset = pd.Series(np.nan, index=pd.RangeIndex(shape[0]))
         else:
             assert False
+        limit = int(limit * shape[0]) if limit else None
+        self.kw = {"value": self.value, "limit": limit}
+
+    def time_fillna(self, value_type, shape, limit):
+        execute(self.series.fillna(**self.kw))
+
+    def time_fillna_inplace(self, value_type, shape, limit):
+        self.series.fillna(inplace=True, **self.kw)
+        execute(self.series)
+
+
+class TimeFillnaDataFrame:
+    param_names = ["value_type", "shape", "limit"]
+    params = [
+        ["scalar", "dict", "DataFrame", "Series"],
+        UNARY_OP_DATA_SIZE[ASV_DATASET_SIZE],
+        [None, 0.8],
+    ]
+
+    def setup(self, value_type, shape, limit):
+        pd = IMPL[ASV_USE_IMPL]
+        self.df = gen_nan_data(ASV_USE_IMPL, *shape)
+        columns = self.df.columns
+
         if value_type == "scalar":
             self.value = 18.19
         elif value_type == "dict":
-            self.value = {k: k * 1.23 for k in range(shape[0])}
+            self.value = {k: i * 1.23 for i, k in enumerate(columns)}
         elif value_type == "Series":
             self.value = pd.Series(
-                [k * 1.23 for k in range(shape[0])], index=pd.RangeIndex(shape[0])
+                [i * 1.23 for i in range(len(columns))], index=columns
             )
         elif value_type == "DataFrame":
-            if self_type == "Series":
-                raise NotImplementedError
             self.value = pd.DataFrame(
                 {
                     k: [i + j * 1.23 for j in range(shape[0])]
@@ -418,15 +443,15 @@ def setup(self, self_type, value_type, shape, limit, inplace):
             )
         else:
             assert False
-        self.limit = int(limit * shape[0]) if limit else None
+        limit = int(limit * shape[0]) if limit else None
+        self.kw = {"value": self.value, "limit": limit}
 
-    def time_fillna(self, self_type, value_type, shape, limit, inplace):
-        kw = {"value": self.value, "limit": self.limit, "inplace": inplace}
-        if inplace:
-            self.dataset.fillna(**kw)
-            execute(self.dataset)
-        else:
-            execute(self.dataset.fillna(**kw))
+    def time_fillna(self, value_type, shape, limit):
+        execute(self.df.fillna(**self.kw))
+
+    def time_fillna_inplace(self, value_type, shape, limit):
+        self.df.fillna(inplace=True, **self.kw)
+        execute(self.df)
 
 
 class BaseTimeValueCounts:
diff --git a/asv_bench/benchmarks/omnisci/benchmarks.py b/asv_bench/benchmarks/omnisci/benchmarks.py
--- a/asv_bench/benchmarks/omnisci/benchmarks.py
+++ b/asv_bench/benchmarks/omnisci/benchmarks.py
@@ -13,10 +13,9 @@
 
 """General Modin on OmniSci backend benchmarks."""
 
-import numpy as np
-
 from ..utils import (
     generate_dataframe,
+    gen_nan_data,
     ASV_USE_IMPL,
     ASV_DATASET_SIZE,
     GROUPBY_NGROUPS,
@@ -233,9 +232,8 @@ class TimeFillna:
     ]
 
     def setup(self, value_type, shape, limit):
-        pd = IMPL[ASV_USE_IMPL]
-        columns = [f"col{x}" for x in range(shape[1])]
-        self.df = pd.DataFrame(np.nan, index=pd.RangeIndex(shape[0]), columns=columns)
+        self.df = gen_nan_data(ASV_USE_IMPL, *shape)
+        columns = self.df.columns
         trigger_import(self.df)
 
         value = self.create_fillna_value(value_type, columns)
diff --git a/asv_bench/benchmarks/utils.py b/asv_bench/benchmarks/utils.py
--- a/asv_bench/benchmarks/utils.py
+++ b/asv_bench/benchmarks/utils.py
@@ -92,6 +92,15 @@
     ],
 }
 
+SERIES_DATA_SIZE = {
+    "big": [
+        (100_000, 1),
+    ],
+    "small": [
+        (10_000, 1),
+    ],
+}
+
 GROUPBY_NGROUPS = {
     "big": [100, "huge_amount_groups"],
     "small": [5],
@@ -134,6 +143,47 @@ class weakdict(dict):  # noqa: GL08
 dataframes_cache = dict()
 
 
+def gen_nan_data(impl: str, nrows: int, ncols: int) -> dict:
+    """
+    Generate nan data with caching.
+
+    The generated data are saved in the dictionary and on a subsequent call,
+    if the keys match, saved data will be returned. Therefore, we need
+    to carefully monitor the changing of saved data and make its copy if needed.
+
+    Parameters
+    ----------
+    impl : str
+        Implementation used to create the DataFrame or Series;
+        supported implemetations: {"modin", "pandas"}.
+    nrows : int
+        Number of rows.
+    ncols : int
+        Number of columns.
+
+    Returns
+    -------
+    modin.pandas.DataFrame or pandas.DataFrame or modin.pandas.Series or pandas.Series
+        DataFrame or Series with shape (nrows, ncols) or (nrows,), respectively.
+    """
+    cache_key = (impl, nrows, ncols)
+    if cache_key in data_cache:
+        return data_cache[cache_key]
+
+    logging.info("Generating nan data {} rows and {} columns".format(nrows, ncols))
+
+    if ncols > 1:
+        columns = [f"col{x}" for x in range(ncols)]
+        data = IMPL[impl].DataFrame(np.nan, index=pd.RangeIndex(nrows), columns=columns)
+    elif ncols == 1:
+        data = IMPL[impl].Series(np.nan, index=pd.RangeIndex(nrows))
+    else:
+        assert False, "Number of columns (ncols) should be >= 1"
+
+    data_cache[cache_key] = data
+    return data
+
+
 def gen_int_data(nrows: int, ncols: int, rand_low: int, rand_high: int) -> dict:
     """
     Generate int data with caching.


Test patch: