Repository: pandas-dev/pandas

Issue:
BUG: unstack(fill_value) does nothing when unstacking multiple columns
In #9746 we got a `fill_value` argument for `pd.unstack()`.  But it does nothing if we unstack multiple levels at once:

```
df = pd.DataFrame({'x':['a', 'a', 'b'], 'y':['j', 'k', 'j'], 'z':[0, 1, 2], 'w':[0, 1, 2]})
df.set_index(['x', 'y', 'z']).unstack(['x', 'y'], fill_value=0)
```

That gives:

```
     w          
x    a         b
y    j    k    j
z               
0  0.0  NaN  NaN
1  NaN  1.0  NaN
2  NaN  NaN  2.0
```

It should give zeros instead of NaN.

Pandas 0.18.1.



Discussion:
multiple unstacks are fraught with issues, see #9023 and #11847. you can do this. of course a PR to fix is welcome!

```
In [19]: df = pd.DataFrame({'x':['a', 'a', 'b'], 'y':['j', 'k', 'j'], 'z':[0, 1, 2], 'w':[0, 1, 2]})
    ...: df.set_index(['x', 'y', 'z']).unstack('x', fill_value=0).unstack('y', fill_value=0)
    ...: 
    ...: 
Out[19]: 
   w         
x  a     b   
y  j  k  j  k
z            
0  0  0  0  0
1  0  1  0  0
2  0  0  2  0
```

I'll mark it, but its a rabbit hole.

Unstacking is inconsistent since `_unstack_frame` is used if column name is passed to `unstack()` and `_unstack_multiple` if the list is passed. Maybe it would be OK to play a little and make it more consistent? Or there is some reason (which I am not aware of), for this (almost) complete separation?


Resolving patch:
diff --git a/pandas/core/reshape/reshape.py b/pandas/core/reshape/reshape.py
--- a/pandas/core/reshape/reshape.py
+++ b/pandas/core/reshape/reshape.py
@@ -292,7 +292,7 @@ def get_new_index(self):
                           names=self.new_index_names, verify_integrity=False)
 
 
-def _unstack_multiple(data, clocs):
+def _unstack_multiple(data, clocs, fill_value=None):
     if len(clocs) == 0:
         return data
 
@@ -330,7 +330,7 @@ def _unstack_multiple(data, clocs):
     if isinstance(data, Series):
         dummy = data.copy()
         dummy.index = dummy_index
-        unstacked = dummy.unstack('__placeholder__')
+        unstacked = dummy.unstack('__placeholder__', fill_value=fill_value)
         new_levels = clevels
         new_names = cnames
         new_labels = recons_labels
@@ -347,7 +347,7 @@ def _unstack_multiple(data, clocs):
         dummy = data.copy()
         dummy.index = dummy_index
 
-        unstacked = dummy.unstack('__placeholder__')
+        unstacked = dummy.unstack('__placeholder__', fill_value=fill_value)
         if isinstance(unstacked, Series):
             unstcols = unstacked.index
         else:
@@ -460,7 +460,7 @@ def unstack(obj, level, fill_value=None):
         if len(level) != 1:
             # _unstack_multiple only handles MultiIndexes,
             # and isn't needed for a single level
-            return _unstack_multiple(obj, level)
+            return _unstack_multiple(obj, level, fill_value=fill_value)
         else:
             level = level[0]
 


Test patch:
diff --git a/pandas/tests/frame/test_reshape.py b/pandas/tests/frame/test_reshape.py
--- a/pandas/tests/frame/test_reshape.py
+++ b/pandas/tests/frame/test_reshape.py
@@ -116,22 +116,22 @@ def test_pivot_index_none(self):
         tm.assert_frame_equal(result, expected)
 
     def test_stack_unstack(self):
-        f = self.frame.copy()
-        f[:] = np.arange(np.prod(f.shape)).reshape(f.shape)
+        df = self.frame.copy()
+        df[:] = np.arange(np.prod(df.shape)).reshape(df.shape)
 
-        stacked = f.stack()
+        stacked = df.stack()
         stacked_df = DataFrame({'foo': stacked, 'bar': stacked})
 
         unstacked = stacked.unstack()
         unstacked_df = stacked_df.unstack()
 
-        assert_frame_equal(unstacked, f)
-        assert_frame_equal(unstacked_df['bar'], f)
+        assert_frame_equal(unstacked, df)
+        assert_frame_equal(unstacked_df['bar'], df)
 
         unstacked_cols = stacked.unstack(0)
         unstacked_cols_df = stacked_df.unstack(0)
-        assert_frame_equal(unstacked_cols.T, f)
-        assert_frame_equal(unstacked_cols_df['bar'].T, f)
+        assert_frame_equal(unstacked_cols.T, df)
+        assert_frame_equal(unstacked_cols_df['bar'].T, df)
 
     def test_unstack_fill(self):
 
@@ -154,6 +154,30 @@ def test_unstack_fill(self):
                              index=['x', 'y', 'z'], dtype=np.float)
         assert_frame_equal(result, expected)
 
+        # GH #13971: fill_value when unstacking multiple levels:
+        df = DataFrame({'x': ['a', 'a', 'b'],
+                        'y': ['j', 'k', 'j'],
+                        'z': [0, 1, 2],
+                        'w': [0, 1, 2]}).set_index(['x', 'y', 'z'])
+        unstacked = df.unstack(['x', 'y'], fill_value=0)
+        key = ('w', 'b', 'j')
+        expected = unstacked[key]
+        result = pd.Series([0, 0, 2], index=unstacked.index, name=key)
+        assert_series_equal(result, expected)
+
+        stacked = unstacked.stack(['x', 'y'])
+        stacked.index = stacked.index.reorder_levels(df.index.names)
+        # Workaround for GH #17886 (unnecessarily casts to float):
+        stacked = stacked.astype(np.int64)
+        result = stacked.loc[df.index]
+        assert_frame_equal(result, df)
+
+        # From a series
+        s = df['w']
+        result = s.unstack(['x', 'y'], fill_value=0)
+        expected = unstacked['w']
+        assert_frame_equal(result, expected)
+
     def test_unstack_fill_frame(self):
 
         # From a dataframe