From d12a7a01888258a4c95fa7003916859f110075f1 Mon Sep 17 00:00:00 2001 From: Jeff Reback Date: Tue, 10 Oct 2017 09:17:01 -0400 Subject: [PATCH] COMPAT: sum/prod on all nan will remain nan regardless of bottleneck install (#17630) xref #15507 closes #9422 --- doc/source/missing_data.rst | 36 +++++ doc/source/whatsnew/v0.21.0.txt | 47 ++++++ pandas/core/generic.py | 2 +- pandas/core/nanops.py | 36 +++-- pandas/tests/frame/test_analytics.py | 73 +++++---- pandas/tests/groupby/test_aggregate.py | 2 +- pandas/tests/series/test_analytics.py | 201 +++++++++---------------- pandas/tests/test_panel.py | 2 +- pandas/tests/test_panel4d.py | 2 +- pandas/tests/test_window.py | 7 +- pandas/util/testing.py | 15 -- 11 files changed, 223 insertions(+), 200 deletions(-) diff --git a/doc/source/missing_data.rst b/doc/source/missing_data.rst index 07740d66a2186..c0b3a2e0edb30 100644 --- a/doc/source/missing_data.rst +++ b/doc/source/missing_data.rst @@ -181,6 +181,42 @@ account for missing data. For example: df.mean(1) df.cumsum() + +.. _missing_data.numeric_sum: + +Sum/Prod of Empties/Nans +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + + This behavior is now standard as of v0.21.0; previously sum/prod would give different + results if the ``bottleneck`` package was installed. See the :ref:`here `. + +With ``sum`` or ``prod`` on an empty or all-``NaN`` ``Series``, or columns of a ``DataFrame``, the result will be all-``NaN``. + +.. ipython:: python + + s = Series([np.nan]) + + s.sum() + +Summing of an empty ``Series`` + +.. ipython:: python + + pd.Series([]).sum() + +.. warning:: + + These behaviors differ from the default in ``numpy`` where an empty sum returns zero. + + .. ipython:: python + + np.nansum(np.array([np.nan])) + np.nansum(np.array([])) + + + NA values in GroupBy ~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index 2eefc7ec1b636..1c4af579d16dc 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -12,6 +12,7 @@ Highlights include: - Integration with `Apache Parquet `__, including a new top-level :func:`read_parquet` and :func:`DataFrame.to_parquet` method, see :ref:`here `. - New user-facing :class:`pandas.api.types.CategoricalDtype` for specifying categoricals independent of the data, see :ref:`here `. +- The behavior of ``sum`` and ``prod`` on all-NaN Series/DataFrames is now consistent and no longer depends on whether `bottleneck `__ is installed, see :ref:`here ` Check the :ref:`API Changes ` and :ref:`deprecations ` before updating. @@ -412,6 +413,52 @@ Current Behavior s.loc[pd.Index([True, False, True])] +.. _whatsnew_0210.api_breaking.bottleneck: + +Sum/Prod of all-NaN Series/DataFrames is now consistently NaN +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The behavior of ``sum`` and ``prod`` on all-NaN Series/DataFrames is now consistent and no longer depends on +whether `bottleneck `__ is installed. (:issue:`9422`, :issue:`15507`). + +With ``sum`` or ``prod`` on an empty or all-``NaN`` ``Series``, or columns of a ``DataFrame``, the result will be all-``NaN``. See the :ref:`docs `. + +.. ipython:: python + + s = Series([np.nan]) + +Previously NO ``bottleneck`` + +.. code_block:: ipython + + In [2]: s.sum() + Out[2]: np.nan + +Previously WITH ``bottleneck`` + +.. code_block:: ipython + + In [2]: s.sum() + Out[2]: 0.0 + +New Behavior, without regards to the bottleneck installation. + +.. ipython:: python + + s.sum() + +Note that this also changes the sum of an empty ``Series`` + +Previously regardless of ``bottlenck`` + +.. code_block:: ipython + + In [1]: pd.Series([]).sum() + Out[1]: 0 + +.. ipython:: python + + pd.Series([]).sum() .. _whatsnew_0210.api_breaking.pandas_eval: diff --git a/pandas/core/generic.py b/pandas/core/generic.py index c7ae9bbee9013..bc0f10a3f79ab 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -6990,7 +6990,7 @@ def _doc_parms(cls): ---------- axis : %(axis_descr)s skipna : boolean, default True - Exclude NA/null values. If an entire row/column is NA, the result + Exclude NA/null values. If an entire row/column is NA or empty, the result will be NA level : int or level name, default None If the axis is a MultiIndex (hierarchical), count along a diff --git a/pandas/core/nanops.py b/pandas/core/nanops.py index 388b2ecdff445..baeb869239c1e 100644 --- a/pandas/core/nanops.py +++ b/pandas/core/nanops.py @@ -18,7 +18,7 @@ is_datetime_or_timedelta_dtype, is_int_or_datetime_dtype, is_any_int_dtype) from pandas.core.dtypes.cast import _int64_max, maybe_upcast_putmask -from pandas.core.dtypes.missing import isna, notna +from pandas.core.dtypes.missing import isna, notna, na_value_for_dtype from pandas.core.config import get_option from pandas.core.common import _values_from_object @@ -89,8 +89,7 @@ def _f(*args, **kwargs): class bottleneck_switch(object): - def __init__(self, zero_value=None, **kwargs): - self.zero_value = zero_value + def __init__(self, **kwargs): self.kwargs = kwargs def __call__(self, alt): @@ -108,18 +107,20 @@ def f(values, axis=None, skipna=True, **kwds): if k not in kwds: kwds[k] = v try: - if self.zero_value is not None and values.size == 0: - if values.ndim == 1: + if values.size == 0: + + # we either return np.nan or pd.NaT + if is_numeric_dtype(values): + values = values.astype('float64') + fill_value = na_value_for_dtype(values.dtype) - # wrap the 0's if needed - if is_timedelta64_dtype(values): - return lib.Timedelta(0) - return 0 + if values.ndim == 1: + return fill_value else: result_shape = (values.shape[:axis] + values.shape[axis + 1:]) - result = np.empty(result_shape) - result.fill(0) + result = np.empty(result_shape, dtype=values.dtype) + result.fill(fill_value) return result if (_USE_BOTTLENECK and skipna and @@ -154,11 +155,16 @@ def _bn_ok_dtype(dt, name): # Bottleneck chokes on datetime64 if (not is_object_dtype(dt) and not is_datetime_or_timedelta_dtype(dt)): + # GH 15507 # bottleneck does not properly upcast during the sum # so can overflow - if name == 'nansum': - if dt.itemsize < 8: - return False + + # GH 9422 + # further we also want to preserve NaN when all elements + # are NaN, unlinke bottleneck/numpy which consider this + # to be 0 + if name in ['nansum', 'nanprod']: + return False return True return False @@ -297,7 +303,7 @@ def nanall(values, axis=None, skipna=True): @disallow('M8') -@bottleneck_switch(zero_value=0) +@bottleneck_switch() def nansum(values, axis=None, skipna=True): values, mask, dtype, dtype_max = _get_values(values, skipna, 0) dtype_sum = dtype_max diff --git a/pandas/tests/frame/test_analytics.py b/pandas/tests/frame/test_analytics.py index dca905b47000e..c36b5957a4283 100644 --- a/pandas/tests/frame/test_analytics.py +++ b/pandas/tests/frame/test_analytics.py @@ -448,7 +448,11 @@ def test_sum(self): has_numeric_only=True, check_dtype=False, check_less_precise=True) - def test_stat_operators_attempt_obj_array(self): + @pytest.mark.parametrize( + "method", ['sum', 'mean', 'prod', 'var', + 'std', 'skew', 'min', 'max']) + def test_stat_operators_attempt_obj_array(self, method): + # GH #676 data = { 'a': [-0.00049987540199591344, -0.0016467257772919831, 0.00067695870775883013], @@ -458,20 +462,17 @@ def test_stat_operators_attempt_obj_array(self): } df1 = DataFrame(data, index=['foo', 'bar', 'baz'], dtype='O') - methods = ['sum', 'mean', 'prod', 'var', 'std', 'skew', 'min', 'max'] - # GH #676 df2 = DataFrame({0: [np.nan, 2], 1: [np.nan, 3], 2: [np.nan, 4]}, dtype=object) for df in [df1, df2]: - for meth in methods: - assert df.values.dtype == np.object_ - result = getattr(df, meth)(1) - expected = getattr(df.astype('f8'), meth)(1) + assert df.values.dtype == np.object_ + result = getattr(df, method)(1) + expected = getattr(df.astype('f8'), method)(1) - if not tm._incompat_bottleneck_version(meth): - tm.assert_series_equal(result, expected) + if method in ['sum', 'prod']: + tm.assert_series_equal(result, expected) def test_mean(self): self._check_stat_op('mean', np.mean, check_dates=True) @@ -563,15 +564,15 @@ def test_var_std(self): arr = np.repeat(np.random.random((1, 1000)), 1000, 0) result = nanops.nanvar(arr, axis=0) assert not (result < 0).any() - if nanops._USE_BOTTLENECK: - nanops._USE_BOTTLENECK = False + + with pd.option_context('use_bottleneck', False): result = nanops.nanvar(arr, axis=0) assert not (result < 0).any() - nanops._USE_BOTTLENECK = True - def test_numeric_only_flag(self): + @pytest.mark.parametrize( + "meth", ['sem', 'var', 'std']) + def test_numeric_only_flag(self, meth): # GH #9201 - methods = ['sem', 'var', 'std'] df1 = DataFrame(np.random.randn(5, 3), columns=['foo', 'bar', 'baz']) # set one entry to a number in str format df1.loc[0, 'foo'] = '100' @@ -580,20 +581,19 @@ def test_numeric_only_flag(self): # set one entry to a non-number str df2.loc[0, 'foo'] = 'a' - for meth in methods: - result = getattr(df1, meth)(axis=1, numeric_only=True) - expected = getattr(df1[['bar', 'baz']], meth)(axis=1) - tm.assert_series_equal(expected, result) + result = getattr(df1, meth)(axis=1, numeric_only=True) + expected = getattr(df1[['bar', 'baz']], meth)(axis=1) + tm.assert_series_equal(expected, result) - result = getattr(df2, meth)(axis=1, numeric_only=True) - expected = getattr(df2[['bar', 'baz']], meth)(axis=1) - tm.assert_series_equal(expected, result) + result = getattr(df2, meth)(axis=1, numeric_only=True) + expected = getattr(df2[['bar', 'baz']], meth)(axis=1) + tm.assert_series_equal(expected, result) - # df1 has all numbers, df2 has a letter inside - pytest.raises(TypeError, lambda: getattr(df1, meth)( - axis=1, numeric_only=False)) - pytest.raises(TypeError, lambda: getattr(df2, meth)( - axis=1, numeric_only=False)) + # df1 has all numbers, df2 has a letter inside + pytest.raises(TypeError, lambda: getattr(df1, meth)( + axis=1, numeric_only=False)) + pytest.raises(TypeError, lambda: getattr(df2, meth)( + axis=1, numeric_only=False)) def test_mixed_ops(self): # GH 16116 @@ -606,11 +606,9 @@ def test_mixed_ops(self): result = getattr(df, op)() assert len(result) == 2 - if nanops._USE_BOTTLENECK: - nanops._USE_BOTTLENECK = False + with pd.option_context('use_bottleneck', False): result = getattr(df, op)() assert len(result) == 2 - nanops._USE_BOTTLENECK = True def test_cumsum(self): self.tsframe.loc[5:10, 0] = nan @@ -676,11 +674,10 @@ def test_sem(self): arr = np.repeat(np.random.random((1, 1000)), 1000, 0) result = nanops.nansem(arr, axis=0) assert not (result < 0).any() - if nanops._USE_BOTTLENECK: - nanops._USE_BOTTLENECK = False + + with pd.option_context('use_bottleneck', False): result = nanops.nansem(arr, axis=0) assert not (result < 0).any() - nanops._USE_BOTTLENECK = True def test_skew(self): tm._skip_if_no_scipy() @@ -767,7 +764,7 @@ def wrapper(x): tm.assert_series_equal(result0, frame.apply(skipna_wrapper), check_dtype=check_dtype, check_less_precise=check_less_precise) - if not tm._incompat_bottleneck_version(name): + if name in ['sum', 'prod']: exp = frame.apply(skipna_wrapper, axis=1) tm.assert_series_equal(result1, exp, check_dtype=False, check_less_precise=check_less_precise) @@ -799,7 +796,7 @@ def wrapper(x): all_na = self.frame * np.NaN r0 = getattr(all_na, name)(axis=0) r1 = getattr(all_na, name)(axis=1) - if not tm._incompat_bottleneck_version(name): + if name in ['sum', 'prod']: assert np.isnan(r0).all() assert np.isnan(r1).all() @@ -1859,14 +1856,14 @@ def test_dataframe_clip(self): assert (clipped_df.values[ub_mask] == ub).all() assert (clipped_df.values[mask] == df.values[mask]).all() - @pytest.mark.xfail(reason=("clip on mixed integer or floats " - "with integer clippers coerces to float")) def test_clip_mixed_numeric(self): - + # TODO(jreback) + # clip on mixed integer or floats + # with integer clippers coerces to float df = DataFrame({'A': [1, 2, 3], 'B': [1., np.nan, 3.]}) result = df.clip(1, 2) - expected = DataFrame({'A': [1, 2, 2], + expected = DataFrame({'A': [1, 2, 2.], 'B': [1., np.nan, 2.]}) tm.assert_frame_equal(result, expected, check_like=True) diff --git a/pandas/tests/groupby/test_aggregate.py b/pandas/tests/groupby/test_aggregate.py index efc833575843c..913d3bcc09869 100644 --- a/pandas/tests/groupby/test_aggregate.py +++ b/pandas/tests/groupby/test_aggregate.py @@ -562,7 +562,7 @@ def _testit(name): exp.name = 'C' result = op(grouped)['C'] - if not tm._incompat_bottleneck_version(name): + if name in ['sum', 'prod']: assert_series_equal(result, exp) _testit('count') diff --git a/pandas/tests/series/test_analytics.py b/pandas/tests/series/test_analytics.py index 6495d748e3823..8cc40bb5146c5 100644 --- a/pandas/tests/series/test_analytics.py +++ b/pandas/tests/series/test_analytics.py @@ -15,110 +15,103 @@ from pandas.core.index import MultiIndex from pandas.core.indexes.datetimes import Timestamp from pandas.core.indexes.timedeltas import Timedelta -import pandas.core.config as cf - import pandas.core.nanops as nanops -from pandas.compat import lrange, range, is_platform_windows +from pandas.compat import lrange, range from pandas import compat from pandas.util.testing import (assert_series_equal, assert_almost_equal, assert_frame_equal, assert_index_equal) import pandas.util.testing as tm - from .common import TestData -skip_if_bottleneck_on_windows = (is_platform_windows() and - nanops._USE_BOTTLENECK) +class TestSeriesAnalytics(TestData): + @pytest.mark.parametrize("use_bottleneck", [True, False]) + @pytest.mark.parametrize("method", ["sum", "prod"]) + def test_empty(self, method, use_bottleneck): -class TestSeriesAnalytics(TestData): + with pd.option_context("use_bottleneck", use_bottleneck): + # GH 9422 + # treat all missing as NaN + s = Series([]) + result = getattr(s, method)() + assert isna(result) - def test_sum_zero(self): - arr = np.array([]) - assert nanops.nansum(arr) == 0 + result = getattr(s, method)(skipna=True) + assert isna(result) - arr = np.empty((10, 0)) - assert (nanops.nansum(arr, axis=1) == 0).all() + s = Series([np.nan]) + result = getattr(s, method)() + assert isna(result) - # GH #844 - s = Series([], index=[]) - assert s.sum() == 0 + result = getattr(s, method)(skipna=True) + assert isna(result) - df = DataFrame(np.empty((10, 0))) - assert (df.sum(1) == 0).all() + s = Series([np.nan, 1]) + result = getattr(s, method)() + assert result == 1.0 + + s = Series([np.nan, 1]) + result = getattr(s, method)(skipna=True) + assert result == 1.0 + + # GH #844 (changed in 9422) + df = DataFrame(np.empty((10, 0))) + assert (df.sum(1).isnull()).all() + + @pytest.mark.parametrize( + "method", ['sum', 'mean', 'median', 'std', 'var']) + def test_ops_consistency_on_empty(self, method): + + # GH 7869 + # consistency on empty + + # float + result = getattr(Series(dtype=float), method)() + assert isna(result) + + # timedelta64[ns] + result = getattr(Series(dtype='m8[ns]'), method)() + assert result is pd.NaT def test_nansum_buglet(self): s = Series([1.0, np.nan], index=[0, 1]) result = np.nansum(s) assert_almost_equal(result, 1) - def test_overflow(self): - # GH 6915 - # overflowing on the smaller int dtypes - for dtype in ['int32', 'int64']: - v = np.arange(5000000, dtype=dtype) - s = Series(v) - - # no bottleneck - result = s.sum(skipna=False) - assert int(result) == v.sum(dtype='int64') - result = s.min(skipna=False) - assert int(result) == 0 - result = s.max(skipna=False) - assert int(result) == v[-1] - - for dtype in ['float32', 'float64']: - v = np.arange(5000000, dtype=dtype) - s = Series(v) - - # no bottleneck - result = s.sum(skipna=False) - assert result == v.sum(dtype=dtype) - result = s.min(skipna=False) - assert np.allclose(float(result), 0.0) - result = s.max(skipna=False) - assert np.allclose(float(result), v[-1]) - - @pytest.mark.xfail( - skip_if_bottleneck_on_windows, - reason="buggy bottleneck with sum overflow on windows") - def test_overflow_with_bottleneck(self): - # GH 6915 - # overflowing on the smaller int dtypes - for dtype in ['int32', 'int64']: - v = np.arange(5000000, dtype=dtype) - s = Series(v) - - # use bottleneck if available - result = s.sum() - assert int(result) == v.sum(dtype='int64') - result = s.min() - assert int(result) == 0 - result = s.max() - assert int(result) == v[-1] - - for dtype in ['float32', 'float64']: - v = np.arange(5000000, dtype=dtype) - s = Series(v) - - # use bottleneck if available - result = s.sum() - assert result == v.sum(dtype=dtype) - result = s.min() - assert np.allclose(float(result), 0.0) - result = s.max() - assert np.allclose(float(result), v[-1]) - - @pytest.mark.xfail( - skip_if_bottleneck_on_windows, - reason="buggy bottleneck with sum overflow on windows") + @pytest.mark.parametrize("use_bottleneck", [True, False]) + def test_sum_overflow(self, use_bottleneck): + + with pd.option_context('use_bottleneck', use_bottleneck): + # GH 6915 + # overflowing on the smaller int dtypes + for dtype in ['int32', 'int64']: + v = np.arange(5000000, dtype=dtype) + s = Series(v) + + result = s.sum(skipna=False) + assert int(result) == v.sum(dtype='int64') + result = s.min(skipna=False) + assert int(result) == 0 + result = s.max(skipna=False) + assert int(result) == v[-1] + + for dtype in ['float32', 'float64']: + v = np.arange(5000000, dtype=dtype) + s = Series(v) + + result = s.sum(skipna=False) + assert result == v.sum(dtype=dtype) + result = s.min(skipna=False) + assert np.allclose(float(result), 0.0) + result = s.max(skipna=False) + assert np.allclose(float(result), v[-1]) + def test_sum(self): self._check_stat_op('sum', np.sum, check_allna=True) def test_sum_inf(self): - import pandas.core.nanops as nanops - s = Series(np.random.randn(10)) s2 = s.copy() @@ -130,7 +123,7 @@ def test_sum_inf(self): arr = np.random.randn(100, 100).astype('f4') arr[:, 2] = np.inf - with cf.option_context("mode.use_inf_as_na", True): + with pd.option_context("mode.use_inf_as_na", True): assert_almost_equal(s.sum(), s2.sum()) res = nanops.nansum(arr, axis=1) @@ -510,9 +503,8 @@ def test_npdiff(self): def _check_stat_op(self, name, alternate, check_objects=False, check_allna=False): - import pandas.core.nanops as nanops - def testit(): + with pd.option_context('use_bottleneck', False): f = getattr(Series, name) # add some NaNs @@ -535,15 +527,7 @@ def testit(): allna = self.series * nan if check_allna: - # xref 9422 - # bottleneck >= 1.0 give 0.0 for an allna Series sum - try: - assert nanops._USE_BOTTLENECK - import bottleneck as bn # noqa - assert bn.__version__ >= LooseVersion('1.0') - assert f(allna) == 0.0 - except: - assert np.isnan(f(allna)) + assert np.isnan(f(allna)) # dtype=object with None, it works! s = Series([1, 2, 3, None, 5]) @@ -574,16 +558,6 @@ def testit(): tm.assert_raises_regex(NotImplementedError, name, f, self.series, numeric_only=True) - testit() - - try: - import bottleneck as bn # noqa - nanops._USE_BOTTLENECK = False - testit() - nanops._USE_BOTTLENECK = True - except ImportError: - pass - def _check_accum_op(self, name, check_dtype=True): func = getattr(np, name) tm.assert_numpy_array_equal(func(self.ts).values, @@ -733,31 +707,6 @@ def test_modulo(self): expected = Series([nan, 0.0]) assert_series_equal(result, expected) - def test_ops_consistency_on_empty(self): - - # GH 7869 - # consistency on empty - - # float - result = Series(dtype=float).sum() - assert result == 0 - - result = Series(dtype=float).mean() - assert isna(result) - - result = Series(dtype=float).median() - assert isna(result) - - # timedelta64[ns] - result = Series(dtype='m8[ns]').sum() - assert result == Timedelta(0) - - result = Series(dtype='m8[ns]').mean() - assert result is pd.NaT - - result = Series(dtype='m8[ns]').median() - assert result is pd.NaT - def test_corr(self): tm._skip_if_no_scipy() diff --git a/pandas/tests/test_panel.py b/pandas/tests/test_panel.py index c8e056f156218..2769ec0d2dbed 100644 --- a/pandas/tests/test_panel.py +++ b/pandas/tests/test_panel.py @@ -172,7 +172,7 @@ def wrapper(x): for i in range(obj.ndim): result = f(axis=i) - if not tm._incompat_bottleneck_version(name): + if name in ['sum', 'prod']: assert_frame_equal(result, obj.apply(skipna_wrapper, axis=i)) pytest.raises(Exception, f, axis=obj.ndim) diff --git a/pandas/tests/test_panel4d.py b/pandas/tests/test_panel4d.py index 863671feb4ed8..49859fd27d7bc 100644 --- a/pandas/tests/test_panel4d.py +++ b/pandas/tests/test_panel4d.py @@ -138,7 +138,7 @@ def wrapper(x): with catch_warnings(record=True): for i in range(obj.ndim): result = f(axis=i) - if not tm._incompat_bottleneck_version(name): + if name in ['sum', 'prod']: expected = obj.apply(skipna_wrapper, axis=i) tm.assert_panel_equal(result, expected) diff --git a/pandas/tests/test_window.py b/pandas/tests/test_window.py index 0fe51121abef6..432350b4849d8 100644 --- a/pandas/tests/test_window.py +++ b/pandas/tests/test_window.py @@ -2355,7 +2355,8 @@ def test_expanding_consistency(self, min_periods): expanding_apply_f_result = x.expanding( min_periods=min_periods).apply(func=f) - if not tm._incompat_bottleneck_version(name): + # GH 9422 + if name in ['sum', 'prod']: assert_equal(expanding_f_result, expanding_apply_f_result) @@ -2453,7 +2454,9 @@ def test_rolling_consistency(self, window, min_periods, center): rolling_apply_f_result = x.rolling( window=window, min_periods=min_periods, center=center).apply(func=f) - if not tm._incompat_bottleneck_version(name): + + # GH 9422 + if name in ['sum', 'prod']: assert_equal(rolling_f_result, rolling_apply_f_result) diff --git a/pandas/util/testing.py b/pandas/util/testing.py index 202c9473eea12..3c23462e10d35 100644 --- a/pandas/util/testing.py +++ b/pandas/util/testing.py @@ -401,21 +401,6 @@ def _skip_if_no_localpath(): pytest.skip("py.path not installed") -def _incompat_bottleneck_version(method): - """ skip if we have bottleneck installed - and its >= 1.0 - as we don't match the nansum/nanprod behavior for all-nan - ops, see GH9422 - """ - if method not in ['sum', 'prod']: - return False - try: - import bottleneck as bn - return bn.__version__ >= LooseVersion('1.0') - except ImportError: - return False - - def skip_if_no_ne(engine='numexpr'): from pandas.core.computation.expressions import ( _USE_NUMEXPR,