Repository: modin-project/modin
Issue: DataFrame.apply fails with function passed as string and added function parameters

### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Linux Ubuntu 18.04
- **Modin version** (`modin.__version__`): 0.7.3+153.g4f95b41
- **Python version**: 3.8.3
- **Code we can use to reproduce**:
```
import pandas
import modin.pandas as pd
import numpy as np

rows_number = 20

data = {"col1": np.random.randint(0, 100, rows_number), "col2": np.random.randint(0, 100, rows_number)}
level_0 = np.random.choice([0, 1], rows_number)
level_1 = np.random.choice([2, 3], rows_number)
index = pd.MultiIndex.from_arrays([level_0, level_1])

func_kwargs = {"level": 0}
df_modin = pd.DataFrame(data, index=index)
df_pandas = pandas.DataFrame(data, index=index)

pandas_output = df_pandas.apply("count", **func_kwargs)
print("pandas_output:\\n", pandas_output)

modin_output = df_modin.apply("count", **func_kwargs)
print("modin_output:\\n", modin_output)
```

### Describe the problem
Reproducer raises exception:
Traceback (most recent call last):
  File "apply_reproducer.py", line 19, in <module>
    modin_output = df_modin.apply("count", **func_kwargs)
  File "/modin/modin/pandas/dataframe.py", line 375, in apply
    if axis == 0 and result.name == self.index[0]:
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

### Source code / logs



Changed files:
modin/pandas/dataframe.py, modin/backends/pandas/query_compiler.py, modin/pandas/test/test_dataframe.py, modin/pandas/base.py, modin/pandas/series.py

diff --git a/modin/backends/pandas/query_compiler.py b/modin/backends/pandas/query_compiler.py
index 0487e23a5..216ba62ef 100644
--- a/modin/backends/pandas/query_compiler.py
+++ b/modin/backends/pandas/query_compiler.py
@@ -1410,7 +1410,9 @@ class PandasQueryCompiler(BaseQueryCompiler):
         Returns:
             A new PandasQueryCompiler.
         """
-        if callable(func):
+        if isinstance(func, str):
+            return self._apply_text_func_elementwise(func, axis, *args, **kwargs)
+        elif callable(func):
             return self._callable_func(func, axis, *args, **kwargs)
         elif isinstance(func, dict):
             return self._dict_func(func, axis, *args, **kwargs)
@@ -1419,6 +1421,23 @@ class PandasQueryCompiler(BaseQueryCompiler):
         else:
             pass
 
+    def _apply_text_func_elementwise(self, func, axis, *args, **kwargs):
+        """Apply func passed as str across given axis in elementwise manner.
+
+        Args:
+            func: The function to apply.
+            axis: Target axis to apply the function along.
+
+        Returns:
+            A new PandasQueryCompiler.
+        """
+        assert isinstance(func, str)
+        kwargs["axis"] = axis
+        new_modin_frame = self._modin_frame._apply_full_axis(
+            axis, lambda df: getattr(df, func)(**kwargs)
+        )
+        return self.__constructor__(new_modin_frame)
+
     def _dict_func(self, func, axis, *args, **kwargs):
         """Apply function to certain indices across given axis.
 
diff --git a/modin/pandas/base.py b/modin/pandas/base.py
index 14a03dd0a..6b1445ea5 100644
--- a/modin/pandas/base.py
+++ b/modin/pandas/base.py
@@ -577,10 +577,7 @@ class BasePandasDataset(object):
         axis = self._get_axis_number(axis)
         ErrorMessage.non_verified_udf()
         if isinstance(func, str):
-            if axis == 1:
-                kwds["axis"] = axis
-            result = self._string_function(func, *args, **kwds)
-            # Sometimes we can return a scalar here
+            result = self._query_compiler.apply(func, axis=axis, *args, **kwds)
             if isinstance(result, BasePandasDataset):
                 return result._query_compiler
             return result
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
index 0267195aa..4e851b497 100644
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -360,7 +360,7 @@ class DataFrame(BasePandasDataset):
                 init_kwargs = {"columns": self.columns}
             return_type = type(
                 getattr(pandas, self.__name__)(**init_kwargs).apply(
-                    func, axis=axis, raw=raw, result_type=result_type,
+                    func, axis=axis, raw=raw, result_type=result_type, args=args, **kwds
                 )
             ).__name__
         except Exception:
@@ -372,9 +372,9 @@ class DataFrame(BasePandasDataset):
                 query_compiler=query_compiler
             )
             if isinstance(result, Series):
-                if axis == 0 and result.name == self.index[0]:
+                if axis == 0 and result.name == self.index[0] or result.name == 0:
                     result.name = None
-                elif axis == 1 and result.name == self.columns[0]:
+                elif axis == 1 and result.name == self.columns[0] or result.name == 0:
                     result.name = None
             return result
 
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
index fea7b6d72..22147f17b 100644
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -493,9 +493,6 @@ class Series(BasePandasDataset):
             or return_type not in ["DataFrame", "Series"]
         ):
             query_compiler = super(Series, self).apply(func, *args, **kwds)
-            # Sometimes we can return a scalar here
-            if not isinstance(query_compiler, type(self._query_compiler)):
-                return query_compiler
         else:
             # handle ufuncs and lambdas
             if kwds or args and not isinstance(func, np.ufunc):
diff --git a/modin/pandas/test/test_dataframe.py b/modin/pandas/test/test_dataframe.py
index cdec8d5d5..e8d81c010 100644
--- a/modin/pandas/test/test_dataframe.py
+++ b/modin/pandas/test/test_dataframe.py
@@ -1727,6 +1727,24 @@ class TestDataFrameUDF:
             modin_result = modin_df.apply(func, axis)
             df_equals(modin_result, pandas_result)
 
+    @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"])
+    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
+        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])
+
+        eval_general(
+            pd.DataFrame(data, index=index),
+            pandas.DataFrame(data, index=index),
+            lambda df, *args, **kwargs: df.apply(func, *args, **kwargs),
+            **func_kwargs,
+        )
+
     @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
     @pytest.mark.parametrize("axis", axis_values, ids=axis_keys)
     def test_apply_args(self, data, axis):