Repository: pandas-dev/pandas

Issue:
DataFrame.replace() doesn't work correctly when passing a nested dict
```
In [47]: import pandas as pd

In [48]: df = pd.DataFrame({'col': [1,2,3,4]})

In [49]: df.replace({'col': {1: 'a', 4: 'b'}})
Out[49]: 
  col
0   a
1   2
2   3
3   b

In [50]: df.replace({'col': {-1: '-', 1: 'a', 4: 'b'}})
Out[50]: 
  col
0   a
1   2
2   3
3   a
^^^^ Value 4 must be mapped to "b" instead of "a"

In [51]: pd.__version__
Out[51]: '0.12.0'
```



Discussion:
Can you reproduce this on master ie the latest commit

Yes, same problem. And not only with -1 value, also when there are no lineal sequence in IDs, for example {10: 'a', 1: 'b', 3: 'c'}.

Thanks a lot for your attention.

@estevopaz I'm not sure what you mean by

> when there are no lineal sequence in IDs

The key value pairs in a Python `dict` are [arbitrarily ordered](http://docs.python.org/2/library/stdtypes.html#dict.items), so the `dict`s `{1: 'a', 2: 'b'}` and `{2: 'b', 1: 'a'}` are equivalent.


Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -2330,8 +2330,8 @@ def replace(self, to_replace=None, value=None, inplace=False, limit=None,
                 value_dict = {}
 
                 for k, v in items:
-                    to_rep_dict[k] = v.keys()
-                    value_dict[k] = v.values()
+                    to_rep_dict[k] = list(v.keys())
+                    value_dict[k] = list(v.values())
 
                 to_replace, value = to_rep_dict, value_dict
             else:
@@ -2349,7 +2349,6 @@ def replace(self, to_replace=None, value=None, inplace=False, limit=None,
             new_data = self._data
             if is_dictlike(to_replace):
                 if is_dictlike(value):  # {'A' : NA} -> {'A' : 0}
-                    new_data = self._data
                     for c, src in compat.iteritems(to_replace):
                         if c in value and c in self:
                             new_data = new_data.replace(to_replace=src,
@@ -2360,7 +2359,6 @@ def replace(self, to_replace=None, value=None, inplace=False, limit=None,
 
                 # {'A': NA} -> 0
                 elif not com.is_list_like(value):
-                    new_data = self._data
                     for k, src in compat.iteritems(to_replace):
                         if k in self:
                             new_data = new_data.replace(to_replace=src,
diff --git a/pandas/core/internals.py b/pandas/core/internals.py
--- a/pandas/core/internals.py
+++ b/pandas/core/internals.py
@@ -729,7 +729,7 @@ def putmask(self, mask, new, align=True, inplace=False):
             def create_block(v, m, n, item, reshape=True):
                 """ return a new block, try to preserve dtype if possible """
 
-                # n should the length of the mask or a scalar here
+                # n should be the length of the mask or a scalar here
                 if not is_list_like(n):
                     n = np.array([n] * len(m))
 
@@ -742,7 +742,7 @@ def create_block(v, m, n, item, reshape=True):
                     if (nn == nn_at).all():
                         nv = v.copy()
                         nv[mask] = nn_at
-                except:
+                except (ValueError, IndexError, TypeError):
                     pass
 
                 # change the dtype
@@ -751,8 +751,10 @@ def create_block(v, m, n, item, reshape=True):
                     nv = v.astype(dtype)
                     try:
                         nv[m] = n
-                    except:
-                        np.putmask(nv, m, n)
+                    except ValueError:
+                        idx, = np.where(np.squeeze(m))
+                        for mask_index, new_val in zip(idx, n):
+                            nv[mask_index] = new_val
 
                 if reshape:
                     nv = _block_shape(nv)


Test patch:
diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py
--- a/pandas/tests/test_frame.py
+++ b/pandas/tests/test_frame.py
@@ -7804,6 +7804,27 @@ def test_replace_mixed(self):
         expected.iloc[1,1] = m[1]
         assert_frame_equal(result,expected)
 
+    def test_replace_simple_nested_dict(self):
+        df = DataFrame({'col': range(1, 5)})
+        expected = DataFrame({'col': ['a', 2, 3, 'b']})
+
+        result = df.replace({'col': {1: 'a', 4: 'b'}})
+        tm.assert_frame_equal(expected, result)
+
+        # in this case, should be the same as the not nested version
+        result = df.replace({1: 'a', 4: 'b'})
+        tm.assert_frame_equal(expected, result)
+
+    def test_replace_simple_nested_dict_with_nonexistent_value(self):
+        df = DataFrame({'col': range(1, 5)})
+        expected = DataFrame({'col': ['a', 2, 3, 'b']})
+
+        result = df.replace({-1: '-', 1: 'a', 4: 'b'})
+        tm.assert_frame_equal(expected, result)
+
+        result = df.replace({'col': {-1: '-', 1: 'a', 4: 'b'}})
+        tm.assert_frame_equal(expected, result)
+
     def test_interpolate(self):
         pass
 
@@ -12148,7 +12169,6 @@ def check_query_with_named_multiindex(self, parser, engine):
                      name='color')
 
         # equality
-        #import ipdb; ipdb.set_trace()
         res1 = df.query('color == "red"', parser=parser, engine=engine)
         res2 = df.query('"red" == color', parser=parser, engine=engine)
         exp = df[ind == 'red']