Repository: databricks/koalas

Issue:
Series.replace
Implement `Series.replace`.

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.replace.html


Discussion:
@ueshin Can you explain a little bit about the issue?

It basically means that we should implement an API that works exactly same as pandas'. For instance see below:

```python
>>> import databricks.koalas as ks
>>> ks.Series([1, 2, 3]).replace()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../koalas/databricks/koalas/missing/__init__.py", line 24, in unsupported_function
    reason=reason)
databricks.koalas.exceptions.PandasNotImplementedError: The method `pd.Series.replace()` is not implemented yet.
```

```python
>>> import pandas as pd
>>> pd.Series([1, 2, 3]).replace()
0    1
1    2
2    3
dtype: int64
```
I'm working on this.

Resolving patch:
diff --git a/databricks/koalas/missing/series.py b/databricks/koalas/missing/series.py
--- a/databricks/koalas/missing/series.py
+++ b/databricks/koalas/missing/series.py
@@ -92,7 +92,6 @@ class _MissingPandasLikeSeries(object):
     rename_axis = unsupported_function('rename_axis')
     reorder_levels = unsupported_function('reorder_levels')
     repeat = unsupported_function('repeat')
-    replace = unsupported_function('replace')
     resample = unsupported_function('resample')
     searchsorted = unsupported_function('searchsorted')
     sem = unsupported_function('sem')
