Repository: pandas-dev/pandas

Issue:
Replace with bfill, ffill or pad on DataFrame
#### Code Sample, a copy-pastable example if possible
With a Series ``replace`` with ``method`` equal to 'bfill', 'ffill' or 'pad' and ``value=None`` works as expected
```python
In [1]: s = pd.Series([0, 1, 2, 3, 4])
In [2]: s.replace([1, 2], method='bfill')
Out[2]: 
0    0
1    3
2    3
3    3
4    4
dtype: int64
In [3]: s.replace(1, method='bfill')
Out[3]: 
0    0
1    2
2    2
3    3
4    4
dtype: int64
```

But for a DataFrame with a list or scalar it throws an error:

``` python
In [4]: df = pd.DataFrame({'A': [0, 1, 2, 3, 4],
                   'B': [0, 1, 2, 3, 4],
                   'C': ['a', 'b', 'c', 'd', 'e']})
```

``` python
In [5]: df.replace(to_replace=[1, 2], value=None, method='bfill')
Out[5]: 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-27-b03353e82971> in <module>()
----> 1 df.replace(2, method='bfill')

~/anaconda3/lib/python3.6/site-packages/pandas/core/generic.py in replace(self, to_replace, value, inplace, limit, regex, method, axis)
   4492             if isinstance(to_replace, (tuple, list)):
   4493                 return _single_replace(self, to_replace, method, inplace,
-> 4494                                        limit)
   4495 
   4496             if not is_dict_like(to_replace):

~/anaconda3/lib/python3.6/site-packages/pandas/core/generic.py in _single_replace(self, to_replace, method, inplace, limit)
     74     if self.ndim != 1:
     75         raise TypeError('cannot replace {0} with method {1} on a {2}'
---> 76                         .format(to_replace, method, type(self).__name__))
     77 
     78     orig_dtype = self.dtype
TypeError: cannot replace [[1, 2]] with method bfill on a DataFrame
```

``` python
In [6]: df.replace(to_replace=2, value=None, method='bfill')
Out[6]:
TypeError                                 Traceback (most recent call last)
<ipython-input-27-b03353e82971> in <module>()
----> 1 df.replace(2, method='bfill')

~/anaconda3/lib/python3.6/site-packages/pandas/core/generic.py in replace(self, to_replace, value, inplace, limit, regex, method, axis)
   4492             if isinstance(to_replace, (tuple, list)):
   4493                 return _single_replace(self, to_replace, method, inplace,
-> 4494                                        limit)
   4495 
   4496             if not is_dict_like(to_replace):

~/anaconda3/lib/python3.6/site-packages/pandas/core/generic.py in _single_replace(self, to_replace, method, inplace, limit)
     74     if self.ndim != 1:
     75         raise TypeError('cannot replace {0} with method {1} on a {2}'
---> 76                         .format(to_replace, method, type(self).__name__))
     77 
     78     orig_dtype = self.dtype

TypeError: cannot replace [2] with method bfill on a DataFrame
```

Should the DataFrame case work in the same way as the Series or is this simply a user-error on my part?

#### Expected Output
``DataFrame.replace(method=)`` to work in a similar way to ``Series.replace(method=)``

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.3.final.0
python-bits: 64
OS: Linux
OS-release: 4.13.0-32-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_IE.UTF-8
LOCALE: en_IE.UTF-8

pandas: 0.22.0
pytest: 3.2.1
pip: 9.0.1
setuptools: 36.5.0.post20170921
Cython: 0.26.1
numpy: 1.14.0
scipy: 0.19.1
pyarrow: None
xarray: None
IPython: 6.1.0
sphinx: 1.6.3
patsy: 0.4.1
dateutil: 2.6.1
pytz: 2018.3
blosc: None
bottleneck: 1.2.1
tables: 3.4.2
numexpr: 2.6.2
feather: None
matplotlib: 2.1.0
openpyxl: 2.4.8
xlrd: 1.1.0
xlwt: 1.3.0
xlsxwriter: 1.0.2
lxml: 4.1.0
bs4: 4.6.0
html5lib: 0.999999999
sqlalchemy: 1.1.13
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>



