Repository: modin-project/modin

Issue:
Series.apply uses DataFrame.apply logic in incorrect order
<!--
General questions should be asked on the mailing list modin-dev@googlegroups.com.

Before submitting an issue, please fill out the following form.
-->

### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: OSX
- **Modin installed from (source or binary)**: source
- **Modin version**: master
- **Python version**: 3.6
- **Exact command to reproduce**: series.apply(str)

<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
`Series` and `DataFrame` differ on the ways that they apply functions. Modin is using the DataFrame logic always instead of checking if the function should apply element-wise.
<!-- Describe the problem clearly here. -->

### Source code / logs
```python
import modin.pandas as pd
import pandas
print(pd.Series([1,2,3,4]).apply(str))
print(pandas.Series([1,2,3,4]).apply(str))
```
<!-- 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. -->



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
@@ -299,11 +299,9 @@ def aggregate(self, func, axis=0, *args, **kwargs):
                 result = self._aggregate(func, _axis=axis, *args, **kwargs)
             except TypeError:
                 pass
-
         if result is None:
             kwargs.pop("is_transform", None)
             return self.apply(func, axis=axis, args=args, **kwargs)
-
         return result
 
     def _aggregate(self, arg, *args, **kwargs):
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -4,6 +4,7 @@
 
 import numpy as np
 import pandas
+from pandas.compat import string_types
 from pandas.core.common import is_bool_indexer
 from pandas.core.dtypes.common import is_dict_like, is_list_like, is_scalar
 import sys
@@ -357,12 +358,7 @@ def apply(self, func, convert_dtype=True, args=(), **kwds):
             apply_func = "agg"
         else:
             apply_func = "apply"
-            # Add this because it only applies for `apply` specifically.
-            kwds["convert_dtype"] = convert_dtype
-        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
+
         # This is the simplest way to determine the return type, but there are checks
         # in pandas that verify that some results are created. This is a challenge for
         # empty DataFrames, but fortunately they only happen when the `func` type is
@@ -374,6 +370,28 @@ def apply(self, func, convert_dtype=True, args=(), **kwds):
                 func, *args, **kwds
             )
         ).__name__
+        if (
+            isinstance(func, string_types)
+            or is_list_like(func)
+            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):
+
+                def f(x):
+                    return func(x, *args, **kwds)
+
+            else:
+                f = func
+            with np.errstate(all="ignore"):
+                if isinstance(f, np.ufunc):
+                    return f(self)
+                query_compiler = self.map(f)._query_compiler
         if return_type not in ["DataFrame", "Series"]:
             return query_compiler.to_pandas().squeeze()
         else:


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
@@ -44,6 +44,10 @@
     int_arg_values,
 )
 
+# TODO remove once modin-project/modin#469 is resolved
+agg_func_keys.remove("str")
+agg_func_values.remove(str)
+
 pd.DEFAULT_NPARTITIONS = 4
 
 # Force matplotlib to not use any Xwindows backend.
diff --git a/modin/pandas/test/utils.py b/modin/pandas/test/utils.py
--- a/modin/pandas/test/utils.py
+++ b/modin/pandas/test/utils.py
@@ -138,6 +138,7 @@
 agg_func = {
     "sum": "sum",
     "df sum": lambda df: df.sum(),
+    "str": str,
     "sum mean": ["sum", "mean"],
     "sum sum": ["sum", "sum"],
     "sum df sum": ["sum", lambda df: df.sum()],