Repository: pandas-dev/pandas

Issue:
Pivot table is making floats
I am having issues with pivot table creation making columns into floats when they don't need to be.

```
In [313]: f = pd.DataFrame({'a' : ['cat', 'bat', 'cat', 'bat'], 'v' : [1,2,3,4], 'i' : ['a','b','a','b']})

In [314]: z = pd.pivot_table(f, values='v', rows=['a'], cols=['i'], fill_value=0, aggfunc=np.sum)

In [315]: z
Out[315]: 
i    a  b
a
bat  0  6
cat  4  0

In [316]: z.b.dtype
Out[316]: dtype('float64')

In [317]: f.v.dtype
Out[317]: dtype('int64')

In [318]: np.sum(f.v)
Out[318]: 10

In [319]: type(np.sum(f.v))
Out[319]: numpy.int64
```

This is making stuff ugly when I to_csv it.



Discussion:

Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -3231,7 +3231,7 @@ def reorder_levels(self, order, axis=0):
     # Filling NA's
 
     def fillna(self, value=None, method=None, axis=0, inplace=False,
-               limit=None):
+               limit=None, downcast=None):
         """
         Fill NA/NaN values using the specified method
 
@@ -3255,6 +3255,8 @@ def fillna(self, value=None, method=None, axis=0, inplace=False,
             a reference to the filled object, which is self if inplace=True
         limit : int, default None
             Maximum size gap to forward or backward fill
+        downcast : dict, default is None, a dict of item->dtype of what to 
+            downcast if possible
 
         See also
         --------
@@ -3300,7 +3302,8 @@ def fillna(self, value=None, method=None, axis=0, inplace=False,
                     result[k].fillna(v, inplace=True)
                 return result
             else:
-                new_data = self._data.fillna(value, inplace=inplace)
+                new_data = self._data.fillna(value, inplace=inplace, 
+                                             downcast=downcast)
 
         if inplace:
             self._data = new_data
diff --git a/pandas/core/internals.py b/pandas/core/internals.py
--- a/pandas/core/internals.py
+++ b/pandas/core/internals.py
@@ -28,6 +28,7 @@ class Block(object):
     is_bool = False
     is_object = False
     _can_hold_na = False
+    _downcast_dtype = None
 
     def __init__(self, values, items, ref_items, ndim=2):
         if issubclass(values.dtype.type, basestring):
@@ -205,7 +206,7 @@ def split_block_at(self, item):
                              self.items[s:e].copy(),
                              self.ref_items)
 
-    def fillna(self, value, inplace=False):
+    def fillna(self, value, inplace=False, downcast=None):
         if not self._can_hold_na:
             if inplace:
                 return self
@@ -216,10 +217,32 @@ def fillna(self, value, inplace=False):
         mask = com.isnull(new_values)
         np.putmask(new_values, mask, value)
 
-        if inplace:
-            return self
-        else:
-            return make_block(new_values, self.items, self.ref_items)
+        block = make_block(new_values, self.items, self.ref_items)
+        if downcast:
+            block = block.downcast()
+        return block
+
+    def downcast(self, dtypes = None):
+        """ try to downcast each item to the dict of dtypes if present """
+
+        if dtypes is None:
+            dtypes = dict()
+
+        values = self.values
+        blocks = []
+        for i, item in enumerate(self.items):
+
+            dtype = dtypes.get(item,self._downcast_dtype)
+            if dtype is None:
+                nv = _block_shape(values[i])
+                blocks.append(make_block(nv, [ item ], self.ref_items))
+                continue
+            
+            nv = _possibly_downcast_to_dtype(values[i], np.dtype(dtype))
+            nv = _block_shape(nv)
+            blocks.append(make_block(nv, [ item ], self.ref_items))
+
+        return blocks
 
     def astype(self, dtype, copy = True, raise_on_error = True):
         """
@@ -563,6 +586,7 @@ def _try_cast_result(self, result):
         return _possibly_downcast_to_dtype(result, self.dtype)
 
 class FloatBlock(NumericBlock):
+    _downcast_dtype = 'int64'
 
     def _can_hold_element(self, element):
         if isinstance(element, np.ndarray):
@@ -974,6 +998,9 @@ def shift(self, *args, **kwargs):
     def fillna(self, *args, **kwargs):
         return self.apply('fillna', *args, **kwargs)
 
+    def downcast(self, *args, **kwargs):
+        return self.apply('downcast', *args, **kwargs)
+
     def astype(self, *args, **kwargs):
         return self.apply('astype', *args, **kwargs)
 
diff --git a/pandas/tools/pivot.py b/pandas/tools/pivot.py
--- a/pandas/tools/pivot.py
+++ b/pandas/tools/pivot.py
@@ -111,7 +111,7 @@ def pivot_table(data, values=None, rows=None, cols=None, aggfunc='mean',
             table = table.sort_index(axis=1)
 
     if fill_value is not None:
-        table = table.fillna(value=fill_value)
+        table = table.fillna(value=fill_value, downcast=True)
 
     if margins:
         table = _add_margins(table, data, values, rows=rows,


Test patch:
diff --git a/pandas/tools/tests/test_pivot.py b/pandas/tools/tests/test_pivot.py
--- a/pandas/tools/tests/test_pivot.py
+++ b/pandas/tools/tests/test_pivot.py
@@ -69,6 +69,26 @@ def test_pivot_table_multiple(self):
         expected = self.data.groupby(rows + [cols]).agg(np.mean).unstack()
         tm.assert_frame_equal(table, expected)
 
+    def test_pivot_dtypes(self):
+
+        # can convert dtypes
+        f = DataFrame({'a' : ['cat', 'bat', 'cat', 'bat'], 'v' : [1,2,3,4], 'i' : ['a','b','a','b']})
+        self.assert_(f.dtypes['v'] == 'int64')
+
+        z = pivot_table(f, values='v', rows=['a'], cols=['i'], fill_value=0, aggfunc=np.sum)
+        result = z.get_dtype_counts()
+        expected = Series(dict(int64 = 2))
+        tm.assert_series_equal(result, expected)
+
+        # cannot convert dtypes
+        f = DataFrame({'a' : ['cat', 'bat', 'cat', 'bat'], 'v' : [1.5,2.5,3.5,4.5], 'i' : ['a','b','a','b']})
+        self.assert_(f.dtypes['v'] == 'float64')
+
+        z = pivot_table(f, values='v', rows=['a'], cols=['i'], fill_value=0, aggfunc=np.mean)
+        result = z.get_dtype_counts()
+        expected = Series(dict(float64 = 2))
+        tm.assert_series_equal(result, expected)
+
     def test_pivot_multi_values(self):
         result = pivot_table(self.data, values=['D', 'E'],
                              rows='A', cols=['B', 'C'], fill_value=0)