Discussion:
this is essentially not implemented. This needs to be a per-column operation.

Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -4886,7 +4886,10 @@ def bfill(self, axis=None, inplace=False, limit=None, downcast=None):
             ``to_replace`` must be ``None``.
         method : string, optional, {'pad', 'ffill', 'bfill'}
             The method to use when for replacement, when ``to_replace`` is a
-            ``list``.
+            scalar, list or tuple and ``value`` is None.
+
+        .. versionchanged:: 0.23.0
+           Added to DataFrame
 
         See Also
         --------
@@ -5055,6 +5058,10 @@ def replace(self, to_replace=None, value=None, inplace=False, limit=None,
                 to_replace = [to_replace]
 
             if isinstance(to_replace, (tuple, list)):
+                if isinstance(self, pd.DataFrame):
+                    return self.apply(_single_replace,
+                                      args=(to_replace, method, inplace,
+                                            limit))
                 return _single_replace(self, to_replace, method, inplace,
                                        limit)
 


Test patch:
diff --git a/pandas/tests/frame/test_replace.py b/pandas/tests/frame/test_replace.py
--- a/pandas/tests/frame/test_replace.py
+++ b/pandas/tests/frame/test_replace.py
@@ -33,9 +33,6 @@ def test_replace_inplace(self):
         tsframe.replace(nan, 0, inplace=True)
         assert_frame_equal(tsframe, self.tsframe.fillna(0))
 
-        pytest.raises(TypeError, self.tsframe.replace, nan, inplace=True)
-        pytest.raises(TypeError, self.tsframe.replace, nan)
-
         # mixed type
         mf = self.mixed_frame
         mf.iloc[5:20, mf.columns.get_loc('foo')] = nan
@@ -720,7 +717,6 @@ def test_replace_simple_nested_dict_with_nonexistent_value(self):
         assert_frame_equal(expected, result)
 
     def test_replace_value_is_none(self):
-        pytest.raises(TypeError, self.tsframe.replace, nan)
         orig_value = self.tsframe.iloc[0, 0]
         orig2 = self.tsframe.iloc[1, 0]
 
@@ -1072,3 +1068,36 @@ def test_replace_with_empty_dictlike(self):
 
         assert_frame_equal(df, df.replace({'b': {}}))
         assert_frame_equal(df, df.replace(Series({'b': {}})))
+
+    @pytest.mark.parametrize("to_replace, method, expected", [
+        (0, 'bfill', {'A': [1, 1, 2],
+                      'B': [5, nan, 7],
+                      'C': ['a', 'b', 'c']}),
+        (nan, 'bfill', {'A': [0, 1, 2],
+                        'B': [5.0, 7.0, 7.0],
+                        'C': ['a', 'b', 'c']}),
+        ('d', 'ffill', {'A': [0, 1, 2],
+                        'B': [5, nan, 7],
+                        'C': ['a', 'b', 'c']}),
+        ([0, 2], 'bfill', {'A': [1, 1, 2],
+                           'B': [5, nan, 7],
+                           'C': ['a', 'b', 'c']}),
+        ([1, 2], 'pad', {'A': [0, 0, 0],
+                         'B': [5, nan, 7],
+                         'C': ['a', 'b', 'c']}),
+        ((1, 2), 'bfill', {'A': [0, 2, 2],
+                           'B': [5, nan, 7],
+                           'C': ['a', 'b', 'c']}),
+        (['b', 'c'], 'ffill', {'A': [0, 1, 2],
+                               'B': [5, nan, 7],
+                               'C': ['a', 'a', 'a']}),
+    ])
+    def test_replace_method(self, to_replace, method, expected):
+        # GH 19632
+        df = DataFrame({'A': [0, 1, 2],
+                        'B': [5, nan, 7],
+                        'C': ['a', 'b', 'c']})
+
+        result = df.replace(to_replace=to_replace, value=None, method=method)
+        expected = DataFrame(expected)
+        assert_frame_equal(result, expected)