Repository: pandas-dev/pandas

Issue:
API: sum of Series of all NaN should return 0 or NaN ?
### Summary

The question is what the sum of a Series of all NaNs should return (which is equivalent to an empty Series after skipping the NaNs): NaN or 0?

```
In [1]: s = Series([np.nan])                 

In [2]: s.sum(skipna=True)  # skipping NaNs is the default
Out[2]: nan or 0     <---- DISCUSSION POINT

In [3]: s.sum(skipna=False)
Out[3]: nan
```

The reason this is a discussion point has the following cause: the internal nansum implementation of pandas returns NaN. But, when bottleneck is installed, pandas will use bottlenecks implementation of nansum, which returns 0 (for the versions >= 1.0). 
Bottleneck changed the behaviour from returning NaN to returning 0 to model it after numpy's nansum function.

This has the very annoying consequence that depending on whether bottleneck is installed or not (which is only an optional dependency), you get a different behaviour.

So the decision we need to make, is to either:

- adapt pandas internal implementation to return 0, so in all cases 0 is returned for all NaN/empty series.
- workaround bottlenecks behaviour or not use it for nansum, in order to consistently return NaN instead of 0
- choose one of both above as the default, but have an option to switch behaviour




---

Original title: **nansum in bottleneck 1.0 will return 0 for all NaN arrays instead of NaN**

