Ответ 1
Укороченная версия:
Причина этого в том, что pandas
использует bottleneck
(если оно установлено) при вызове mean
операции, а не просто полагается на numpy
. bottleneck
используется, так как кажется, что оно быстрее, чем numpy
(по крайней мере, на моей машине), но за счет точности. Они совпадают для 64-битной версии, но отличаются по 32-битной земле (что является интересной частью).
Длинная версия:
Чрезвычайно сложно сказать, что происходит, просто изучив исходный код этих модулей (они довольно сложны, даже для простых вычислений, таких как mean
, оказывается, что численные вычисления трудны). Лучше всего использовать отладчик, чтобы избежать компиляции мозгов и подобных ошибок. Отладчик не ошибется в логике, он точно скажет вам, что происходит.
Вот некоторые из моих следов стека (значения немного отличаются, так как нет места для RNG):
Можно воспроизвести (Windows):
>>> import numpy as np; import pandas as pd
>>> x=np.random.normal(-9.,.005,size=900000)
>>> df=pd.DataFrame(x,dtype='float32',columns=['x'])
>>> df['x'].mean()
-9.0
>>> x.mean()
-9.0000037501099754
>>> x.astype(np.float32).mean()
-9.0000029
Ничего экстраординарного не происходит с numpy
версией Это версия pandas
что немного странно.
Давайте заглянем внутрь df['x'].mean()
:
>>> def test_it_2():
... import pdb; pdb.set_trace()
... df['x'].mean()
>>> test_it_2()
... # Some stepping/poking around that isn't important
(Pdb) l
2307
2308 if we have an ndarray as a value, then simply perform the operation,
2309 otherwise delegate to the object
2310
2311 """
2312 -> delegate = self._values
2313 if isinstance(delegate, np.ndarray):
2314 # Validate that 'axis' is consistent with Series single axis.
2315 self._get_axis_number(axis)
2316 if numeric_only:
2317 raise NotImplementedError('Series.{0} does not implement '
(Pdb) delegate.dtype
dtype('float32')
(Pdb) l
2315 self._get_axis_number(axis)
2316 if numeric_only:
2317 raise NotImplementedError('Series.{0} does not implement '
2318 'numeric_only.'.format(name))
2319 with np.errstate(all='ignore'):
2320 -> return op(delegate, skipna=skipna, **kwds)
2321
2322 return delegate._reduce(op=op, name=name, axis=axis, skipna=skipna,
2323 numeric_only=numeric_only,
2324 filter_type=filter_type, **kwds)
Итак, мы нашли проблемное место, но теперь все становится немного странно:
(Pdb) op
<function nanmean at 0x000002CD8ACD4488>
(Pdb) op(delegate)
-9.0
(Pdb) delegate_64 = delegate.astype(np.float64)
(Pdb) op(delegate_64)
-9.000003749978807
(Pdb) delegate.mean()
-9.0000029
(Pdb) delegate_64.mean()
-9.0000037499788075
(Pdb) np.nanmean(delegate, dtype=np.float64)
-9.0000037499788075
(Pdb) np.nanmean(delegate, dtype=np.float32)
-9.0000029
Обратите внимание, что np.nanmean
delegate.mean()
и np.nanmean
выводят -9.0000029
с типом float32
, а не -9.0
как nanmean
делает pandas
nanmean
. Немного покопавшись, вы можете найти источник pandas
nanmean
в pandas.core.nanops
. Интересно, что на самом деле это выглядит так, как будто сначала должно совпадать с numpy
. Давайте посмотрим на pandas
nanmean
:
(Pdb) import inspect
(Pdb) src = inspect.getsource(op).split("\n")
(Pdb) for line in src: print(line)
@disallow('M8')
@bottleneck_switch()
def nanmean(values, axis=None, skipna=True):
values, mask, dtype, dtype_max = _get_values(values, skipna, 0)
dtype_sum = dtype_max
dtype_count = np.float64
if is_integer_dtype(dtype) or is_timedelta64_dtype(dtype):
dtype_sum = np.float64
elif is_float_dtype(dtype):
dtype_sum = dtype
dtype_count = dtype
count = _get_counts(mask, axis, dtype=dtype_count)
the_sum = _ensure_numeric(values.sum(axis, dtype=dtype_sum))
if axis is not None and getattr(the_sum, 'ndim', False):
the_mean = the_sum / count
ct_mask = count == 0
if ct_mask.any():
the_mean[ct_mask] = np.nan
else:
the_mean = the_sum / count if count > 0 else np.nan
return _wrap_results(the_mean, dtype)
Вот (короткая) версия декоратора bottleneck_switch
:
import bottleneck as bn
...
class bottleneck_switch(object):
def __init__(self, **kwargs):
self.kwargs = kwargs
def __call__(self, alt):
bn_name = alt.__name__
try:
bn_func = getattr(bn, bn_name)
except (AttributeError, NameError): # pragma: no cover
bn_func = None
...
if (_USE_BOTTLENECK and skipna and
_bn_ok_dtype(values.dtype, bn_name)):
result = bn_func(values, axis=axis, **kwds)
Это вызывается с alt
в качестве функции nanmean
pandas
, поэтому bn_name
- это 'nanmean'
, и это 'nanmean'
attr, извлеченный из модуля " bottleneck
":
(Pdb) l
93 result = np.empty(result_shape)
94 result.fill(0)
95 return result
96
97 if (_USE_BOTTLENECK and skipna and
98 -> _bn_ok_dtype(values.dtype, bn_name)):
99 result = bn_func(values, axis=axis, **kwds)
100
101 # prefer to treat inf/-inf as NA, but must compute the fun
102 # twice :(
103 if _has_infs(result):
(Pdb) n
> d:\anaconda3\lib\site-packages\pandas\core\nanops.py(99)f()
-> result = bn_func(values, axis=axis, **kwds)
(Pdb) alt
<function nanmean at 0x000001D2C8C04378>
(Pdb) alt.__name__
'nanmean'
(Pdb) bn_func
<built-in function nanmean>
(Pdb) bn_name
'nanmean'
(Pdb) bn_func(values, axis=axis, **kwds)
-9.0
Представьте, что декоратор bottleneck_switch()
не существует ни на секунду. На самом деле мы можем видеть, что вызов этого ручного пошагового выполнения этой функции (без bottleneck
) даст вам тот же результат, что и numpy
:
(Pdb) from pandas.core.nanops import _get_counts
(Pdb) from pandas.core.nanops import _get_values
(Pdb) from pandas.core.nanops import _ensure_numeric
(Pdb) values, mask, dtype, dtype_max = _get_values(delegate, skipna=skipna)
(Pdb) count = _get_counts(mask, axis=None, dtype=dtype)
(Pdb) count
900000.0
(Pdb) values.sum(axis=None, dtype=dtype) / count
-9.0000029
Это никогда не вызывается, если у вас установлено bottleneck
. Вместо этого декоратор bottleneck_switch()
вместо этого nanmean
функцию nanmean
с версией bottleneck
. В этом и заключается несоответствие (что интересно в случае float64
):
(Pdb) import bottleneck as bn
(Pdb) bn.nanmean(delegate)
-9.0
(Pdb) bn.nanmean(delegate.astype(np.float64))
-9.000003749978807
Насколько я могу судить, bottleneck
используется исключительно для скорости. Я предполагаю, что они используют какой-то тип ярлыков с их функцией nanmean
, но я не особо разбирался в этом (подробности по этой теме см. В ответе @ead). Вы можете видеть, что это обычно немного быстрее, чем numpy
по их тестам: https://github.com/kwgoodman/bottleneck. Очевидно, что цена за эту скорость - точность.
Узкое место на самом деле быстрее?
Конечно, выглядит так (по крайней мере, на моей машине).
In [1]: import numpy as np; import pandas as pd
In [2]: x=np.random.normal(-9.8,.05,size=900000)
In [3]: y_32 = x.astype(np.float32)
In [13]: %timeit np.nanmean(y_32)
100 loops, best of 3: 5.72 ms per loop
In [14]: %timeit bn.nanmean(y_32)
1000 loops, best of 3: 854 µs per loop
Для pandas
было бы неплохо ввести здесь флаг (один для скорости, другой для большей точности, по умолчанию для скорости, так как текущий подразумевается). Некоторым пользователям важнее точность вычислений, чем скорость, с которой это происходит.
НТН.