From e9fd2cf7cff570c239dffbe18f8cb9605e420e42 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Sun, 2 May 2021 20:04:55 -0400 Subject: [PATCH] BUG: groupby.agg/transform casts UDF results (#40790) --- doc/source/user_guide/gotchas.rst | 2 +- doc/source/user_guide/groupby.rst | 31 ++++++++- doc/source/whatsnew/v1.3.0.rst | 30 +++++++++ pandas/core/frame.py | 2 +- pandas/core/groupby/generic.py | 41 +++++++----- pandas/core/groupby/groupby.py | 67 ++++++++++++++----- pandas/core/series.py | 2 +- pandas/core/shared_docs.py | 4 +- .../tests/groupby/aggregate/test_aggregate.py | 62 +++++++++++++++-- pandas/tests/groupby/aggregate/test_cython.py | 3 + pandas/tests/groupby/test_categorical.py | 2 +- pandas/tests/groupby/test_function.py | 5 +- pandas/tests/groupby/test_groupby.py | 5 +- .../tests/groupby/transform/test_transform.py | 8 +-- pandas/tests/resample/test_datetime_index.py | 5 ++ .../tests/resample/test_resampler_grouper.py | 4 +- pandas/tests/resample/test_timedelta.py | 2 +- pandas/tests/reshape/test_crosstab.py | 2 + pandas/tests/reshape/test_pivot.py | 1 - 19 files changed, 221 insertions(+), 57 deletions(-) diff --git a/doc/source/user_guide/gotchas.rst b/doc/source/user_guide/gotchas.rst index 180f833a2753d..1de978b195382 100644 --- a/doc/source/user_guide/gotchas.rst +++ b/doc/source/user_guide/gotchas.rst @@ -178,7 +178,7 @@ To test for membership in the values, use the method :meth:`~pandas.Series.isin` For ``DataFrames``, likewise, ``in`` applies to the column axis, testing for membership in the list of column names. -.. _udf-mutation: +.. _gotchas.udf-mutation: Mutating with User Defined Function (UDF) methods ------------------------------------------------- diff --git a/doc/source/user_guide/groupby.rst b/doc/source/user_guide/groupby.rst index afb2e72cbff07..3f596388ca226 100644 --- a/doc/source/user_guide/groupby.rst +++ b/doc/source/user_guide/groupby.rst @@ -739,6 +739,26 @@ optimized Cython implementations: Of course ``sum`` and ``mean`` are implemented on pandas objects, so the above code would work even without the special versions via dispatching (see below). +.. _groupby.aggregate.udfs: + +Aggregations with User-Defined Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Users can also provide their own functions for custom aggregations. When aggregating +with a User-Defined Function (UDF), the UDF should not mutate the provided ``Series``, see +:ref:`gotchas.udf-mutation` for more information. + +.. ipython:: python + + animals.groupby("kind")[["height"]].agg(lambda x: set(x)) + +The resulting dtype will reflect that of the aggregating function. If the results from different groups have +different dtypes, then a common dtype will be determined in the same way as ``DataFrame`` construction. + +.. ipython:: python + + animals.groupby("kind")[["height"]].agg(lambda x: x.astype(int).sum()) + .. _groupby.transform: Transformation @@ -759,7 +779,11 @@ as the one being grouped. The transform function must: * (Optionally) operates on the entire group chunk. If this is supported, a fast path is used starting from the *second* chunk. -For example, suppose we wished to standardize the data within each group: +Similar to :ref:`groupby.aggregate.udfs`, the resulting dtype will reflect that of the +transformation function. If the results from different groups have different dtypes, then +a common dtype will be determined in the same way as ``DataFrame`` construction. + +Suppose we wished to standardize the data within each group: .. ipython:: python @@ -1065,13 +1089,16 @@ that is itself a series, and possibly upcast the result to a DataFrame: s s.apply(f) - .. note:: ``apply`` can act as a reducer, transformer, *or* filter function, depending on exactly what is passed to it. So depending on the path taken, and exactly what you are grouping. Thus the grouped columns(s) may be included in the output as well as set the indices. +Similar to :ref:`groupby.aggregate.udfs`, the resulting dtype will reflect that of the +apply function. If the results from different groups have different dtypes, then +a common dtype will be determined in the same way as ``DataFrame`` construction. + Numba Accelerated Routines -------------------------- diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 658e68d1465e9..8ad8eaff6cd63 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -298,6 +298,36 @@ Preserve dtypes in :meth:`~pandas.DataFrame.combine_first` combined.dtypes +Group by methods agg and transform no longer changes return dtype for callables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously the methods :meth:`.DataFrameGroupBy.aggregate`, +:meth:`.SeriesGroupBy.aggregate`, :meth:`.DataFrameGroupBy.transform`, and +:meth:`.SeriesGroupBy.transform` might cast the result dtype when the argument ``func`` +is callable, possibly leading to undesirable results (:issue:`21240`). The cast would +occur if the result is numeric and casting back to the input dtype does not change any +values as measured by ``np.allclose``. Now no such casting occurs. + +.. ipython:: python + + df = pd.DataFrame({'key': [1, 1], 'a': [True, False], 'b': [True, True]}) + df + +*pandas 1.2.x* + +.. code-block:: ipython + + In [5]: df.groupby('key').agg(lambda x: x.sum()) + Out[5]: + a b + key + 1 True 2 + +*pandas 1.3.0* + +.. ipython:: python + + df.groupby('key').agg(lambda x: x.sum()) Try operating inplace when setting values with ``loc`` and ``iloc`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 69e88cd221f6d..8e12a8cb18b68 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -8552,7 +8552,7 @@ def apply( Notes ----- Functions that mutate the passed object can produce unexpected - behavior or errors and are not supported. See :ref:`udf-mutation` + behavior or errors and are not supported. See :ref:`gotchas.udf-mutation` for more details. Examples diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index 7edd458ced790..fd4b0cfa87950 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -44,10 +44,6 @@ doc, ) -from pandas.core.dtypes.cast import ( - find_common_type, - maybe_downcast_numeric, -) from pandas.core.dtypes.common import ( ensure_int64, is_bool, @@ -226,7 +222,16 @@ def _selection_name(self): ... ) minimum maximum 1 1 2 - 2 3 4""" + 2 3 4 + + .. versionchanged:: 1.3.0 + + The resulting dtype will reflect the return value of the aggregating function. + + >>> s.groupby([1, 1, 2, 2]).agg(lambda x: x.astype(float).min()) + 1 1.0 + 2 3.0 + dtype: float64""" ) @Appender( @@ -566,8 +571,9 @@ def transform(self, func, *args, engine=None, engine_kwargs=None, **kwargs): def _transform_general(self, func, *args, **kwargs): """ - Transform with a non-str `func`. + Transform with a callable func`. """ + assert callable(func) klass = type(self._selected_obj) results = [] @@ -589,13 +595,6 @@ def _transform_general(self, func, *args, **kwargs): result = self._set_result_index_ordered(concatenated) else: result = self.obj._constructor(dtype=np.float64) - # we will only try to coerce the result type if - # we have a numeric dtype, as these are *always* user-defined funcs - # the cython take a different path (and casting) - if is_numeric_dtype(result.dtype): - common_dtype = find_common_type([self._selected_obj.dtype, result.dtype]) - if common_dtype is result.dtype: - result = maybe_downcast_numeric(result, self._selected_obj.dtype) result.name = self._selected_obj.name return result @@ -625,7 +624,7 @@ def filter(self, func, dropna=True, *args, **kwargs): Notes ----- Functions that mutate the passed object can produce unexpected - behavior or errors and are not supported. See :ref:`udf-mutation` + behavior or errors and are not supported. See :ref:`gotchas.udf-mutation` for more details. Examples @@ -1006,7 +1005,17 @@ class DataFrameGroupBy(GroupBy[DataFrame]): ``['column', 'aggfunc']`` to make it clearer what the arguments are. As usual, the aggregation can be a callable or a string alias. - See :ref:`groupby.aggregate.named` for more.""" + See :ref:`groupby.aggregate.named` for more. + + .. versionchanged:: 1.3.0 + + The resulting dtype will reflect the return value of the aggregating function. + + >>> df.groupby("A")[["B"]].agg(lambda x: x.astype(float).min()) + B + A + 1 1.0 + 2 3.0""" ) @doc(_agg_template, examples=_agg_examples_doc, klass="DataFrame") @@ -1533,7 +1542,7 @@ def filter(self, func, dropna=True, *args, **kwargs): which group you are working on. Functions that mutate the passed object can produce unexpected - behavior or errors and are not supported. See :ref:`udf-mutation` + behavior or errors and are not supported. See :ref:`gotchas.udf-mutation` for more details. Examples diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index 2ce928ec61760..55c3bfd316462 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -158,6 +158,11 @@ class providing the base-class of operations. side-effects, as they will take effect twice for the first group. + .. versionchanged:: 1.3.0 + + The resulting dtype will reflect the return value of the passed ``func``, + see the examples below. + Examples -------- {examples} @@ -165,7 +170,7 @@ class providing the base-class of operations. "dataframe_examples": """ >>> df = pd.DataFrame({'A': 'a a b'.split(), ... 'B': [1,2,3], - ... 'C': [4,6, 5]}) + ... 'C': [4,6,5]}) >>> g = df.groupby('A') Notice that ``g`` has two groups, ``a`` and ``b``. @@ -183,13 +188,17 @@ class providing the base-class of operations. Example 2: The function passed to `apply` takes a DataFrame as its argument and returns a Series. `apply` combines the result for - each group together into a new DataFrame: + each group together into a new DataFrame. + + .. versionchanged:: 1.3.0 - >>> g[['B', 'C']].apply(lambda x: x.max() - x.min()) - B C + The resulting dtype will reflect the return value of the passed ``func``. + + >>> g[['B', 'C']].apply(lambda x: x.astype(float).max() - x.min()) + B C A - a 1 2 - b 0 0 + a 1.0 2.0 + b 0.0 0.0 Example 3: The function passed to `apply` takes a DataFrame as its argument and returns a scalar. `apply` combines the result for @@ -210,12 +219,16 @@ class providing the base-class of operations. Example 1: The function passed to `apply` takes a Series as its argument and returns a Series. `apply` combines the result for - each group together into a new Series: + each group together into a new Series. + + .. versionchanged:: 1.3.0 - >>> g.apply(lambda x: x*2 if x.name == 'b' else x/2) + The resulting dtype will reflect the return value of the passed ``func``. + + >>> g.apply(lambda x: x*2 if x.name == 'a' else x/2) a 0.0 - a 0.5 - b 4.0 + a 2.0 + b 1.0 dtype: float64 Example 2: The function passed to `apply` takes a Series as @@ -367,12 +380,17 @@ class providing the base-class of operations. in the subframe. If f also supports application to the entire subframe, then a fast path is used starting from the second chunk. * f must not mutate groups. Mutation is not supported and may - produce unexpected results. See :ref:`udf-mutation` for more details. + produce unexpected results. See :ref:`gotchas.udf-mutation` for more details. When using ``engine='numba'``, there will be no "fall back" behavior internally. The group data and group index will be passed as numpy arrays to the JITed user defined function, and no alternative execution attempts will be tried. +.. versionchanged:: 1.3.0 + + The resulting dtype will reflect the return value of the passed ``func``, + see the examples below. + Examples -------- @@ -402,6 +420,20 @@ class providing the base-class of operations. 3 3 8.0 4 4 6.0 5 3 8.0 + +.. versionchanged:: 1.3.0 + + The resulting dtype will reflect the return value of the passed ``func``, + for example: + +>>> grouped[['C', 'D']].transform(lambda x: x.astype(int).max()) + C D +0 5 8 +1 5 9 +2 5 8 +3 5 9 +4 5 8 +5 5 9 """ _agg_template = """ @@ -469,12 +501,16 @@ class providing the base-class of operations. When using ``engine='numba'``, there will be no "fall back" behavior internally. The group data and group index will be passed as numpy arrays to the JITed user defined function, and no alternative execution attempts will be tried. -{examples} Functions that mutate the passed object can produce unexpected -behavior or errors and are not supported. See :ref:`udf-mutation` +behavior or errors and are not supported. See :ref:`gotchas.udf-mutation` for more details. -""" + +.. versionchanged:: 1.3.0 + + The resulting dtype will reflect the return value of the passed ``func``, + see the examples below. +{examples}""" @final @@ -1232,9 +1268,6 @@ def _python_agg_general(self, func, *args, **kwargs): assert result is not None key = base.OutputKey(label=name, position=idx) - if is_numeric_dtype(obj.dtype): - result = maybe_downcast_numeric(result, obj.dtype) - if self.grouper._filter_empty_groups: mask = counts.ravel() > 0 diff --git a/pandas/core/series.py b/pandas/core/series.py index ed9ce848678a0..a07a685c2ffde 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -4190,7 +4190,7 @@ def apply( Notes ----- Functions that mutate the passed object can produce unexpected - behavior or errors and are not supported. See :ref:`udf-mutation` + behavior or errors and are not supported. See :ref:`gotchas.udf-mutation` for more details. Examples diff --git a/pandas/core/shared_docs.py b/pandas/core/shared_docs.py index a4ee4bb636450..a3fa24c7ee1e0 100644 --- a/pandas/core/shared_docs.py +++ b/pandas/core/shared_docs.py @@ -42,7 +42,7 @@ `agg` is an alias for `aggregate`. Use the alias. Functions that mutate the passed object can produce unexpected -behavior or errors and are not supported. See :ref:`udf-mutation` +behavior or errors and are not supported. See :ref:`gotchas.udf-mutation` for more details. A passed user-defined-function will be passed a Series for evaluation. @@ -303,7 +303,7 @@ Notes ----- Functions that mutate the passed object can produce unexpected -behavior or errors and are not supported. See :ref:`udf-mutation` +behavior or errors and are not supported. See :ref:`gotchas.udf-mutation` for more details. Examples diff --git a/pandas/tests/groupby/aggregate/test_aggregate.py b/pandas/tests/groupby/aggregate/test_aggregate.py index c61360e67e505..b601ba92886d9 100644 --- a/pandas/tests/groupby/aggregate/test_aggregate.py +++ b/pandas/tests/groupby/aggregate/test_aggregate.py @@ -234,11 +234,10 @@ def test_aggregate_item_by_item(df): K = len(result.columns) # GH5782 - # odd comparisons can result here, so cast to make easy - exp = Series(np.array([foo] * K), index=list("BCD"), dtype=np.float64, name="foo") + exp = Series(np.array([foo] * K), index=list("BCD"), name="foo") tm.assert_series_equal(result.xs("foo"), exp) - exp = Series(np.array([bar] * K), index=list("BCD"), dtype=np.float64, name="bar") + exp = Series(np.array([bar] * K), index=list("BCD"), name="bar") tm.assert_almost_equal(result.xs("bar"), exp) def aggfun(ser): @@ -442,6 +441,57 @@ def test_bool_agg_dtype(op): assert is_integer_dtype(result) +@pytest.mark.parametrize( + "keys, agg_index", + [ + (["a"], Index([1], name="a")), + (["a", "b"], MultiIndex([[1], [2]], [[0], [0]], names=["a", "b"])), + ], +) +@pytest.mark.parametrize( + "input_dtype", ["bool", "int32", "int64", "float32", "float64"] +) +@pytest.mark.parametrize( + "result_dtype", ["bool", "int32", "int64", "float32", "float64"] +) +@pytest.mark.parametrize("method", ["apply", "aggregate", "transform"]) +def test_callable_result_dtype_frame( + keys, agg_index, input_dtype, result_dtype, method +): + # GH 21240 + df = DataFrame({"a": [1], "b": [2], "c": [True]}) + df["c"] = df["c"].astype(input_dtype) + op = getattr(df.groupby(keys)[["c"]], method) + result = op(lambda x: x.astype(result_dtype).iloc[0]) + expected_index = pd.RangeIndex(0, 1) if method == "transform" else agg_index + expected = DataFrame({"c": [df["c"].iloc[0]]}, index=expected_index).astype( + result_dtype + ) + if method == "apply": + expected.columns.names = [0] + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize( + "keys, agg_index", + [ + (["a"], Index([1], name="a")), + (["a", "b"], MultiIndex([[1], [2]], [[0], [0]], names=["a", "b"])), + ], +) +@pytest.mark.parametrize("input", [True, 1, 1.0]) +@pytest.mark.parametrize("dtype", [bool, int, float]) +@pytest.mark.parametrize("method", ["apply", "aggregate", "transform"]) +def test_callable_result_dtype_series(keys, agg_index, input, dtype, method): + # GH 21240 + df = DataFrame({"a": [1], "b": [2], "c": [input]}) + op = getattr(df.groupby(keys)["c"], method) + result = op(lambda x: x.astype(dtype).iloc[0]) + expected_index = pd.RangeIndex(0, 1) if method == "transform" else agg_index + expected = Series([df["c"].iloc[0]], index=expected_index, name="c").astype(dtype) + tm.assert_series_equal(result, expected) + + def test_order_aggregate_multiple_funcs(): # GH 25692 df = DataFrame({"A": [1, 1, 2, 2], "B": [1, 2, 3, 4]}) @@ -849,7 +899,11 @@ def test_multiindex_custom_func(func): data = [[1, 4, 2], [5, 7, 1]] df = DataFrame(data, columns=MultiIndex.from_arrays([[1, 1, 2], [3, 4, 3]])) result = df.groupby(np.array([0, 1])).agg(func) - expected_dict = {(1, 3): {0: 1, 1: 5}, (1, 4): {0: 4, 1: 7}, (2, 3): {0: 2, 1: 1}} + expected_dict = { + (1, 3): {0: 1.0, 1: 5.0}, + (1, 4): {0: 4.0, 1: 7.0}, + (2, 3): {0: 2.0, 1: 1.0}, + } expected = DataFrame(expected_dict) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/groupby/aggregate/test_cython.py b/pandas/tests/groupby/aggregate/test_cython.py index 4a8aabe41b754..ded10ab11d5a8 100644 --- a/pandas/tests/groupby/aggregate/test_cython.py +++ b/pandas/tests/groupby/aggregate/test_cython.py @@ -196,6 +196,9 @@ def test_cython_agg_empty_buckets(op, targop, observed): g = df.groupby(pd.cut(df[0], grps), observed=observed) expected = g.agg(lambda x: targop(x)) + if observed and op not in ("min", "max"): + # TODO: GH 41137 + expected = expected.astype("int64") tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/groupby/test_categorical.py b/pandas/tests/groupby/test_categorical.py index 0509b3e165bcb..7349664614614 100644 --- a/pandas/tests/groupby/test_categorical.py +++ b/pandas/tests/groupby/test_categorical.py @@ -1619,7 +1619,7 @@ def test_aggregate_categorical_with_isnan(): index = MultiIndex.from_arrays([[1, 1], [1, 2]], names=("A", "B")) expected = DataFrame( data={ - "numerical_col": [1.0, 0.0], + "numerical_col": [1, 0], "object_col": [0, 0], "categorical_col": [0, 0], }, diff --git a/pandas/tests/groupby/test_function.py b/pandas/tests/groupby/test_function.py index 163303168c240..3f43c34b6eb34 100644 --- a/pandas/tests/groupby/test_function.py +++ b/pandas/tests/groupby/test_function.py @@ -439,7 +439,8 @@ def test_median_empty_bins(observed): result = df.groupby(bins, observed=observed).median() expected = df.groupby(bins, observed=observed).agg(lambda x: x.median()) - tm.assert_frame_equal(result, expected) + # TODO: GH 41137 + tm.assert_frame_equal(result, expected, check_dtype=False) @pytest.mark.parametrize( @@ -619,7 +620,7 @@ def test_ops_general(op, targop): df = DataFrame(np.random.randn(1000)) labels = np.random.randint(0, 50, size=1000).astype(float) - result = getattr(df.groupby(labels), op)().astype(float) + result = getattr(df.groupby(labels), op)() expected = df.groupby(labels).agg(targop) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index 884966ca6e85c..abfa2a23a4402 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -299,10 +299,9 @@ def f(x): return float(len(x)) agged = grouped.agg(f) - expected = Series([4, 2], index=["bar", "foo"]) + expected = Series([4.0, 2.0], index=["bar", "foo"]) - tm.assert_series_equal(agged, expected, check_dtype=False) - assert issubclass(agged.dtype.type, np.dtype(dtype).type) + tm.assert_series_equal(agged, expected) def test_indices_concatenation_order(): diff --git a/pandas/tests/groupby/transform/test_transform.py b/pandas/tests/groupby/transform/test_transform.py index 14c117bf7257a..b22e4749bfdfc 100644 --- a/pandas/tests/groupby/transform/test_transform.py +++ b/pandas/tests/groupby/transform/test_transform.py @@ -242,7 +242,7 @@ def test_transform_bug(): # transforming on a datetime column df = DataFrame({"A": Timestamp("20130101"), "B": np.arange(5)}) result = df.groupby("A")["B"].transform(lambda x: x.rank(ascending=False)) - expected = Series(np.arange(5, 0, step=-1), name="B") + expected = Series(np.arange(5, 0, step=-1), name="B", dtype="float64") tm.assert_series_equal(result, expected) @@ -493,7 +493,7 @@ def test_groupby_transform_with_int(): ) with np.errstate(all="ignore"): result = df.groupby("A").transform(lambda x: (x - x.mean()) / x.std()) - expected = DataFrame({"B": np.nan, "C": [-1, 0, 1, -1, 0, 1]}) + expected = DataFrame({"B": np.nan, "C": [-1.0, 0.0, 1.0, -1.0, 0.0, 1.0]}) tm.assert_frame_equal(result, expected) # int that needs float conversion @@ -509,9 +509,9 @@ def test_groupby_transform_with_int(): expected = DataFrame({"B": np.nan, "C": concat([s1, s2])}) tm.assert_frame_equal(result, expected) - # int downcasting + # int doesn't get downcasted result = df.groupby("A").transform(lambda x: x * 2 / 2) - expected = DataFrame({"B": 1, "C": [2, 3, 4, 10, 5, -1]}) + expected = DataFrame({"B": 1.0, "C": [2.0, 3.0, 4.0, 10.0, 5.0, -1.0]}) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/resample/test_datetime_index.py b/pandas/tests/resample/test_datetime_index.py index bbe9ac6fa8094..66cb2f2291e98 100644 --- a/pandas/tests/resample/test_datetime_index.py +++ b/pandas/tests/resample/test_datetime_index.py @@ -1206,6 +1206,9 @@ def test_resample_median_bug_1688(): result = df.resample("T").apply(lambda x: x.mean()) exp = df.asfreq("T") + if dtype == "float32": + # TODO: Empty groups cause x.mean() to return float64 + exp = exp.astype("float64") tm.assert_frame_equal(result, exp) result = df.resample("T").median() @@ -1686,6 +1689,8 @@ def f(data, add_arg): df = DataFrame({"A": 1, "B": 2}, index=date_range("2017", periods=10)) result = df.groupby("A").resample("D").agg(f, multiplier) expected = df.groupby("A").resample("D").mean().multiply(multiplier) + # TODO: GH 41137 + expected = expected.astype("float64") tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/resample/test_resampler_grouper.py b/pandas/tests/resample/test_resampler_grouper.py index f5fc1888d611c..3e78d6ebf4c0c 100644 --- a/pandas/tests/resample/test_resampler_grouper.py +++ b/pandas/tests/resample/test_resampler_grouper.py @@ -258,6 +258,8 @@ def f(x): return x.resample("2s").apply(lambda y: y.sum()) result = g.apply(f) + # y.sum() results in int64 instead of int32 on 32-bit architectures + expected = expected.astype("int64") tm.assert_frame_equal(result, expected) @@ -289,7 +291,7 @@ def test_apply_columns_multilevel(): agg_dict = {col: (np.sum if col[3] == "one" else np.mean) for col in df.columns} result = df.resample("H").apply(lambda x: agg_dict[x.name](x)) expected = DataFrame( - np.array([0] * 4).reshape(2, 2), + 2 * [[0, 0.0]], index=date_range(start="2017-01-01", freq="1H", periods=2), columns=pd.MultiIndex.from_tuples( [("A", "a", "", "one"), ("B", "b", "i", "two")] diff --git a/pandas/tests/resample/test_timedelta.py b/pandas/tests/resample/test_timedelta.py index c6ee295208607..b1560623cd871 100644 --- a/pandas/tests/resample/test_timedelta.py +++ b/pandas/tests/resample/test_timedelta.py @@ -162,7 +162,7 @@ def test_resample_with_timedelta_yields_no_empty_groups(): result = df.loc["1s":, :].resample("3s").apply(lambda x: len(x)) expected = DataFrame( - [[768.0] * 4] * 12 + [[528.0] * 4], + [[768] * 4] * 12 + [[528] * 4], index=timedelta_range(start="1s", periods=13, freq="3s"), ) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/reshape/test_crosstab.py b/pandas/tests/reshape/test_crosstab.py index 44299d51a878f..62fd93026d5e2 100644 --- a/pandas/tests/reshape/test_crosstab.py +++ b/pandas/tests/reshape/test_crosstab.py @@ -559,6 +559,8 @@ def test_crosstab_with_numpy_size(self): expected = DataFrame( expected_data, index=expected_index, columns=expected_column ) + # aggfunc is np.size, resulting in integers + expected["All"] = expected["All"].astype("int64") tm.assert_frame_equal(result, expected) def test_crosstab_duplicate_names(self): diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py index 3d1c3b81c492f..2276281e3ecf8 100644 --- a/pandas/tests/reshape/test_pivot.py +++ b/pandas/tests/reshape/test_pivot.py @@ -986,7 +986,6 @@ def test_margins_dtype(self): tm.assert_frame_equal(expected, result) - @pytest.mark.xfail(reason="GH#17035 (len of floats is casted back to floats)") def test_margins_dtype_len(self): mi_val = list(product(["bar", "foo"], ["one", "two"])) + [("All", "")] mi = MultiIndex.from_tuples(mi_val, names=("A", "B"))