| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- from typing import final
- import pytest
- import pandas as pd
- import pandas._testing as tm
- from pandas.api.types import is_numeric_dtype
- class BaseReduceTests:
- """
- Reduction specific tests. Generally these only
- make sense for numeric/boolean operations.
- """
- def _supports_reduction(self, ser: pd.Series, op_name: str) -> bool:
- # Specify if we expect this reduction to succeed.
- return False
- def check_reduce(self, ser: pd.Series, op_name: str, skipna: bool):
- # We perform the same operation on the np.float64 data and check
- # that the results match. Override if you need to cast to something
- # other than float64.
- res_op = getattr(ser, op_name)
- try:
- alt = ser.astype("float64")
- except (TypeError, ValueError):
- # e.g. Interval can't cast (TypeError), StringArray can't cast
- # (ValueError), so let's cast to object and do
- # the reduction pointwise
- alt = ser.astype(object)
- exp_op = getattr(alt, op_name)
- if op_name == "count":
- result = res_op()
- expected = exp_op()
- else:
- result = res_op(skipna=skipna)
- expected = exp_op(skipna=skipna)
- tm.assert_almost_equal(result, expected)
- def _get_expected_reduction_dtype(self, arr, op_name: str, skipna: bool):
- # Find the expected dtype when the given reduction is done on a DataFrame
- # column with this array. The default assumes float64-like behavior,
- # i.e. retains the dtype.
- return arr.dtype
- # We anticipate that authors should not need to override check_reduce_frame,
- # but should be able to do any necessary overriding in
- # _get_expected_reduction_dtype. If you have a use case where this
- # does not hold, please let us know at github.com/pandas-dev/pandas/issues.
- @final
- def check_reduce_frame(self, ser: pd.Series, op_name: str, skipna: bool):
- # Check that the 2D reduction done in a DataFrame reduction "looks like"
- # a wrapped version of the 1D reduction done by Series.
- arr = ser.array
- df = pd.DataFrame({"a": arr})
- kwargs = {"ddof": 1} if op_name in ["var", "std"] else {}
- cmp_dtype = self._get_expected_reduction_dtype(arr, op_name, skipna)
- # The DataFrame method just calls arr._reduce with keepdims=True,
- # so this first check is perfunctory.
- result1 = arr._reduce(op_name, skipna=skipna, keepdims=True, **kwargs)
- result2 = getattr(df, op_name)(skipna=skipna, **kwargs).array
- tm.assert_extension_array_equal(result1, result2)
- # Check that the 2D reduction looks like a wrapped version of the
- # 1D reduction
- if not skipna and ser.isna().any():
- expected = pd.array([pd.NA], dtype=cmp_dtype)
- else:
- exp_value = getattr(ser.dropna(), op_name)()
- expected = pd.array([exp_value], dtype=cmp_dtype)
- tm.assert_extension_array_equal(result1, expected)
- @pytest.mark.parametrize("skipna", [True, False])
- def test_reduce_series_boolean(self, data, all_boolean_reductions, skipna):
- op_name = all_boolean_reductions
- ser = pd.Series(data)
- if not self._supports_reduction(ser, op_name):
- # TODO: the message being checked here isn't actually checking anything
- msg = (
- "[Cc]annot perform|Categorical is not ordered for operation|"
- "does not support reduction|"
- )
- with pytest.raises(TypeError, match=msg):
- getattr(ser, op_name)(skipna=skipna)
- else:
- self.check_reduce(ser, op_name, skipna)
- @pytest.mark.filterwarnings("ignore::RuntimeWarning")
- @pytest.mark.parametrize("skipna", [True, False])
- def test_reduce_series_numeric(self, data, all_numeric_reductions, skipna):
- op_name = all_numeric_reductions
- ser = pd.Series(data)
- if not self._supports_reduction(ser, op_name):
- # TODO: the message being checked here isn't actually checking anything
- msg = (
- "[Cc]annot perform|Categorical is not ordered for operation|"
- "does not support reduction|"
- )
- with pytest.raises(TypeError, match=msg):
- getattr(ser, op_name)(skipna=skipna)
- else:
- # min/max with empty produce numpy warnings
- self.check_reduce(ser, op_name, skipna)
- @pytest.mark.parametrize("skipna", [True, False])
- def test_reduce_frame(self, data, all_numeric_reductions, skipna):
- op_name = all_numeric_reductions
- ser = pd.Series(data)
- if not is_numeric_dtype(ser.dtype):
- pytest.skip(f"{ser.dtype} is not numeric dtype")
- if op_name in ["count", "kurt", "sem"]:
- pytest.skip(f"{op_name} not an array method")
- if not self._supports_reduction(ser, op_name):
- pytest.skip(f"Reduction {op_name} not supported for this dtype")
- self.check_reduce_frame(ser, op_name, skipna)
- # TODO(3.0): remove BaseNoReduceTests, BaseNumericReduceTests,
- # BaseBooleanReduceTests
- class BaseNoReduceTests(BaseReduceTests):
- """we don't define any reductions"""
- class BaseNumericReduceTests(BaseReduceTests):
- # For backward compatibility only, this only runs the numeric reductions
- def _supports_reduction(self, ser: pd.Series, op_name: str) -> bool:
- if op_name in ["any", "all"]:
- pytest.skip("These are tested in BaseBooleanReduceTests")
- return True
- class BaseBooleanReduceTests(BaseReduceTests):
- # For backward compatibility only, this only runs the numeric reductions
- def _supports_reduction(self, ser: pd.Series, op_name: str) -> bool:
- if op_name not in ["any", "all"]:
- pytest.skip("These are tested in BaseNumericReduceTests")
- return True
|