Repository: modin-project/modin

Issue:
`Series.apply("array")` doesn't work if Series contains one element 
<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
<!-- Describe the problem clearly here. -->

The problem is occurred if we call method of `Series` that contains one element. It happens because of we use direct method call of inner df ([see example](https://github.com/modin-project/modin/pull/1657/files#diff-db8dc531e116caee2b9cc5ccbcc5c642R1437)). In this case `array` method will be called from `pandas.DataFrame`, but `pandas.DataFrame` doesn't support it. In order to call method from `Series` we should use `squeeze(axis=1)` for df. An example of exception is below:

### Source code / logs
<!-- Include any logs or source code that would be helpful to diagnose the problem. If including tracebacks, please include the full traceback. Large logs and files should be attached. Try to provide a reproducible test case that is the bare minimum necessary to generate the problem. -->
```python
import modin.pandas as pd
s = pd.Series([1])
s.apply("array")
AttributeError: 'DataFrame' object has no attribute 'array'
```


Discussion:

Resolving patch:
diff --git a/modin/pandas/base.py b/modin/pandas/base.py
--- a/modin/pandas/base.py
+++ b/modin/pandas/base.py
@@ -583,9 +583,11 @@ def apply(
         axis = self._get_axis_number(axis)
         ErrorMessage.non_verified_udf()
         if isinstance(func, str):
-            result = self._query_compiler.apply(
-                func, axis=axis, raw=raw, result_type=result_type, *args, **kwds,
-            )
+            # if axis != 1 function can be bounded to the Series, which doesn't
+            # support axis parameter
+            if axis == 1:
+                kwds["axis"] = axis
+            result = self._string_function(func, *args, **kwds)
             if isinstance(result, BasePandasDataset):
                 return result._query_compiler
             return result
@@ -741,7 +743,6 @@ def count(self, axis=0, level=None, numeric_only=False):
         Returns:
             The count, in a Series (or DataFrame if level is specified).
         """
-
         axis = self._get_axis_number(axis) if axis is not None else 0
         if numeric_only is not None and numeric_only:
             self._validate_dtypes(numeric_only=numeric_only)
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -494,22 +494,29 @@ def apply(self, func, convert_dtype=True, args=(), **kwds):
         # a list or a dictionary, which means that the return type won't change from
         # type(self), so we catch that error and use `type(self).__name__` for the return
         # type.
-        # Because a `Series` cannot be empty in pandas, we create a "dummy" `Series` to
-        # do the error checking and determining the return type.
+        # We create a "dummy" `Series` to do the error checking and determining
+        # the return type.
         try:
             return_type = type(
-                getattr(pandas.Series([""], index=self.index[:1]), apply_func)(
+                getattr(pandas.Series("", index=self.index[:1]), apply_func)(
                     func, *args, **kwds
                 )
             ).__name__
         except Exception:
-            return_type = type(self).__name__
+            try:
+                return_type = type(
+                    getattr(pandas.Series(0, index=self.index[:1]), apply_func)(
+                        func, *args, **kwds
+                    )
+                ).__name__
+            except Exception:
+                return_type = type(self).__name__
         if (
             isinstance(func, str)
             or is_list_like(func)
             or return_type not in ["DataFrame", "Series"]
         ):
-            query_compiler = super(Series, self).apply(func, *args, **kwds)
+            result = super(Series, self).apply(func, *args, **kwds)
         else:
             # handle ufuncs and lambdas
             if kwds or args and not isinstance(func, np.ufunc):
@@ -522,12 +529,17 @@ def f(x):
             with np.errstate(all="ignore"):
                 if isinstance(f, np.ufunc):
                     return f(self)
-                query_compiler = self.map(f)._query_compiler
+                result = self.map(f)._query_compiler
         if return_type not in ["DataFrame", "Series"]:
-            return query_compiler.to_pandas().squeeze()
+            # sometimes result can be not a query_compiler, but scalar (for example
+            # for sum or count functions)
+            if isinstance(result, type(self._query_compiler)):
+                return result.to_pandas().squeeze()
+            else:
+                return result
         else:
             result = getattr(sys.modules[self.__module__], return_type)(
-                query_compiler=query_compiler
+                query_compiler=result
             )
             if result.name == self.index[0]:
                 result.name = None


Test patch:
diff --git a/modin/pandas/test/test_dataframe.py b/modin/pandas/test/test_dataframe.py
--- a/modin/pandas/test/test_dataframe.py
+++ b/modin/pandas/test/test_dataframe.py
@@ -1739,7 +1739,36 @@ def test_apply(self, request, data, func, axis):
     @pytest.mark.parametrize("axis", [0, 1])
     @pytest.mark.parametrize("level", [None, -1, 0, 1])
     @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
-    @pytest.mark.parametrize("func", ["count", "sum", "mean", "all", "kurt"])
+    @pytest.mark.parametrize(
+        "func",
+        [
+            "kurt",
+            pytest.param(
+                "count",
+                marks=pytest.mark.xfail(
+                    reason="count method handle level parameter incorrectly"
+                ),
+            ),
+            pytest.param(
+                "sum",
+                marks=pytest.mark.xfail(
+                    reason="sum method handle level parameter incorrectly"
+                ),
+            ),
+            pytest.param(
+                "mean",
+                marks=pytest.mark.xfail(
+                    reason="mean method handle level parameter incorrectly"
+                ),
+            ),
+            pytest.param(
+                "all",
+                marks=pytest.mark.xfail(
+                    reason="all method handle level parameter incorrectly"
+                ),
+            ),
+        ],
+    )
     def test_apply_text_func_with_level(self, level, data, func, axis):
         func_kwargs = {"level": level, "axis": axis}
         rows_number = len(next(iter(data.values())))  # length of the first data column
diff --git a/modin/pandas/test/test_series.py b/modin/pandas/test/test_series.py
--- a/modin/pandas/test/test_series.py
+++ b/modin/pandas/test/test_series.py
@@ -777,6 +777,28 @@ def test_apply_numeric(request, data, func):
             df_equals(modin_result, pandas_result)
 
 
+@pytest.mark.parametrize("axis", [None, 0, 1])
+@pytest.mark.parametrize("level", [None, -1, 0, 1])
+@pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
+@pytest.mark.parametrize("func", ["count", "all", "kurt", "array", "searchsorted"])
+def test_apply_text_func(level, data, func, axis):
+    func_kwargs = {}
+    if level:
+        func_kwargs.update({"level": level})
+    if axis:
+        func_kwargs.update({"axis": axis})
+    rows_number = len(next(iter(data.values())))  # length of the first data column
+    level_0 = np.random.choice([0, 1, 2], rows_number)
+    level_1 = np.random.choice([3, 4, 5], rows_number)
+    index = pd.MultiIndex.from_arrays([level_0, level_1])
+
+    modin_series, pandas_series = create_test_series(data)
+    modin_series.index = index
+    pandas_series.index = index
+
+    eval_general(modin_series, pandas_series, lambda df: df.apply(func), **func_kwargs)
+
+
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 @pytest.mark.parametrize("skipna", [True, False])
 def test_argmax(data, skipna):