diff --git a/databricks/koalas/series.py b/databricks/koalas/series.py
--- a/databricks/koalas/series.py
+++ b/databricks/koalas/series.py
@@ -3227,6 +3227,188 @@ def copy(self) -> 'Series':
         """
         return _col(DataFrame(self._internal.copy()))
 
+    # TODO: 'regex', 'method' parameter
+    def replace(self, to_replace=None, value=None, regex=False) -> 'Series':
+        """
+        Replace values given in to_replace with value.
+        Values of the Series are replaced with other values dynamically.
+
+        Parameters
+        ----------
+        to_replace : str, list, dict, Series, int, float, or None
+            How to find the values that will be replaced.
+            * numeric, str:
+
+                - numeric: numeric values equal to to_replace will be replaced with value
+                - str: string exactly matching to_replace will be replaced with value
+
+            * list of str or numeric:
+
+                - if to_replace and value are both lists, they must be the same length.
+                - str and numeric rules apply as above.
+
+            * dict:
+
+                - Dicts can be used to specify different replacement values for different
+                  existing values.
+                  For example, {'a': 'b', 'y': 'z'} replaces the value ‘a’ with ‘b’ and ‘y’
+                  with ‘z’. To use a dict in this way the value parameter should be None.
+                - For a DataFrame a dict can specify that different values should be replaced
+                  in different columns. For example, {'a': 1, 'b': 'z'} looks for the value 1
+                  in column ‘a’ and the value ‘z’ in column ‘b’ and replaces these values with
+                  whatever is specified in value.
+                  The value parameter should not be None in this case.
+                  You can treat this as a special case of passing two lists except that you are
+                  specifying the column to search in.
+
+            See the examples section for examples of each of these.
+
+        value : scalar, dict, list, str default None
+            Value to replace any values matching to_replace with.
+            For a DataFrame a dict of values can be used to specify which value to use
+            for each column (columns not in the dict will not be filled).
+            Regular expressions, strings and lists or dicts of such objects are also allowed.
+
+        Returns
+        -------
+        Series
+            Object after replacement.
+
+        Examples
+        --------
+
+        Scalar `to_replace` and `value`
+
+        >>> s = ks.Series([0, 1, 2, 3, 4])
+        >>> s
+        0    0
+        1    1
+        2    2
+        3    3
+        4    4
+        Name: 0, dtype: int64
+
+        >>> s.replace(0, 5)
+        0    5
+        1    1
+        2    2
+        3    3
+        4    4
+        Name: 0, dtype: int64
+
+        List-like `to_replace`
+
+        >>> s.replace([0, 4], 5000)
+        0    5000
+        1       1
+        2       2
+        3       3
+        4    5000
+        Name: 0, dtype: int64
+
+        >>> s.replace([1, 2, 3], [10, 20, 30])
+        0     0
+        1    10
+        2    20
+        3    30
+        4     4
+        Name: 0, dtype: int64
+
+        Dict-like `to_replace`
+
+        >>> s.replace({1: 1000, 2: 2000, 3: 3000, 4: 4000})
+        0       0
+        1    1000
+        2    2000
+        3    3000
+        4    4000
+        Name: 0, dtype: int64
+
+        Also support for MultiIndex
+
+        >>> midx = pd.MultiIndex([['lama', 'cow', 'falcon'],
+        ...                       ['speed', 'weight', 'length']],
+        ...                      [[0, 0, 0, 1, 1, 1, 2, 2, 2],
+        ...                       [0, 1, 2, 0, 1, 2, 0, 1, 2]])
+        >>> s = ks.Series([45, 200, 1.2, 30, 250, 1.5, 320, 1, 0.3],
+        ...               index=midx)
+        >>> s
+        lama    speed      45.0
+                weight    200.0
+                length      1.2
+        cow     speed      30.0
+                weight    250.0
+                length      1.5
+        falcon  speed     320.0
+                weight      1.0
+                length      0.3
+        Name: 0, dtype: float64
+
+        >>> s.replace(45, 450)
+        lama    speed     450.0
+                weight    200.0
+                length      1.2
+        cow     speed      30.0
+                weight    250.0
+                length      1.5
+        falcon  speed     320.0
+                weight      1.0
+                length      0.3
+        Name: 0, dtype: float64
+
+        >>> s.replace([45, 30, 320], 500)
+        lama    speed     500.0
+                weight    200.0
+                length      1.2
+        cow     speed     500.0
+                weight    250.0
+                length      1.5
+        falcon  speed     500.0
+                weight      1.0
+                length      0.3
+        Name: 0, dtype: float64
+
+        >>> s.replace({45: 450, 30: 300})
+        lama    speed     450.0
+                weight    200.0
+                length      1.2
+        cow     speed     300.0
+                weight    250.0
+                length      1.5
+        falcon  speed     320.0
+                weight      1.0
+                length      0.3
+        Name: 0, dtype: float64
+        """
+        if to_replace is None:
+            return self
+        if not isinstance(to_replace, (str, list, dict, int, float)):
+            raise ValueError(
+                "'to_replace' should be one of str, list, dict, int, float")
+        if regex:
+            raise NotImplementedError("replace currently not support for regex")
+        if isinstance(to_replace, list) and isinstance(value, list):
+            if not len(to_replace) == len(value):
+                raise ValueError("Replacement lists must match in length. Expecting {} got {}"
+                                 .format(len(to_replace), len(value)))
+            to_replace = {k: v for k, v in zip(to_replace, value)}
+        if isinstance(to_replace, dict):
+            is_start = True
+            if len(to_replace) == 0:
+                current = self._scol
+            else:
+                for to_replace_, value in to_replace.items():
+                    if is_start:
+                        current = F.when(self._scol == F.lit(to_replace_), value)
+                        is_start = False
+                    else:
+                        current = current.when(self._scol == F.lit(to_replace_), value)
+                current = current.otherwise(self._scol)
+        else:
+            current = F.when(self._scol.isin(to_replace), value).otherwise(self._scol)
+
+        return self._with_new_scol(current)
+
     def _cum(self, func, skipna, part_cols=()):
         # This is used to cummin, cummax, cumsum, etc.
         index_columns = self._internal.index_columns


Test patch:
diff --git a/databricks/koalas/tests/test_series.py b/databricks/koalas/tests/test_series.py
--- a/databricks/koalas/tests/test_series.py
+++ b/databricks/koalas/tests/test_series.py
@@ -695,6 +695,23 @@ def test_drop(self):
             kser.drop(('lama', 'speed', 'x'))
         self.assert_eq(kser.drop(('lama', 'speed', 'x'), level=1), kser)
 
+    def test_replace(self):
+        pser = pd.Series([10, 20, 15, 30, 45], name='x')
+        kser = koalas.Series(pser)
+
+        self.assert_eq(kser.replace(), pser.replace())
+        self.assert_eq(kser.replace({}), pser.replace({}))
+
+        msg = "'to_replace' should be one of str, list, dict, int, float"
+        with self.assertRaisesRegex(ValueError, msg):
+            kser.replace(koalas.range(5))
+        msg = "Replacement lists must match in length. Expecting 3 got 2"
+        with self.assertRaisesRegex(ValueError, msg):
+            kser.replace([10, 20, 30], [1, 2])
+        msg = "replace currently not support for regex"
+        with self.assertRaisesRegex(NotImplementedError, msg):
+            kser.replace(r'^1.$', regex=True)
+
     def test_duplicates(self):
         # test on texts
         pser = pd.Series(['lama', 'cow', 'lama', 'beetle', 'lama', 'hippo'],