From 45b304e1a41b7726c47e186ac7f498bc94691ce2 Mon Sep 17 00:00:00 2001 From: Dr-Irv Date: Wed, 9 Dec 2015 13:46:22 -0500 Subject: [PATCH] GH11763: Implement round(DataFrame), round(Series), round(Panel), Panel.round() --- doc/source/whatsnew/v0.18.0.txt | 4 + pandas/core/frame.py | 10 ++- pandas/core/generic.py | 5 +- pandas/core/panel.py | 27 +++++++ pandas/core/series.py | 26 ++++-- pandas/tests/test_format.py | 124 +---------------------------- pandas/tests/test_frame.py | 137 ++++++++++++++++++++++++++++++++ pandas/tests/test_panel.py | 16 ++++ pandas/tests/test_series.py | 19 ++++- 9 files changed, 233 insertions(+), 135 deletions(-) diff --git a/doc/source/whatsnew/v0.18.0.txt b/doc/source/whatsnew/v0.18.0.txt index e71830d7dd8d8..a534c3ec0017a 100644 --- a/doc/source/whatsnew/v0.18.0.txt +++ b/doc/source/whatsnew/v0.18.0.txt @@ -34,6 +34,8 @@ Other enhancements - Handle truncated floats in SAS xport files (:issue:`11713`) - Added option to hide index in ``Series.to_string`` (:issue:`11729`) - ``read_excel`` now supports s3 urls of the format ``s3://bucketname/filename`` (:issue:`11447`) +- A simple version of ``Panel.round()`` is now implemented (:issue:`11763`) +- For Python 3.x, ``round(DataFrame)``, ``round(Series)``, ``round(Panel)`` will work (:issue:`11763`) .. _whatsnew_0180.enhancements.rounding: @@ -90,6 +92,8 @@ In addition, ``.round()`` will be available thru the ``.dt`` accessor of ``Serie Backwards incompatible API changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- The parameter ``out`` has been removed from the ``Series.round()`` method. (:issue:`11763`) + Bug in QuarterBegin with n=0 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pandas/core/frame.py b/pandas/core/frame.py index dd41075ddf92e..9480136e418e9 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -4376,13 +4376,17 @@ def round(self, decimals=0, out=None): Returns ------- DataFrame object + + See Also + -------- + numpy.around """ from pandas.tools.merge import concat def _dict_round(df, decimals): for col, vals in df.iteritems(): try: - yield np.round(vals, decimals[col]) + yield vals.round(decimals[col]) except KeyError: yield vals @@ -4392,8 +4396,8 @@ def _dict_round(df, decimals): raise ValueError("Index of decimals must be unique") new_cols = [col for col in _dict_round(self, decimals)] elif com.is_integer(decimals): - # Dispatch to numpy.round - new_cols = [np.round(v, decimals) for _, v in self.iteritems()] + # Dispatch to Series.round + new_cols = [v.round(decimals) for _, v in self.iteritems()] else: raise TypeError("decimals must be an integer, a dict-like or a Series") diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 725e33083da5d..b75573edc7157 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -749,7 +749,10 @@ def bool(self): def __abs__(self): return self.abs() - + + def __round__(self,decimals=0): + return self.round(decimals) + #---------------------------------------------------------------------- # Array Interface diff --git a/pandas/core/panel.py b/pandas/core/panel.py index b57e90509080d..e0d9405a66b75 100644 --- a/pandas/core/panel.py +++ b/pandas/core/panel.py @@ -624,6 +624,33 @@ def head(self, n=5): def tail(self, n=5): raise NotImplementedError + + def round(self, decimals=0): + """ + Round each value in Panel to a specified number of decimal places. + + .. versionadded:: 0.18.0 + + Parameters + ---------- + decimals : int + Number of decimal places to round to (default: 0). + If decimals is negative, it specifies the number of + positions to the left of the decimal point. + + Returns + ------- + Panel object + + See Also + -------- + numpy.around + """ + if com.is_integer(decimals): + result = np.apply_along_axis(np.round, 0, self.values) + return self._wrap_result(result, axis=0) + raise TypeError("decimals must be an integer") + def _needs_reindex_multi(self, axes, method, level): """ don't allow a multi reindex on Panel or above ndim """ diff --git a/pandas/core/series.py b/pandas/core/series.py index 50616ce61f610..ca55a834a33d2 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -1235,15 +1235,29 @@ def idxmax(self, axis=None, out=None, skipna=True): argmin = idxmin argmax = idxmax - @Appender(np.ndarray.round.__doc__) - def round(self, decimals=0, out=None): + def round(self, decimals=0): """ + Round each value in a Series to the given number of decimals. + + Parameters + ---------- + decimals : int + Number of decimal places to round to (default: 0). + If decimals is negative, it specifies the number of + positions to the left of the decimal point. + + Returns + ------- + Series object + + See Also + -------- + numpy.around """ - result = _values_from_object(self).round(decimals, out=out) - if out is None: - result = self._constructor(result, - index=self.index).__finalize__(self) + result = _values_from_object(self).round(decimals) + result = self._constructor(result, + index=self.index).__finalize__(self) return result diff --git a/pandas/tests/test_format.py b/pandas/tests/test_format.py index 935c85ca3e29d..f2290877676fa 100644 --- a/pandas/tests/test_format.py +++ b/pandas/tests/test_format.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from __future__ import print_function from distutils.version import LooseVersion import re @@ -2969,128 +2969,6 @@ def test_to_csv_engine_kw_deprecation(self): df = DataFrame({'col1' : [1], 'col2' : ['a'], 'col3' : [10.1] }) df.to_csv(engine='python') - def test_round_dataframe(self): - - # GH 2665 - - # Test that rounding an empty DataFrame does nothing - df = DataFrame() - tm.assert_frame_equal(df, df.round()) - - # Here's the test frame we'll be working with - df = DataFrame( - {'col1': [1.123, 2.123, 3.123], 'col2': [1.234, 2.234, 3.234]}) - - # Default round to integer (i.e. decimals=0) - expected_rounded = DataFrame( - {'col1': [1., 2., 3.], 'col2': [1., 2., 3.]}) - tm.assert_frame_equal(df.round(), expected_rounded) - - # Round with an integer - decimals = 2 - expected_rounded = DataFrame( - {'col1': [1.12, 2.12, 3.12], 'col2': [1.23, 2.23, 3.23]}) - tm.assert_frame_equal(df.round(decimals), expected_rounded) - - # This should also work with np.round (since np.round dispatches to - # df.round) - tm.assert_frame_equal(np.round(df, decimals), expected_rounded) - - # Round with a list - round_list = [1, 2] - with self.assertRaises(TypeError): - df.round(round_list) - - # Round with a dictionary - expected_rounded = DataFrame( - {'col1': [1.1, 2.1, 3.1], 'col2': [1.23, 2.23, 3.23]}) - round_dict = {'col1': 1, 'col2': 2} - tm.assert_frame_equal(df.round(round_dict), expected_rounded) - - # Incomplete dict - expected_partially_rounded = DataFrame( - {'col1': [1.123, 2.123, 3.123], 'col2': [1.2, 2.2, 3.2]}) - partial_round_dict = {'col2': 1} - tm.assert_frame_equal( - df.round(partial_round_dict), expected_partially_rounded) - - # Dict with unknown elements - wrong_round_dict = {'col3': 2, 'col2': 1} - tm.assert_frame_equal( - df.round(wrong_round_dict), expected_partially_rounded) - - # float input to `decimals` - non_int_round_dict = {'col1': 1, 'col2': 0.5} - if sys.version < LooseVersion('2.7'): - # np.round([1.123, 2.123], 0.5) is only a warning in Python 2.6 - with self.assert_produces_warning(DeprecationWarning, check_stacklevel=False): - df.round(non_int_round_dict) - else: - with self.assertRaises(TypeError): - df.round(non_int_round_dict) - - # String input - non_int_round_dict = {'col1': 1, 'col2': 'foo'} - with self.assertRaises(TypeError): - df.round(non_int_round_dict) - - non_int_round_Series = Series(non_int_round_dict) - with self.assertRaises(TypeError): - df.round(non_int_round_Series) - - # List input - non_int_round_dict = {'col1': 1, 'col2': [1, 2]} - with self.assertRaises(TypeError): - df.round(non_int_round_dict) - - non_int_round_Series = Series(non_int_round_dict) - with self.assertRaises(TypeError): - df.round(non_int_round_Series) - - # Non integer Series inputs - non_int_round_Series = Series(non_int_round_dict) - with self.assertRaises(TypeError): - df.round(non_int_round_Series) - - non_int_round_Series = Series(non_int_round_dict) - with self.assertRaises(TypeError): - df.round(non_int_round_Series) - - # Negative numbers - negative_round_dict = {'col1': -1, 'col2': -2} - big_df = df * 100 - expected_neg_rounded = DataFrame( - {'col1':[110., 210, 310], 'col2':[100., 200, 300]}) - tm.assert_frame_equal( - big_df.round(negative_round_dict), expected_neg_rounded) - - # nan in Series round - nan_round_Series = Series({'col1': nan, 'col2':1}) - expected_nan_round = DataFrame( - {'col1': [1.123, 2.123, 3.123], 'col2': [1.2, 2.2, 3.2]}) - if sys.version < LooseVersion('2.7'): - # Rounding with decimal is a ValueError in Python < 2.7 - with self.assertRaises(ValueError): - df.round(nan_round_Series) - else: - with self.assertRaises(TypeError): - df.round(nan_round_Series) - - # Make sure this doesn't break existing Series.round - tm.assert_series_equal(df['col1'].round(1), expected_rounded['col1']) - - def test_round_issue(self): - # GH11611 - - df = pd.DataFrame(np.random.random([3, 3]), columns=['A', 'B', 'C'], - index=['first', 'second', 'third']) - - dfs = pd.concat((df, df), axis=1) - rounded = dfs.round() - self.assertTrue(rounded.index.equals(dfs.index)) - - decimals = pd.Series([1, 0, 2], index=['A', 'B', 'A']) - self.assertRaises(ValueError, df.round, decimals) class TestSeriesFormatting(tm.TestCase): _multiprocess_can_split_ = True diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py index 09de3bf4a8046..70548b7c9f42f 100644 --- a/pandas/tests/test_frame.py +++ b/pandas/tests/test_frame.py @@ -13391,6 +13391,143 @@ def wrapper(x): self._check_stat_op('median', wrapper, frame=self.intframe, check_dtype=False, check_dates=True) + def test_round(self): + + # GH 2665 + + # Test that rounding an empty DataFrame does nothing + df = DataFrame() + tm.assert_frame_equal(df, df.round()) + + # Here's the test frame we'll be working with + df = DataFrame( + {'col1': [1.123, 2.123, 3.123], 'col2': [1.234, 2.234, 3.234]}) + + # Default round to integer (i.e. decimals=0) + expected_rounded = DataFrame( + {'col1': [1., 2., 3.], 'col2': [1., 2., 3.]}) + tm.assert_frame_equal(df.round(), expected_rounded) + + # Round with an integer + decimals = 2 + expected_rounded = DataFrame( + {'col1': [1.12, 2.12, 3.12], 'col2': [1.23, 2.23, 3.23]}) + tm.assert_frame_equal(df.round(decimals), expected_rounded) + + # This should also work with np.round (since np.round dispatches to + # df.round) + tm.assert_frame_equal(np.round(df, decimals), expected_rounded) + + # Round with a list + round_list = [1, 2] + with self.assertRaises(TypeError): + df.round(round_list) + + # Round with a dictionary + expected_rounded = DataFrame( + {'col1': [1.1, 2.1, 3.1], 'col2': [1.23, 2.23, 3.23]}) + round_dict = {'col1': 1, 'col2': 2} + tm.assert_frame_equal(df.round(round_dict), expected_rounded) + + # Incomplete dict + expected_partially_rounded = DataFrame( + {'col1': [1.123, 2.123, 3.123], 'col2': [1.2, 2.2, 3.2]}) + partial_round_dict = {'col2': 1} + tm.assert_frame_equal( + df.round(partial_round_dict), expected_partially_rounded) + + # Dict with unknown elements + wrong_round_dict = {'col3': 2, 'col2': 1} + tm.assert_frame_equal( + df.round(wrong_round_dict), expected_partially_rounded) + + # float input to `decimals` + non_int_round_dict = {'col1': 1, 'col2': 0.5} + if sys.version < LooseVersion('2.7'): + # np.round([1.123, 2.123], 0.5) is only a warning in Python 2.6 + with self.assert_produces_warning(DeprecationWarning, check_stacklevel=False): + df.round(non_int_round_dict) + else: + with self.assertRaises(TypeError): + df.round(non_int_round_dict) + + # String input + non_int_round_dict = {'col1': 1, 'col2': 'foo'} + with self.assertRaises(TypeError): + df.round(non_int_round_dict) + + non_int_round_Series = Series(non_int_round_dict) + with self.assertRaises(TypeError): + df.round(non_int_round_Series) + + # List input + non_int_round_dict = {'col1': 1, 'col2': [1, 2]} + with self.assertRaises(TypeError): + df.round(non_int_round_dict) + + non_int_round_Series = Series(non_int_round_dict) + with self.assertRaises(TypeError): + df.round(non_int_round_Series) + + # Non integer Series inputs + non_int_round_Series = Series(non_int_round_dict) + with self.assertRaises(TypeError): + df.round(non_int_round_Series) + + non_int_round_Series = Series(non_int_round_dict) + with self.assertRaises(TypeError): + df.round(non_int_round_Series) + + # Negative numbers + negative_round_dict = {'col1': -1, 'col2': -2} + big_df = df * 100 + expected_neg_rounded = DataFrame( + {'col1':[110., 210, 310], 'col2':[100., 200, 300]}) + tm.assert_frame_equal( + big_df.round(negative_round_dict), expected_neg_rounded) + + # nan in Series round + nan_round_Series = Series({'col1': nan, 'col2':1}) + expected_nan_round = DataFrame( + {'col1': [1.123, 2.123, 3.123], 'col2': [1.2, 2.2, 3.2]}) + if sys.version < LooseVersion('2.7'): + # Rounding with decimal is a ValueError in Python < 2.7 + with self.assertRaises(ValueError): + df.round(nan_round_Series) + else: + with self.assertRaises(TypeError): + df.round(nan_round_Series) + + # Make sure this doesn't break existing Series.round + tm.assert_series_equal(df['col1'].round(1), expected_rounded['col1']) + + def test_round_issue(self): + # GH11611 + + df = pd.DataFrame(np.random.random([3, 3]), columns=['A', 'B', 'C'], + index=['first', 'second', 'third']) + + dfs = pd.concat((df, df), axis=1) + rounded = dfs.round() + self.assertTrue(rounded.index.equals(dfs.index)) + + decimals = pd.Series([1, 0, 2], index=['A', 'B', 'A']) + self.assertRaises(ValueError, df.round, decimals) + + def test_built_in_round(self): + if not compat.PY3: + raise nose.SkipTest('build in round cannot be overriden prior to Python 3') + + # GH11763 + # Here's the test frame we'll be working with + df = DataFrame( + {'col1': [1.123, 2.123, 3.123], 'col2': [1.234, 2.234, 3.234]}) + + # Default round to integer (i.e. decimals=0) + expected_rounded = DataFrame( + {'col1': [1., 2., 3.], 'col2': [1., 2., 3.]}) + tm.assert_frame_equal(round(df), expected_rounded) + def test_quantile(self): from numpy import percentile diff --git a/pandas/tests/test_panel.py b/pandas/tests/test_panel.py index 1f8bcf8c9879f..f12d851a6772d 100644 --- a/pandas/tests/test_panel.py +++ b/pandas/tests/test_panel.py @@ -1901,6 +1901,22 @@ def test_pct_change(self): 'i3': DataFrame({'c1': [2, 1, .4], 'c2': [2./3, .5, 1./3]})}) assert_panel_equal(result, expected) + + def test_round(self): + values = [[[-3.2,2.2],[0,-4.8213],[3.123,123.12], + [-1566.213,88.88],[-12,94.5]], + [[-5.82,3.5],[6.21,-73.272], [-9.087,23.12], + [272.212,-99.99],[23,-76.5]]] + evalues = [[[float(np.around(i)) for i in j] for j in k] for k in values] + p = Panel(values, items=['Item1', 'Item2'], + major_axis=pd.date_range('1/1/2000', periods=5), + minor_axis=['A','B']) + expected = Panel(evalues, items=['Item1', 'Item2'], + major_axis=pd.date_range('1/1/2000', periods=5), + minor_axis=['A','B']) + result = p.round() + self.assert_panel_equal(expected, result) + def test_multiindex_get(self): ind = MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)], diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py index bbdd7c3637981..34d2d0de35977 100644 --- a/pandas/tests/test_series.py +++ b/pandas/tests/test_series.py @@ -1,4 +1,4 @@ -# coding=utf-8 +# coding=utf-8 # pylint: disable-msg=E1101,W0612 import re @@ -3003,11 +3003,26 @@ def _check_accum_op(self, name): def test_round(self): # numpy.round doesn't preserve metadata, probably a numpy bug, # re: GH #314 - result = np.round(self.ts, 2) + result = self.ts.round(2) expected = Series(np.round(self.ts.values, 2), index=self.ts.index, name='ts') assert_series_equal(result, expected) self.assertEqual(result.name, self.ts.name) + + def test_built_in_round(self): + if not compat.PY3: + raise nose.SkipTest('build in round cannot be overriden prior to Python 3') + + s = Series([1.123, 2.123, 3.123], index=lrange(3)) + result = round(s) + expected_rounded0 = Series([1., 2., 3.], index=lrange(3)) + self.assert_series_equal(result, expected_rounded0) + + decimals = 2 + expected_rounded = Series([1.12, 2.12, 3.12], index=lrange(3)) + result = round(s, decimals) + self.assert_series_equal(result, expected_rounded) + def test_prod_numpy16_bug(self): s = Series([1., 1., 1.], index=lrange(3))