diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcbd816..251070a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: matrix: python-version: [3.9, "3.10", "3.11"] numpy: ["numpy>=1.20.3,<2.0.0"] - pandas: ["pandas==2.0.2", ] + pandas: ["pandas==2.0.2", "pandas==2.1.0rc0" ] pint: ["pint>=0.21.1", "pint==0.22"] runs-on: ubuntu-latest diff --git a/CHANGES b/CHANGES index 598fb77..c63d8f7 100644 --- a/CHANGES +++ b/CHANGES @@ -5,6 +5,10 @@ pint-pandas Changelog ---------------- - ReadTheDocs Documentation created. +- Support for Pandas version 2.1.0. #196 +- Support for dtype-preserving `PintArray.map` for both Pandas 2.0.2 and Pandas 2.1. #196 +- Support for values in columns with integer magnitudes +- Support for magnitudes of any type, such as complex128 or tuples #146 - Support for pandas 2.0, allowing `.cumsum, .cummax, .cummin` methods for `Series` and `DataFrame`. #186 - Minimum Pint version is 0.21 - Minimum Pandas vesrion is 2.0 diff --git a/pint_pandas/pint_array.py b/pint_pandas/pint_array.py index 2b5c40f..7154be5 100644 --- a/pint_pandas/pint_array.py +++ b/pint_pandas/pint_array.py @@ -2,11 +2,12 @@ import re import warnings from collections import OrderedDict +from importlib.metadata import version import numpy as np import pandas as pd import pint -from pandas import DataFrame, Series +from pandas import DataFrame, Series, Index from pandas.api.extensions import ( ExtensionArray, ExtensionDtype, @@ -27,6 +28,11 @@ # quantify/dequantify NO_UNIT = "No Unit" +pandas_version = version("pandas") +pandas_version_info = tuple( + int(x) if x.isdigit() else x for x in pandas_version.split(".") +) + class PintType(ExtensionDtype): """ @@ -65,7 +71,7 @@ def __new__(cls, units=None): if not isinstance(units, _Unit): units = cls._parse_dtype_strict(units) # ureg.unit returns a quantity with a magnitude of 1 - # eg 1 mm. Initialising a quantity and taking it's unit + # eg 1 mm. Initialising a quantity and taking its unit # TODO: Seperate units from quantities in pint # to simplify this bit units = cls.ureg.Quantity(1, units).units @@ -185,6 +191,12 @@ def __repr__(self): return self.name +_NumpyEADtype = ( + pd.core.dtypes.dtypes.PandasDtype + if pandas_version_info < (2, 1) + else pd.core.dtypes.dtypes.NumpyEADtype +) + dtypemap = { int: pd.Int64Dtype(), np.int64: pd.Int64Dtype(), @@ -195,8 +207,8 @@ def __repr__(self): float: pd.Float64Dtype(), np.float64: pd.Float64Dtype(), np.float32: pd.Float32Dtype(), - np.complex128: pd.core.dtypes.dtypes.PandasDtype("complex128"), - np.complex64: pd.core.dtypes.dtypes.PandasDtype("complex64"), + np.complex128: _NumpyEADtype("complex128"), + np.complex64: _NumpyEADtype("complex64"), # np.float16: pd.Float16Dtype(), } dtypeunmap = {v: k for k, v in dtypemap.items()} @@ -250,7 +262,6 @@ def __init__(self, values, dtype=None, copy=False): copy = False elif not isinstance(values, pd.core.arrays.numeric.NumericArray): values = pd.array(values, copy=copy) - copy = False if copy: values = values.copy() self._data = values @@ -311,10 +322,14 @@ def __setitem__(self, key, value): if isinstance(value, _Quantity): value = value.to(self.units).magnitude - elif is_list_like(value) and len(value) > 0 and isinstance(value[0], _Quantity): - value = [item.to(self.units).magnitude for item in value] + elif is_list_like(value) and len(value) > 0: + if isinstance(value[0], _Quantity): + value = [item.to(self.units).magnitude for item in value] + if len(value) == 1: + value = value[0] key = check_array_indexer(self, key) + # Filter out invalid values for our array type(s) try: self._data[key] = value except IndexError as e: @@ -458,7 +473,8 @@ def take(self, indices, allow_fill=False, fill_value=None): Examples -------- """ - from pandas.core.algorithms import take, is_scalar + from pandas.core.algorithms import take + from pandas.api.types import is_scalar data = self._data if allow_fill and fill_value is None: @@ -470,7 +486,10 @@ def take(self, indices, allow_fill=False, fill_value=None): # magnitude is in fact an array scalar, which will get rejected by pandas. fill_value = fill_value[()] - result = take(data, indices, fill_value=fill_value, allow_fill=allow_fill) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # Turn off warning that PandasArray is deprecated for ``take`` + result = take(data, indices, fill_value=fill_value, allow_fill=allow_fill) return PintArray(result, dtype=self.dtype) @@ -512,18 +531,12 @@ def _from_sequence(cls, scalars, dtype=None, copy=False): raise ValueError( "Cannot infer dtype. No dtype specified and empty array" ) - if dtype is None and not isinstance(master_scalar, _Quantity): - raise ValueError("No dtype specified and not a sequence of quantities") - if dtype is None and isinstance(master_scalar, _Quantity): + if dtype is None: + if not isinstance(master_scalar, _Quantity): + raise ValueError("No dtype specified and not a sequence of quantities") dtype = PintType(master_scalar.units) - def quantify_nan(item): - if type(item) is float: - return item * dtype.units - return item - if isinstance(master_scalar, _Quantity): - scalars = [quantify_nan(item) for item in scalars] scalars = [ (item.to(dtype.units).magnitude if hasattr(item, "to") else item) for item in scalars @@ -538,10 +551,21 @@ def _from_sequence_of_strings(cls, scalars, dtype=None, copy=False): @classmethod def _from_factorized(cls, values, original): + from pandas.api.types import infer_dtype + + if infer_dtype(values) != "object": + values = pd.array(values, copy=False) return cls(values, dtype=original.dtype) def _values_for_factorize(self): - return self._data._values_for_factorize() + # factorize can now handle differentiating various types of null values. + # These can only occur when the array has object dtype. + # However, for backwards compatibility we only use the null for the + # provided dtype. This may be revisited in the future, see GH#48476. + arr = self._data + if arr.dtype.kind == "O": + return np.array(arr, copy=False), self.dtype.na_value + return arr._values_for_factorize() def value_counts(self, dropna=True): """ @@ -567,18 +591,19 @@ def value_counts(self, dropna=True): # compute counts on the data with no nans data = self._data - nafilt = np.isnan(data) + nafilt = pd.isna(data) + na_value = pd.NA # NA value for index, not data, so not quantified data = data[~nafilt] + index = list(set(data)) data_list = data.tolist() - index = list(set(data)) array = [data_list.count(item) for item in index] if not dropna: - index.append(np.nan) + index.append(na_value) array.append(nafilt.sum()) - return Series(array, index=index) + return Series(np.asarray(array), index=index) def unique(self): """Compute the PintArray of unique values. @@ -589,7 +614,8 @@ def unique(self): """ from pandas import unique - return self._from_sequence(unique(self._data), dtype=self.dtype) + data = self._data + return self._from_sequence(unique(data), dtype=self.dtype) def __contains__(self, item) -> bool: if not isinstance(item, _Quantity): @@ -691,7 +717,7 @@ def convert_values(param): else: return param - if isinstance(other, (Series, DataFrame)): + if isinstance(other, (Series, DataFrame, Index)): return NotImplemented lvalues = self.quantity validate_length(lvalues, other) @@ -740,7 +766,9 @@ def __array__(self, dtype=None, copy=False): def _to_array_of_quantity(self, copy=False): qtys = [ - self._Q(item, self._dtype.units) if not pd.isna(item) else item + self._Q(item, self._dtype.units) + if not pd.isna(item) + else self.dtype.na_value for item in self._data ] with warnings.catch_warnings(record=True): @@ -798,7 +826,42 @@ def searchsorted(self, value, side="left", sorter=None): value = [item.to(self.units).magnitude for item in value] return arr.searchsorted(value, side=side, sorter=sorter) - def _reduce(self, name, **kwds): + def map(self, mapper, na_action=None): + """ + Map values using an input mapping or function. + + Parameters + ---------- + mapper : function, dict, or Series + Mapping correspondence. + na_action : {None, 'ignore'}, default None + If 'ignore', propagate NA values, without passing them to the + mapping correspondence. If 'ignore' is not supported, a + ``NotImplementedError`` should be raised. + + Returns + ------- + If mapper is a function, operate on the magnitudes of the array and + + """ + if pandas_version_info < (2, 1): + ser = pd.Series(self._to_array_of_quantity()) + arr = ser.map(mapper, na_action).values + else: + from pandas.core.algorithms import map_array + + arr = map_array(self, mapper, na_action) + + master_scalar = None + try: + master_scalar = next(i for i in arr if hasattr(i, "units")) + except StopIteration: + # JSON mapper formatting Qs as str don't create PintArrays + # ...and that's OK. Caller will get array of values + return arr + return PintArray._from_sequence(arr, PintType(master_scalar.units)) + + def _reduce(self, name, *, skipna: bool = True, keepdims: bool = False, **kwds): """ Return a scalar result of performing the reduction operation. @@ -842,14 +905,20 @@ def _reduce(self, name, **kwds): if isinstance(self._data, ExtensionArray): try: - result = self._data._reduce(name, **kwds) + result = self._data._reduce( + name, skipna=skipna, keepdims=keepdims, **kwds + ) except NotImplementedError: result = functions[name](self.numpy_data, **kwds) if name in {"all", "any", "kurt", "skew"}: return result if name == "var": + if keepdims: + return PintArray(result, f"pint[({self.units})**2]") return self._Q(result, self.units**2) + if keepdims: + return PintArray(result, self.dtype) return self._Q(result, self.units) def _accumulate(self, name: str, *, skipna: bool = True, **kwds): @@ -866,7 +935,6 @@ def _accumulate(self, name: str, *, skipna: bool = True, **kwds): result = self._data._accumulate(name, **kwds) except NotImplementedError: result = functions[name](self.numpy_data, **kwds) - print(result) return self._from_sequence(result, self.units) diff --git a/pint_pandas/testsuite/test_issues.py b/pint_pandas/testsuite/test_issues.py index 316efce..95d85b2 100644 --- a/pint_pandas/testsuite/test_issues.py +++ b/pint_pandas/testsuite/test_issues.py @@ -3,12 +3,14 @@ import numpy as np import pandas as pd +import pandas._testing as tm import pytest import pint from pandas.tests.extension.base.base import BaseExtensionTests from pint.testsuite import helpers from pint_pandas import PintArray, PintType +from pint_pandas.pint_array import pandas_version_info ureg = PintType.ureg @@ -41,7 +43,7 @@ def test_force_ndarray_like(self): expected = pd.DataFrame( {0: PintArray(q_a_), 1: PintArray(q_b)}, dtype="pint[degC]" ) - self.assert_equal(result, expected) + tm.assert_equal(result, expected) finally: # restore registry @@ -64,7 +66,7 @@ def test_offset_concat(self): expected = pd.DataFrame( {0: PintArray(q_a_), 1: PintArray(q_b)}, dtype="pint[degC]" ) - self.assert_equal(result, expected) + tm.assert_equal(result, expected) # issue #141 print(PintArray(q_a)) @@ -80,7 +82,7 @@ def test_assignment_add_empty(self): result = pd.Series(data) result[[]] += data[0] expected = pd.Series(data) - self.assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) class TestIssue80: @@ -167,3 +169,19 @@ def test_issue_127(): a = PintType.construct_from_string("pint[dimensionless]") b = PintType.construct_from_string("pint[]") assert a == b + + +class TestIssue174(BaseExtensionTests): + def test_sum(self): + if pandas_version_info < (2, 1): + pytest.skip("Pandas reduce functions strip units prior to version 2.1.0") + a = pd.DataFrame([[0, 1, 2], [3, 4, 5]]).astype("pint[m]") + row_sum = a.sum(axis=0) + expected_1 = pd.Series([3, 5, 7], dtype="pint[m]") + + tm.assert_series_equal(row_sum, expected_1) + + col_sum = a.sum(axis=1) + expected_2 = pd.Series([3, 12], dtype="pint[m]") + + tm.assert_series_equal(col_sum, expected_2) diff --git a/pint_pandas/testsuite/test_pandas_extensiontests.py b/pint_pandas/testsuite/test_pandas_extensiontests.py index 698cbb5..1427baa 100644 --- a/pint_pandas/testsuite/test_pandas_extensiontests.py +++ b/pint_pandas/testsuite/test_pandas_extensiontests.py @@ -11,7 +11,7 @@ from pandas.tests.extension import base from pandas.tests.extension.conftest import ( as_frame, # noqa: F401 - as_array, # noqa: F401, + as_array, # noqa: F401 as_series, # noqa: F401 fillna_method, # noqa: F401 groupby_apply_op, # noqa: F401 @@ -22,7 +22,7 @@ from pint.errors import DimensionalityError from pint_pandas import PintArray, PintType -from pint_pandas.pint_array import dtypemap +from pint_pandas.pint_array import dtypemap, pandas_version_info ureg = PintType.ureg @@ -133,7 +133,6 @@ def data_for_grouping(numeric_dtype): a = 1.0 b = 2.0**32 + 1 c = 2.0**32 + 10 - numeric_dtype = dtypemap.get(numeric_dtype, numeric_dtype) return PintArray.from_1darray_quantity( ureg.Quantity( @@ -185,7 +184,7 @@ def all_compare_operators(request): return request.param -# commented functions aren't implemented +# commented functions aren't implemented in numpy/pandas _all_numeric_reductions = [ "sum", "max", @@ -275,7 +274,7 @@ def test_groupby_apply_identity(self, data_for_grouping): index=pd.Index([1, 2, 3, 4], name="A"), name="B", ) - self.assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) @pytest.mark.xfail(run=True, reason="assert_frame_equal issue") @pytest.mark.parametrize("as_index", [True, False]) @@ -287,10 +286,10 @@ def test_groupby_extension_agg(self, as_index, data_for_grouping): if as_index: index = pd.Index._with_infer(uniques, name="B") expected = pd.Series([3.0, 1.0, 4.0], index=index, name="A") - self.assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) else: expected = pd.DataFrame({"B": uniques, "A": [3.0, 1.0, 4.0]}) - self.assert_frame_equal(result, expected) + tm.assert_frame_equal(result, expected) def test_in_numeric_groupby(self, data_for_grouping): df = pd.DataFrame( @@ -314,7 +313,7 @@ def test_groupby_extension_no_sort(self, data_for_grouping): index = pd.Index._with_infer(index, name="B") expected = pd.Series([1.0, 3.0, 4.0], index=index, name="A") - self.assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) class TestInterface(base.BaseInterfaceTests): @@ -322,12 +321,114 @@ class TestInterface(base.BaseInterfaceTests): class TestMethods(base.BaseMethodsTests): + def test_apply_simple_series(self, data): + result = pd.Series(data).apply(lambda x: x * 2 + ureg.Quantity(1, x.u)) + assert isinstance(result, pd.Series) + + @pytest.mark.parametrize("na_action", [None, "ignore"]) + def test_map(self, data_missing, na_action): + s = pd.Series(data_missing) + if pandas_version_info < (2, 1) and na_action is not None: + pytest.skip( + "Pandas EA map function only accepts None as na_action parameter" + ) + result = s.map(lambda x: x, na_action=na_action) + expected = s + tm.assert_series_equal(result, expected) + @pytest.mark.skip("All values are valid as magnitudes") def test_insert_invalid(self): pass class TestArithmeticOps(base.BaseArithmeticOpsTests): + divmod_exc = None + series_scalar_exc = None + frame_scalar_exc = None + series_array_exc = None + + def _get_expected_exception( + self, op_name: str, obj, other + ): # -> type[Exception] | None, but Union types not understood by Python 3.9 + if op_name in ["__pow__", "__rpow__"]: + return DimensionalityError + if op_name in [ + "__divmod__", + "__rdivmod__", + "floor_divide", + "remainder", + "__floordiv__", + "__rfloordiv__", + "__mod__", + "__rmod__", + ]: + exc = None + if isinstance(obj, complex): + pytest.skip(f"{type(obj).__name__} does not support {op_name}") + return TypeError + if isinstance(other, complex): + pytest.skip(f"{type(other).__name__} does not support {op_name}") + return TypeError + if isinstance(obj, ureg.Quantity): + pytest.skip( + f"{type(obj.m).__name__} Quantity does not support {op_name}" + ) + return TypeError + if isinstance(other, ureg.Quantity): + pytest.skip( + f"{type(other.m).__name__} Quantity does not support {op_name}" + ) + return TypeError + if isinstance(obj, pd.Series): + try: + if obj.pint.m.dtype.kind == "c": + pytest.skip( + f"{obj.pint.m.dtype.name} {obj.dtype} does not support {op_name}" + ) + return TypeError + except AttributeError: + exc = super()._get_expected_exception(op_name, obj, other) + if exc: + return exc + if isinstance(other, pd.Series): + try: + if other.pint.m.dtype.kind == "c": + pytest.skip( + f"{other.pint.m.dtype.name} {other.dtype} does not support {op_name}" + ) + return TypeError + except AttributeError: + exc = super()._get_expected_exception(op_name, obj, other) + if exc: + return exc + if isinstance(obj, pd.DataFrame): + try: + df = obj.pint.dequantify() + for i, col in enumerate(df.columns): + if df.iloc[:, i].dtype.kind == "c": + pytest.skip( + f"{df.iloc[:, i].dtype.name} {df.dtypes[i]} does not support {op_name}" + ) + return TypeError + except AttributeError: + exc = super()._get_expected_exception(op_name, obj, other) + if exc: + return exc + if isinstance(other, pd.DataFrame): + try: + df = other.pint.dequantify() + for i, col in enumerate(df.columns): + if df.iloc[:, i].dtype.kind == "c": + pytest.skip( + f"{df.iloc[:, i].dtype.name} {df.dtypes[i]} does not support {op_name}" + ) + return TypeError + except AttributeError: + exc = super()._get_expected_exception(op_name, obj, other) + # Fall through... + return exc + + # The following methods are needed to work with Pandas < 2.1 def _check_divmod_op(self, s, op, other, exc=None): # divmod has multiple return values, so check separately if exc is None: @@ -336,8 +437,8 @@ def _check_divmod_op(self, s, op, other, exc=None): expected_div, expected_mod = s // other, s % other else: expected_div, expected_mod = other // s, other % s - self.assert_series_equal(result_div, expected_div) - self.assert_series_equal(result_mod, expected_mod) + tm.assert_series_equal(result_div, expected_div) + tm.assert_series_equal(result_mod, expected_mod) else: with pytest.raises(exc): divmod(s, other) @@ -351,36 +452,58 @@ def _get_exception(self, data, op_name): return op_name, None - @pytest.mark.parametrize("numeric_dtype", _base_numeric_dtypes, indirect=True) - def test_divmod_series_array(self, data, data_for_twos): - base.BaseArithmeticOpsTests.test_divmod_series_array(self, data, data_for_twos) - def test_arith_series_with_scalar(self, data, all_arithmetic_operators): # With Pint 0.21, series and scalar need to have compatible units for # the arithmetic to work # series & scalar - op_name, exc = self._get_exception(data, all_arithmetic_operators) - s = pd.Series(data) - self.check_opname(s, op_name, s.iloc[0], exc=exc) + if pandas_version_info < (2, 1): + op_name, exc = self._get_exception(data, all_arithmetic_operators) + s = pd.Series(data) + self.check_opname(s, op_name, s.iloc[0], exc=exc) + else: + op_name = all_arithmetic_operators + ser = pd.Series(data) + self.check_opname(ser, op_name, ser.iloc[0]) def test_arith_series_with_array(self, data, all_arithmetic_operators): # ndarray & other series - op_name, exc = self._get_exception(data, all_arithmetic_operators) - ser = pd.Series(data) - self.check_opname(ser, op_name, pd.Series([ser.iloc[0]] * len(ser)), exc) + if pandas_version_info < (2, 1): + op_name, exc = self._get_exception(data, all_arithmetic_operators) + ser = pd.Series(data) + self.check_opname(ser, op_name, pd.Series([ser.iloc[0]] * len(ser)), exc) + else: + op_name = all_arithmetic_operators + ser = pd.Series(data) + self.check_opname(ser, op_name, pd.Series([ser.iloc[0]] * len(ser))) def test_arith_frame_with_scalar(self, data, all_arithmetic_operators): # frame & scalar - op_name, exc = self._get_exception(data, all_arithmetic_operators) - df = pd.DataFrame({"A": data}) - self.check_opname(df, op_name, data[0], exc=exc) + if pandas_version_info < (2, 1): + op_name, exc = self._get_exception(data, all_arithmetic_operators) + df = pd.DataFrame({"A": data}) + self.check_opname(df, op_name, data[0], exc=exc) + else: + op_name = all_arithmetic_operators + df = pd.DataFrame({"A": data}) + self.check_opname(df, op_name, data[0]) - # parameterise this to try divisor not equal to 1 + # parameterise this to try divisor not equal to 1 Mm @pytest.mark.parametrize("numeric_dtype", _base_numeric_dtypes, indirect=True) def test_divmod(self, data): - s = pd.Series(data) - self._check_divmod_op(s, divmod, 1 * ureg.Mm) - self._check_divmod_op(1 * ureg.Mm, ops.rdivmod, s) + ser = pd.Series(data) + self._check_divmod_op(ser, divmod, 1 * ureg.Mm) + self._check_divmod_op(1 * ureg.Mm, ops.rdivmod, ser) + + @pytest.mark.parametrize("numeric_dtype", _base_numeric_dtypes, indirect=True) + def test_divmod_series_array(self, data, data_for_twos): + ser = pd.Series(data) + self._check_divmod_op(ser, divmod, data) + + other = data_for_twos + self._check_divmod_op(other, ops.rdivmod, ser) + + other = pd.Series(other) + self._check_divmod_op(other, ops.rdivmod, ser) class TestComparisonOps(base.BaseComparisonOpsTests): @@ -441,6 +564,10 @@ def check_reduce(self, s, op_name, skipna): expected = expected_m assert result == expected + @pytest.mark.skip("tests not written yet") + def check_reduce_frame(self, ser: pd.Series, op_name: str, skipna: bool): + pass + @pytest.mark.parametrize("skipna", [True, False]) def test_reduce_scaling(self, data, all_numeric_reductions, skipna): """Make sure that the reductions give the same physical result independent of the unit representation. @@ -467,6 +594,16 @@ def test_reduce_scaling(self, data, all_numeric_reductions, skipna): v_mm = r_mm assert np.isclose(v_nm, v_mm, rtol=1e-3), f"{r_nm} == {r_mm}" + @pytest.mark.parametrize("skipna", [True, False]) + def test_reduce_series_xx(self, data, all_numeric_reductions, skipna): + op_name = all_numeric_reductions + s = pd.Series(data) + + # min/max with empty produce numpy warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + self.check_reduce(s, op_name, skipna) + class TestBooleanReduce(base.BaseBooleanReduceTests): def check_reduce(self, s, op_name, skipna): @@ -507,13 +644,28 @@ def test_unstack(self, data, index, obj): class TestSetitem(base.BaseSetitemTests): @pytest.mark.parametrize("numeric_dtype", _base_numeric_dtypes, indirect=True) def test_setitem_scalar_key_sequence_raise(self, data): + # This can be removed when https://github.com/pandas-dev/pandas/pull/54441 is accepted base.BaseSetitemTests.test_setitem_scalar_key_sequence_raise(self, data) + @pytest.mark.parametrize("numeric_dtype", _base_numeric_dtypes, indirect=True) + def test_setitem_2d_values(self, data): + # GH50085 + original = data.copy() + df = pd.DataFrame({"a": data, "b": data}) + df.loc[[0, 1], :] = df.loc[[1, 0], :].values + assert (df.loc[0, :] == original[1]).all() + assert (df.loc[1, :] == original[0]).all() + class TestAccumulate(base.BaseAccumulateTests): @pytest.mark.parametrize("skipna", [True, False]) def test_accumulate_series_raises(self, data, all_numeric_accumulations, skipna): - pass + if pandas_version_info < (2, 1): + # Should this be skip? Historic code simply used pass. + pass + + def _supports_accumulation(self, ser: pd.Series, op_name: str) -> bool: + return True def check_accumulate(self, s, op_name, skipna): if op_name == "cumprod": @@ -524,4 +676,4 @@ def check_accumulate(self, s, op_name, skipna): s_unitless = pd.Series(s.values.data) expected = getattr(s_unitless, op_name)(skipna=skipna) expected = pd.Series(expected, dtype=s.dtype) - self.assert_series_equal(result, expected, check_dtype=False) + tm.assert_series_equal(result, expected, check_dtype=False)