xref https://github.com/kwgoodman/bottleneck/issues/96
xref https://github.com/pydata/pandas/issues/9421
- [ ] Tests are turned off for bottleneck >1.0 (xref https://github.com/pydata/pandas/pull/10986)

This matches a change from numpy 1.8 -> 1.9.

We should address this for pandas 0.16.

Should we work around the new behavior (probably the simplest choice) or change nansum in pandas?



Discussion:
I guess the `np.nan*` methods 'ignore' nans by default  and thus all-nans is really an empty array. But this seems wrong to me. You can _never_ get a `NaN` result back. 

However pandas wants to propogate `NaN` results, so this would break existing behavior (which looking at it seems slightly wrong).

```
In [13]: s = Series([np.nan])                 

In [15]: s.sum(skipna=True)
Out[15]: nan

In [16]: s.sum(skipna=False)
Out[16]: nan
```

so I think that you are proposing that `[15]` should be 0.0 (this is the default, FYI).

Yes, we might want to change your line `[15]` to 0.0. There is certainly something to be said for staying in sync with numpy/bottleneck.

I think `sum` and `prod` are the only functions that would need to change to match the numpy behavior (`nanprod` will be new in numpy 1.10: https://github.com/numpy/numpy/pull/5418). All the other NA-skipping aggregation functions (mean, std, var, median, etc) are undefined for empty arrays, so it still makes sense for them to return NaN in the all NaN case.

This change also has implications for windowed and grouped sum/prod. These might also need a corresponding change to match.

Yeh an option passed to `core/nanops/_maybe_null_out` in `nansum/nanprod` would do the trick here.

In any case, the current docstring is no longer fully correct in all cases:

```
skipna : boolean, default True
    Exclude NA/null values. If an entire row/column is NA, the result
    will be NA
```

So we should mention something there that the result of an entire NaN row/column depends on the installed version of bottleneck 

TBH I don't see the logic of why summing `NaN` should return anything but `NaN`, (maybe sum starts from zero and adds numbers on, and so an 'empty' array sums to zero?), but even if there's a reasonable set of cases where that's true, it's _much_ easier to get that behavior if the result is `NaN` (i.e. `fillna(0)` on the result), rather than the current bottleneck behavior.

If we've decided on a policy, do you have a view of the easiest way to make the change?

Also @shoyer :

> This matches a change from numpy 1.8 -> 1.9.

I couldn't find this:

``` python
In [2]: import numpy as np

In [3]: nan_array=np.asarray([np.nan]*5)

In [4]: nan_array.sum()
Out[4]: nan
```

Even though an empty array does return 0.0. (Am I building my `nan_array` incorrectly?)

``` python
In [15]: np.asarray([]).sum()
Out[15]: 0.0
```

```

In [9]: pd.show_versions()

INSTALLED VERSIONS
------------------
commit: None
python: 3.5.0.final.0
python-bits: 64
OS: Darwin
OS-release: 15.0.0
machine: x86_64
processor: i386
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8

pandas: 0.17.1
nose: 1.3.7
pip: 7.1.2
setuptools: 18.2
Cython: None
numpy: 1.10.1
scipy: None
statsmodels: None
IPython: 4.0.1
sphinx: None
patsy: None
dateutil: 2.4.2
pytz: 2015.7
blosc: None
bottleneck: 1.0.0
tables: None
numexpr: None
matplotlib: None
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
httplib2: None
apiclient: None
sqlalchemy: None
pymysql: None
psycopg2: None
Jinja2: None
```

see the numpy issue referenced at the top for the rationale (from. numpy side)

I think pandas should be different here from as I agree it makes a lot more sense (to propagate nan)
we have lots of options to skip and fillna

but others disagree

use nansum in your example with numpy (sum is unchanged)

Some numpy discussion is here: https://github.com/numpy/numpy/issues/1721

Should we comparing pandas `sum` to numpy's `sum` or `nansum`? A quote from that issue:

> This is the "nansum" function we are talking about, not the normal "sum" function, which of course returns nan if there is any nan value in the array.
> nansum is designed to treat nan like 0, which is sometimes exactly what you need. This is not your case, but nobody forces you to use that function. We are not even discussing the general behavior of nansum, but a few corner cases of it.

see the point is pandas `.sum(skipna=True)` is the default, so we basically use `nansum` like behavior for everthing. The issues is if you ONLY have `nans` then numpy says it should be 0, because `nansum` 'skips' nans.

But in pandas that is completely misleading and lossy, because nans by definition propogate (unless you specifically dont' propogate them). numpy simply does not handle nans well and I think that is the fundamental issue. 

the classic case is this:

```
In [2]: df = DataFrame({'A' : [np.nan, np.nan], 'B' : [np.nan, 1]})

In [3]: df.sum()
Out[3]: 
A    0
B    1
dtype: float64

In [4]: pandas.core.nanops._USE_BOTTLENECK=False

In [5]: df.sum()
Out[5]: 
A   NaN
B     1
dtype: float64
```

Since we are deferring computation to `bottleneck` (eg. I have 1.0 installed). [3] is completely wrong IMHO. and [5] is much much more useful and non-lossy. A user can always `.fillna` on [5], but info is lost on [3].

I couldn't agree more

What's the easiest way to make the change?

> I think pandas should be different here from as I agree it makes a lot more sense (to propagate nan)

+1 (and reiterating my [comment](https://github.com/pydata/pandas/pull/10815#issuecomment-137324254) in #10815).

I looked into how you could add something like an `all_nan='nan'` kwarg to pandas `.sum` but it seemed complicated to do it efficiently/reliably/elegantly. So, I took the easy way out of modifying my code in a few key spots using something like...

``` python
allnans = data.notnull().sum(axis=axis) == 0
thesum = data.sum(axis=axis)
thesum[allnans] = np.nan
```

I think the easiest thing to do is simply to to modify:https://github.com/pydata/pandas/blob/master/pandas/core/nanops.py#L122

right now we defer to bottleneck (if its installed) for `nansum` (they are all named this way), only for `float64/int64`, smaller numerics don't go. You can make all `nansum` (and actually all `nanprod`) go thru the alt pandas routines.

And you'd have to back out some of the test fixing, search for `_incompat_bottleneck_version` (as bottleneck < 1.0 has the behavior like this, its only >= 1.0 that changes to match numpy).

And of course a loud whatsnew note.

I suppose we _could_ make this an `pd.options.numeric.all_nan_is_zero=False` (and switch based on this option). Though not really a fan of that.

OK great, let me see if I can do this efficiently

@shoyer we'll do this in XRay too? 

> @shoyer we'll do this in XRay too?

Yep, I suppose so. Though it's a real pain to get an alternate version of everything working rather than just using numpy/bottleneck/dask...

As another data point, R also returns 0 for an all NA vector:

```
> sum(c(NA, NA, NA))
[1] NA
> sum(c(NA, NA, NA), na.rm = T)
[1] 0
```

It is stated in their docs of `sum`: _NB: the sum of an empty set is zero, by definition._

@shoyer I know. 
Is there another way? Is the `nan`-x definition just unclear? What do you do in numbagg?

I just copied bottleneck in numbagg. But `nansum` is also [only a few lines](https://github.com/shoyer/numbagg/blob/0e5bf2c65fb3965ab373ab0d26d354ce65aad48e/numbagg/funcs.py#L36-L41) in numbagg, so it would be pretty easy to write both versions.

I find returning `0` quite sensible, because otherwise `0` would fail to be an additive identity and `1` a multiplicative identity.  

`NaN` will have a clearer meaning (as a silent error) in these functions -- if the function has enough arguments to compute something, and if the arguments can give valid results, then it will spit out a number.  What the product of an empty array should be may be a bit more debatable, but by symmetry `1` seems suitable.

`NaN` would not give you certainty that the whole column is `NaN`, so I don't think any information will be lost.

I've been running into this issue a lot recently - here are my two (or twenty) cents:

In a system that can represent the concept of "no data" (NaN), what is an operation on "no data"? I would assert: "no data". **If I give an algorithm nothing to work with, it gives me nothing back.**

When I can represent the concept of "no data", that concept is VERY different from "data: no magnitude" (i.e. zero). In such a context, confusing the two (NaN vs 0) is troublesome and problematic.

If I cannot represent the concept of "no data", then I have to default to whatever makes algorithmic sense. (e.g. vanilla Python)

The algorithmic treatment of "no data" that makes the most sense to me is that you only operate on the data you are given. 

So:

nansum([NaN, 1]) should be 1
nansum([Nan, 0]) should be 0
**nansum([]) should be NaN** (I sum no numbers, so my result is not a number)
nansum([NaN, NaN]) should be NaN

I believe that the scientific/statistic/numeric community got off on the wrong foot when we ported the logic and interpretation of the non-scientific Python method sum() to the scientific necessity nansum().

Pandas, this is your chance to get us back on track!

And, for a practical argument, I have large arrays full of real data, with an occasional NaN in the mix. However, every once in a while, something catastrophic goes wrong upstream, and I get a vector of NaNs. 

Ideally, I just want to sum/mean/etc. on the data, ignoring the occasional missing (NaN) value. But if all the data are NaN, I want that "complete missing-ness" to propagate so the vector isn't mistaken for all zeros (a very different input).

I could interweave nan-checks throughout my code, but it has the same code smell as null-checking. You can do it, but it isn't as elegant/concise/theoretically-pure.

In summary:
**If I have no information to start with, I should return no information.** 

Is there any update on this issue ? It's a little disconcerting to have the specified behaviour of Pandas sum() change when a dependency is added.
@maxencedb well I agree, bottleneck & numpy changed their behavior a while back and pandas didn't, for good reason. I stated above that we should simply continue to use the pandas behavior (e.g.. sum ``NaN`` is nan), as this doesn't lose information, while numpy/bottleneck does.

e.g. numpy 1.11.3
```
In [2]: np.nansum([np.nan])
Out[2]: 0.0

In [3]: np.nanmean([np.nan])
/Users/jreback/miniconda3/envs/pandas/lib/python3.5/site-packages/numpy/lib/nanfunctions.py:703: RuntimeWarning: Mean of empty slice
  warnings.warn("Mean of empty slice", RuntimeWarning)
Out[3]: nan
```

master
```
In [5]: Series([np.nan]).sum()
Out[5]: 0.0

In [6]: Series([np.nan]).mean()
Out[6]: nan

In [7]: pandas.core.nanops._USE_BOTTLENECK = False

In [8]: Series([np.nan]).sum()
Out[8]: nan

In [9]: Series([np.nan]).mean()
Out[9]: nan
```

So this is unfortunate, and I have actually run into this in the real-world.

So clearly this shouldn't depend on the dependencies. I also suspect that its going to be less disruptive to continue with the (correct) way of doing things by using the pandas method.


so we need to do something about this, keeps coming up (xref #15542), and separately current bottleneck (1.2.0) uses in32 as an accumulator (rather than int64) so it can overflow (#15453).

So here are my thoughts.

examples

```
In [1]: df = pd.DataFrame({'A' : [np.nan, np.nan], 'B':[np.nan, 0]})

In [2]: df
Out[2]: 
    A    B
0 NaN  NaN
1 NaN  0.0

In [3]: s = Series([np.nan])

In [4]: s
Out[4]: 
0   NaN
dtype: float64

```

- when I originally added bottleneck to the mix (a long time ago), pandas, bottleneck, and numpy all did the same thing (numpy was < 1.8, see docs [here](https://docs.scipy.org/doc/numpy/reference/generated/numpy.nansum.html)
- bottleneck >= 1.0 took on the numpy model
- so pandas now has 2 behaviors, depending on if bottleneck is installed or not (we don't directly use numpy at all, rather ``core/nanops/nansum.py``).

So my 2c is that this is very problematic and leads to hard to find bugs. So I w/o reservation would keep the pandas behavior (impl is easy, we simply disable the dispatch to ``bottleneck`` for ``nansum``). And here's why

- its consistent with ANY version of pandas when bottleneck is not installed
- it does not depend (going forward) on whether bottleneck is installed
- its consistent with the current documentation
- the current community who uses pandas very likely has a much smaller installed base of (pandas + bottleneck), as compared to just pandas alone.
- most importantly, you *dont' lose information*!  e.g.

```
In [5]: from pandas.core import nanops

In [6]: nanops._USE_BOTTLENECK
Out[6]: True

In [7]: s.sum()
Out[7]: 0.0

In [8]: df.sum()
Out[8]: 
A    0.0
B    0.0
dtype: float64

In [9]: nanops._USE_BOTTLENECK = False

In [11]: s.sum()
Out[11]: nan

In [10]: df.sum()
Out[10]: 
A    NaN
B    0.0
dtype: float64
```

It is *obvious* that [8] is completely WRONG, while [10] is the RIGHT answer here. If a user wants, they can simply fill [10], but if you get [8] then you are SOL.

So IMHO w/o hesistation, we should simply disable bottleneck for ``nansum``. This also has the nice propery of affecting a fairly small percentage of users (e.g. (pandas + bottleneck) / just pandas. While there reverse change (e.g. go to numpy/bottleneck) would be a disaster and we would be forced to reverse it causing even more confusion.

From a personal note, I have actually debugged and found this exact situation in multiple engagements. This is not a small issue at all and we should fix this for 0.20.0.
cc @shoyer @jorisvandenbossche @wesm @TomAugspurger @sinhrks @chris-b1 @cpcloud 
We need to take a decision on this, as this has lingered long enough (as you can see on the amount of linked issues above :-))

For those not up to speed with what this is about, I tried to make a summary at the top https://github.com/pandas-dev/pandas/issues/9422#issue-56707245.

There have been several who have argued for returning NaN, others for returning 0 (and I didn't yet state that explicitly above, but I would also put myself in the 0-camp). 

How do we go about this decision? 
Somebody who wants to try to summarize pro/cons or reasoning for both options?

cc @wesm 
@jreback ok, our posts just crossed each other :-)
@jorisvandenbossche yep :->
I believe the answer is `NaN` because there are no valid observations. Any reduction over an indeterminate  number of values should be NaN. 

If you truly want 0 when NaN -- in a groupby setting you can fillna with 0 after the groupby, or in a scalar setting you can do

```
x = s.sum()
if pd.isnull(x):
    x = 0
```

The 0 solution drops information (i.e. you have to ask "what kind of 0 is that?"), the NaN solution does not -- with `skipna=True`, the only way to get NaN is to have no valid values
I don't think there is anything "completely wrong" about the behaviour of returning 0. I certainly understand the reasoning of wanting NaN as a result, but there is just as much a reasoning that explains 0 as a result.

So some arguments for returning 0:

- This is consistent with the idea of "skipping NaNs" that pandas does by default -> if you skip all NaNs, you actually have an empty Series, and the sum of an empty Series is 0:

    ```
    In [1]: pd.Series([]).sum()
    Out[1]: 0
    ``` 

  And note that this behaviour of empty series is always 0, not depending on whether bottleneck is installed or not.

- This is consistent with numpy and bottleneck (disregarding older versions). In numpy, both the sum of a empty array (``np.sum([])``) as nansum of all-nan array (`np.nansum([np.nan])`) return 0.  
  I know they have a different 'domain' (scientific array computing vs more statistical dataframe computing) and therefore end up with different choices in some occasions (there are other examples in pandas of making a different choice as numpy), but still, consistency with the rest of the python ecosystem is nice.

- This would be consistent with R and matlab, both returning 0 (and probably some other languages as well)

- Regarding which choice will impact most users: bottleneck is installed by default in Anaconda, so all people using a plain Anaconda, will already have the 0 behaviour. Not saying that this is the majority of users, but I am also not sure it is the 'much smaller user base' / 'small percentage of users' 
  (but OK, the other way around would of course also affect a lot of people. I just think this is not a strong argument)

Regarding  the argument of "it does not depend (going forward) on whether bottleneck is installed". This can also be the case of returning 0, if want (we can also disable dispatch to bottleneck for nansum when we changed our nansum).
anaconda just recently added ``bottleneck`` by default FYI (I think 4.1, release july 2016).
My opinion is also that entirely missing aggregations should return NA.  I'm sympathetic to the "0 is the sum identity" argument, but generally seems more useful in the context of a system that has a first(ish) class missing value to use it.

FWIW, this is how `sqlite` works too.

```pythoon
import sqlite3 as db

con = db.connect(':memory:')

con.execute("""
CREATE TABLE test_table (a NUMBER, b NUMBER)""")
con.execute("""
INSERT INTO test_table VALUES (NULL, NULL)""")
con.execute("""
INSERT INTO test_table VALUES (1.0, NULL)""")


con.execute("SELECT SUM(a), SUM(b) from test_table").fetchone()
Out[58]: (1, None)
```


> anaconda just recently added bottleneck by default FYI (I think 4.1), so within last 6mo-1 year. (4.3 just released)

according to the changelog, bottleneck was added to the installer in Anaconda 2.3.0
Let's set aside concerns about consistency with whether or not bottleneck is installed. I think we all agree that should have consistent behavior in 0.20, regardless of which optional dependencies are used, and it would be relatively easy to fix things either way.

Let me try to summarize the arguments:

Arguments in favor of returning NaN:
- It is consistent with the current documentation and many (most?) pandas installations.
- It is consistent with the behavior of `SUM` in SQL (at least sqlite).
- It preserves more information about the data. You can always replace NaN with 0 later if desired.
- It works better for error checking, surfacing cases where there are all NaNs.

Arguments in favor of returning 0 (this has my vote):
- It would be consistent with current behavior if bottleneck is installed.
- It's consistent with NumPy, R and matlab.
- It has a simpler explanation "the sum skipping NaN elements" that is consistent with the argument name `skipna`, rather than "the sum skipping NaN elements, unless there are only NaNs".

In the long term, one possible way to handle this would be to replace and/or augment `skipna` with a `min_count` argument, like the `min_count` that bottleneck defines for [moving window aggregations](http://berkeleyanalytics.com/bottleneck/reference.html#moving-window-functions). For sum, `min_count=0` would correspond to returning 0, whereas `min_count=1` would correspond to returning NaN (and could be used for error checking in such cases). I would still hesitate to set `min_count=1` as the default, though.
I slightly prefer returning 0 because

> It has a simpler explanation "the sum skipping NaN elements" that is consistent with the argument name skipna, rather than "the sum skipping NaN elements, unless there are only NaNs".

I'm not a contributor, but this issue has been a nuisance to me for 8+ years, so I wanted to weigh in one last time. In ALL of my use cases (scientific research), the behavior I have needed is to get NaN from a sum of NaNs. It bothered me that MATLAB returned 0, and it bothers me that numpy returns 0. I have always had to maintain two sums - the sum and the count of non-NaN values. 

From a mathematics-theory perspective, returning 0 as the sum of "no numbers" is wrong. The correct behavior to "sum no numbers" is to return "not a number". From this perspective, everybody has been doing this wrong (except pandas... :).

However, from a computer-science-theory/constraints perspective, return NaN as the sum of an empty list is sometimes wrong: if the type of your list is int, you should return an int, not a float. And as NaN is only defined for floating point numbers, you can only have consistent behavior by returning 0. So, the sum of an empty list must be 0, and the precedent has been to return 0. \*

*However*, summing an empty list is different from summing a list full of NaN (although one implementation would be to dropna, then sum). By containing NaN, you are guaranteeing that the list type is floating point and can return NaN. Also, by containing Nan, the list is not empty, and this is real information that can (and should!) be reflected in the return value.

**So**, I would argue that, as convenient as it is to think of a list of NaN as an empty list under some operations, it should never be treated as "an empty list" in those cases, because the list is not empty. And because the list is not empty, the correct return value for nansum([NaN, etc. ]) is NaN. 

Good luck making a decision and keep up the good work!

\* It would be ideal to have every numeric type define NaN, but that just isn't feasible for small types (int8, int16, etc.) 
thanks for the commentary @brazilbean 

the dtype is actually immaterial, in that, currently we cannot hold missing values for integer Series, so by-definition that cannot contain ``NaN`` (though in pandas2 this will not be the case and missing values will be fully supported across dtypes).

Furthermore, we also do this (which is independent of bottleneck; this is handled internally as i8, with a mask for the missing values).

```
In [1]: Series([pd.NaT],dtype='m8[ns]')
Out[1]: 
0   NaT
dtype: timedelta64[ns]

In [2]: Series([pd.NaT],dtype='m8[ns]').sum()
Out[2]: NaT
```
I like using `min_count` as a tool to address the different use cases. Effectively we have:

- sum: min count of 1 to get a valid value
- mean: min count of 1 needed
- std/var: min count of 2 needed

but you can do

`s.sum(skipna=True, min_count=0)` to get 0, even for all-NA. This defrays the need to write:

```
x = s.sum()
if s.count() == 0: 
    x = 0
```

Neither default behavior is going to make everyone happy, so my recommendation is to choose the default that we expect to cause the *least* amount of incorrect data analysis. All data analysis tools unfortunately will lead users to incorrect conclusions / analyses (see all the high profile screwups caused by Excel mistakes), so the best we: can do is 

a) choose the behavior that does the least harm (in aggregate) to the user base
b) does not close the door to a more precise answer for those who want a particular behavior (which I think the `min_count=0` argument covers)

EDITED: typos
Potential docstring:

```python
"""
Return the sum of the values for the requested axis

Parameters
----------
axis : {index (0), columns (1)}
skipna : boolean, default True
    Exclude NA/null values.
level : int or level name, default None
    If the axis is a MultiIndex (hierarchical), count along a
    particular level, collapsing into a Series
numeric_only : boolean, default None
    Include only float, int, boolean columns. If None, will attempt to use
    everything, then use only numeric data. Not implemented for Series.
min_count : int, default 1
    If fewer than `min_count` values are valid, return `NaN` instead.
    By default, an array with all missing values will sum to `NaN`

Returns
-------
sum : Series or DataFrame (if level specified)

Examples
----------

>>> pd.Series([1, 2, 3]).sum()
... 6
>>> pd.Series([np.nan, np.nan]).sum()
... NaN
>>> pd.Series([np.nan, np.nan]).sum(min_count=0)
... 0
"""
```

I would support `min_count=1` as the default.
how would ``skipna`` interact with ``min_count``?

- could have ``skip=True`` -> ``min_count=x``
  where x is function dependent, 1 for mean, 2 for std/var, 0 for sum (or 1 depending on discussion here)
 ``min_count=None`` would have to be the signature.

- or could deprecate it

- ``skipna=False`` -> ``min_count=len``


> could have skip=True -> min_count=x
where x is function dependent, 1 for mean, 2 for std/var, 0 for sum (or 1 depending on discussion here)

We don't need to make the default value of `min_count` function dependent -- `mean` and `std`/`var` already return NaN (not NA) if there aren't enough values, because they are dividing by zero.

As much as I like `min_count`, I don't see a good way to handle it at the same time as `skipna`. Suppose we have a series of length `L`. What do the following do?

- `.sum(skipna=False)`: should to be equivalent to `.sum(min_count=L)`.
- `.sum(skipna=False, min_count=X)` for any `X<L`: I think this has to be an error. Otherwise it's not clear which argument takes precedence. This implies we can't use a the default argument value of `min_count=0` or `min_count=1`, but rather need `min_count` to default to some magic value that means "0/1 if skipna=True, undefined otherwise" (depending on which choice we make here).
- `.sum(skipna=False, min_count=L)`: in principle, this could be valid, but in practice it should probably also error for consistency with the above.
- `.sum(skipna=False, min_count=None)`: if `None` denotes the default value, this should be equivalent to `.sum(skipna=False)`. This would break with the behavior of bottleneck (which uses `min_count=None` to imply `min_count=L`), but maybe that's OK.

So I think the rule has to be you cannot set `min_count` and `skipna=False` at the same time. Effectively, these arguments are mutually exclusive. This isn't great API design but maybe it's the most practical choice. It would be a severe hit to readability to require every use of `.sum(skipna=False)` in pandas to switch to `.sum(min_count=None)` like bottleneck.

The function signature now looks like `sum(skipna=True, min_count=None)` or `sum(skipna=True, min_count=_SENTINEL)`, where the sentinel value denotes a default min_count of whatever value we decide here.
This probably calls for the options of different strategies. I'd like to be able to specify whether I wanted NaN or 0 to come back from the sum operation. I don't think I can really say what the best default is -- possibly even just raise an exception if the user doesn't specify an aggregation strategy (e.g. skip NaNs vs return NaN). Basically, sometimes NaNs are used very carefully to indicate problems or something you want to react to, and sometimes you probably just want to "skim over them" and just deal with the data you've got. 
Is it relevant to mention the impact of the `groupby` function here when extending this to a grouped `pd.DataFrame` object?
Currently `groupby` seems to be autoskipping `NaN` as far as I can see

```
import pandas as pd
import numpy as np
d = {'l':  ['left', 'right', 'left', 'right', 'left', 'right'],
     'r': ['right', 'left', 'right', 'left', 'right', 'left'],
     'v': [-1, 1, -1, 1, -1, np.nan]}
df = pd.DataFrame(d)
pd_sum = df.groupby('l')['v'].sum()['right']
np_sum = df.groupby('l')['v'].apply(np.sum)['right']
print np.isnan(np_sum)
print np_sum == pd_sum 
```

will return 
```
False
True
```

given  that `np.isnan(np.sum([np.NaN]))` returns `True` this indicates that the expected output from the above code should actually the exact opposite ... i.e.

```
True
False
```

`pandas` should reflect it's underlying dependence on `numpy` which has the default result of `NaN` for these sums...

A prime example is where a reader wants to understand a `pd.DataFrame` and know when a value has come through as `NaN` e.g. in a case where two `pd.DataFrames` have been merged and one has a missing entry that could skew a result - this is a common occurrence in practice
@flipdazed you raise an issue not directly related to this one. please open a new issue.
Revisiting this. We need to do something for 0.21.0

Current behavior
```
In [1]: pd.set_option('use_bottleneck',True)

In [2]: pd.Series([np.nan]).sum(skipna=True)
Out[2]: 0.0

In [3]: pd.Series([np.nan]).sum(skipna=False)
Out[3]: nan

In [4]: pd.set_option('use_bottleneck',False)

In [5]: pd.Series([np.nan]).sum(skipna=True)
Out[5]: nan

In [6]: pd.Series([np.nan]).sum(skipna=False)
Out[6]: nan
```

Heres a possibility to make this simpler and not having to add [another kw](https://github.com/pandas-dev/pandas/issues/9422#issuecomment-283702370)

```
In [1]: pd.Series([np.nan]).sum(skipna=True)
Out[1]: nan

In [2]: pd.Series([np.nan]).sum(skipna=False)
Out[2]: nan

In [3]: pd.Series([np.nan]).sum(skipna='exclude')
Out[3]: 0.0
```

This doesn't make any different in the NOT-all-NaN case.
```
In [4]: pd.Series([np.nan, 1.0]).sum(skipna=True)
Out[4]: 1.0

In [5]: pd.Series([np.nan, 1.0]).sum(skipna=False)
Out[5]: nan

In [6]: pd.Series([np.nan, 1.0]).sum(skipna='exclude')
Out[6]: 1.0
```

So we add ``exclude`` as an accepted value to ``skipna`` (only for ``.sum()/.prod()`` I think, or maybe just allow it but ignore for others.

We then turn off bottleneck completely for ``sum/prod``. The default is the same as in pandas now (IOW sum on all-nans is still ``NaN``). This allows one to customize the behavior if desired. 
Personally, I don't find skipna='exclude' very clear. I suppose we can come up with a better value if we want to go this route, but we also had the idea of adding the min_count arg. After rereading the discussion here, I actually quite like that solution. 
One problem that was raised is how it would interact with the skipna keyword, but I think we can simply say that min_count is ignored when skipna is set to False.
> One problem that was raised is how it woyld interact with the skipna keywird, but I think we can simply say that min_count is ignored when skipna is set to False.

am against this for additional confusion. ok with adding ``min_count``, but then would deprecate/remove skipna, BUT this is pretty pervasive and that would be even more invasive.

I think we need to do *something*; simplest / easiest is to make the behavior consistent with the rest of pandas and de-couple the bottleneck dep. making sum just return nan for 0 count is the way to do. folks can always fill. The reverse is not true.

Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- 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
--- 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


Test patch:
diff --git a/pandas/tests/frame/test_analytics.py b/pandas/tests/frame/test_analytics.py
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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,