Repository: pandas-dev/pandas

Issue:
"AttributeError: _ref_locs" while assigning non-contiguous MultiIndex columns
Hitting an exception with the following code on 0.12.0 (and current master).  Copied 0.11.0 behavior for reference, but feel free to reject if assignment to non-contiguous columns of a MultiIndex using a partial label is illegal/unsupported.

```
import numpy as np
import pandas as pd

df = pd.DataFrame(
    np.ones((1, 3)),
    columns=pd.MultiIndex.from_tuples(
        [('A', '1'), ('B', '1'), ('A', '2')]
    ),
    dtype=object
)
print 'Before:'
print df.dtypes
df['A'] = df['A'].astype(float)
print 'After:'
print df.dtypes
```

0.11.0 output:

```
Before:
A  1    object
B  1    object
A  2    object
dtype: object
After:
A  1    float64
B  1     object
A  2    float64
dtype: object
```

0.12.0 output:

```
Before:
A  1    object
B  1    object
A  2    object
dtype: object
Traceback (most recent call last):
  File "repro.py", line 15, in <module>
    df['A'] = df['A'].astype(float)
  File "/home/gmd/ENV/pandas-master/lib/python2.7/site-packages/pandas-0.12.0_270_ge3c71f2-py2.7-linux-i686.egg/pandas/core/frame.py", line 1929, in __setitem__
    self._set_item(key, value)
  File "/home/gmd/ENV/pandas-master/lib/python2.7/site-packages/pandas-0.12.0_270_ge3c71f2-py2.7-linux-i686.egg/pandas/core/frame.py", line 1977, in _set_item
    NDFrame._set_item(self, key, value)
  File "/home/gmd/ENV/pandas-master/lib/python2.7/site-packages/pandas-0.12.0_270_ge3c71f2-py2.7-linux-i686.egg/pandas/core/generic.py", line 798, in _set_item
    self._data.set(key, value)
  File "/home/gmd/ENV/pandas-master/lib/python2.7/site-packages/pandas-0.12.0_270_ge3c71f2-py2.7-linux-i686.egg/pandas/core/internals.py", line 2448, in set
    self._reset_ref_locs()
  File "/home/gmd/ENV/pandas-master/lib/python2.7/site-packages/pandas-0.12.0_270_ge3c71f2-py2.7-linux-i686.egg/pandas/core/internals.py", line 1644, in _reset_ref_locs
    self._rebuild_ref_locs()
  File "/home/gmd/ENV/pandas-master/lib/python2.7/site-packages/pandas-0.12.0_270_ge3c71f2-py2.7-linux-i686.egg/pandas/core/internals.py", line 1652, in _rebuild_ref_locs
    if self._ref_locs is not None:
AttributeError: _ref_locs
```

Thanks!



Discussion:
Do it this way.

Not sorted assignment is definitnely not supported (and should raise a better error).
This is actually quite tricky. Will mark as a bug and look at it at some point.

```
In [23]: df
Out[23]: 
   A  B  A
   1  1  2
0  1  1  1

In [24]: df.dtypes
Out[24]: 
A  1    object
B  1    object
A  2    object
dtype: object

In [25]: df.sortlevel(axis=1)
Out[25]: 
   A     B
   1  2  1
0  1  1  1

In [26]: df.sortlevel(axis=1).dtypes
Out[26]: 
A  1    object
   2    object
B  1    object
dtype: object

In [27]: df.sortlevel(axis=1).convert_objects()
Out[27]: 
   A     B
   1  2  1
0  1  1  1

In [28]: df.sortlevel(axis=1).convert_objects().dtypes
Out[28]: 
A  1    float64
   2    float64
B  1    float64
dtype: object
```


Resolving patch:
diff --git a/pandas/core/internals.py b/pandas/core/internals.py
--- a/pandas/core/internals.py
+++ b/pandas/core/internals.py
@@ -1797,11 +1797,13 @@ def _reset_ref_locs(self):
     def _rebuild_ref_locs(self):
         """ take _ref_locs and set the individual block ref_locs, skipping Nones
             no effect on a unique index """
-        if self._ref_locs is not None:
+        if getattr(self,'_ref_locs',None) is not None:
             item_count = 0
             for v in self._ref_locs:
                 if v is not None:
                     block, item_loc = v
+                    if block._ref_locs is None:
+                        block.reset_ref_locs()
                     block._ref_locs[item_loc] = item_count
                     item_count += 1
 
@@ -2595,11 +2597,11 @@ def _set_item(item, arr):
                     self.delete(item)
 
                     loc = _possibly_convert_to_indexer(loc)
-                    for i, (l, arr) in enumerate(zip(loc, value)):
+                    for i, (l, k, arr) in enumerate(zip(loc, subset, value)):
 
                         # insert the item
                         self.insert(
-                            l, item, arr[None, :], allow_duplicates=True)
+                            l, k, arr[None, :], allow_duplicates=True)
 
                         # reset the _ref_locs on indiviual blocks
                         # rebuild ref_locs


Test patch:
diff --git a/pandas/tests/test_indexing.py b/pandas/tests/test_indexing.py
--- a/pandas/tests/test_indexing.py
+++ b/pandas/tests/test_indexing.py
@@ -1292,6 +1292,24 @@ def test_astype_assignment_with_iloc(self):
         result = df.get_dtype_counts().sort_index()
         expected = Series({ 'int64' : 4, 'float64' : 1, 'object' : 2 }).sort_index()
 
+    def test_astype_assignment_with_dups(self):
+
+        # GH 4686
+        # assignment with dups that has a dtype change
+        df = DataFrame(
+            np.arange(3).reshape((1,3)),
+            columns=pd.MultiIndex.from_tuples(
+                [('A', '1'), ('B', '1'), ('A', '2')]
+                ),
+            dtype=object
+            )
+        index = df.index.copy()
+
+        df['A'] = df['A'].astype(np.float64)
+        result = df.get_dtype_counts().sort_index()
+        expected = Series({ 'float64' : 2, 'object' : 1 }).sort_index()
+        self.assert_(df.index.equals(index))
+
     def test_dups_loc(self):
 
         # GH4726