Среднее от панд и numpy отличается

У меня есть MEMS IMU, на котором я собирал данные, и я использую pandas, чтобы получить от него некоторые статистические данные. В каждом цикле собирается 6 32-битных чисел с плавающей запятой. Скорость передачи данных фиксирована для данного цикла сбора данных. Скорость передачи данных варьируется от 100 Гц до 1000 Гц, а время сбора данных составляет 72 часа. Данные сохраняются в плоском двоичном файле. Я читаю данные так:

import numpy as np
import pandas as pd
dataType=np.dtype([('a','<f4'),('b','<f4'),('c','<f4'),('d','<f4'),('e','<f4'),('e','<f4')])
df=pd.DataFrame(np.fromfile('FILENAME',dataType))
df['c'].mean()
-9.880581855773926
x=df['c'].values
x.mean()
-9.8332081

-9,833 - правильный результат. Я могу создать аналогичный результат, который кто-то сможет повторить следующим образом:

import numpy as np
import pandas as pd
x=np.random.normal(-9.8,.05,size=900000)
df=pd.DataFrame(x,dtype='float32',columns=['x'])
df['x'].mean()
-9.859579086303711
x.mean()
-9.8000648778888628

Я повторил это в Linux и Windows, на процессорах AMD и Intel, в Python 2.7 и 3.5. Я в тупике. Что я делаю неправильно? И получи вот это:

x=np.random.normal(-9.,.005,size=900000)
df=pd.DataFrame(x,dtype='float32',columns=['x'])
df['x'].mean()
-8.999998092651367
x.mean()
-9.0000075889406528

Я мог принять эту разницу. Это предел точности 32-битных чисел с плавающей запятой.

НИЧЕГО. Я написал это в пятницу, и решение пришло ко мне сегодня утром. Это проблема точности с плавающей запятой, усугубляемая большим объемом данных. Мне нужно было преобразовать данные в 64-битное число с плавающей запятой при создании фрейма данных следующим образом:

df=pd.DataFrame(np.fromfile('FILENAME',dataType),dtype='float64')

Я оставлю сообщение, если кто-то еще столкнется с подобной проблемой.

Не могу воспроизвести вашу первую проверку, получаю ошибки размером с float32. Обратите внимание, что ваш x содержит двойники, но ваш df содержит числа с плавающей запятой. Это всегда будет давать вам разницу, но не такую ​​большую, как исходная. Есть ли шанс, что у вас есть пропущенные значения, которые мешают вычислению среднего?

Andras Deak 29.10.2018 10:26

Отчасти проблема в том, что Pandas использует плохой алгоритм для вычисления среднего значения; в конечном итоге по мере накопления суммы значение, близкое к -9.8, многократно добавляется к чему-то большему, чем 2**23, а ограниченное разрешение float32 означает, что фактическая сумма изменяется точно на -10,0 для большинства случайных выборок. Использование попарного суммирования или суммирования Кахана вместо простой накопительной суммы могло бы значительно улучшить результат здесь. Но да, вычисление среднего значения с двойной точностью - очевидное быстрое решение.

Mark Dickinson 29.10.2018 19:33

@MarkDickinson, Почему же тогда проблема не проявляется с df['x'].sum() / len(df.index), который дает правильный результат даже с float32?

jpp 29.10.2018 20:49

@jpp: Хороший вопрос. Думаю, вам стоит спросить авторов Pandas. NumPy делает использует попарное суммирование для своих операций sum в некоторых (но не во всех) обстоятельствах; возможно, что по какой-либо причине это конкретное использование df['x'].sum() попадает в один из этих случаев NumPy.

Mark Dickinson 29.10.2018 22:05
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
31
4
5 312
2

Ответы 2

Укороченная версия:

Причина этого в том, что 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's 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

Обратите внимание, что delegate.mean() и np.nanmean выводят -9.0000029 с типом float32, нет-9.0, как pandasnanmean. Немного покопавшись, вы можете найти источник pandasnanmean в pandas.core.nanops. Интересно, что сначала кажется, что должен соответствует numpy. Посмотрим на pandasnanmean:

(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 как функция pandasnanmean, поэтому bn_name - это '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 с версией 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 было бы неплохо ввести здесь флаг (один для скорости, другой для большей точности, по умолчанию - для скорости, поскольку это текущий имп). Некоторые пользователи гораздо больше заботятся о точности вычислений, чем о скорости, с которой они происходят.

HTH.

Вы говорите «numpy превосходит его в float64 для повышения точности», но код, который вы показываете, похоже, не поддерживает это. В numpy.core._methods._mean сумма (вызов umr_sum) в конечном итоге выполняется с помощью dtype=None.

Mark Dickinson 05.11.2018 09:26

Ах, если вы смотрите на x.mean(), то x имеет в первую очередь dtype np.float64. Это могло бы объяснить, почему вы видите результаты float64 внутри среднего.

Mark Dickinson 05.11.2018 09:31

И если вы хотите убедиться, что NumPy не выполняет автоматическое преобразование из float32 в float64 перед выполнением суммирования, попробуйте выполнить np.ones((10**8, 2), dtype=np.float32).mean(axis=0). Именно использование попарное суммирование действительно влияет на точность в случае NumPy. (Что касается того, что делает Pandas: понятия не имею.)

Mark Dickinson 05.11.2018 10:04

@MarkDickinson да, моя ошибка. Я отредактирую позже (в телефоне банкомат и скоро на работу)

Matt Messersmith 05.11.2018 13:14

@MarkDickinson, что касается pandas, однако происходит то, что они исправляют nanmean в пользу bottleneck версии указанной функции. Думаю, я уберу часть о numpy, поскольку то, что они делают, более или менее прямолинейно, и просто проясню, как себя ведет pandas.

Matt Messersmith 05.11.2018 13:20

@MarkDickinson Хорошо, обновлено. Я только что рассмотрел часть numpy: это довольно просто. Это pandas идиот.

Matt Messersmith 05.11.2018 14:26

Отличный ответ + объяснение. Я собираюсь дать этому немного эфирного времени, чтобы он получил больше просмотров. Я надеюсь, что он достигнет разработчиков Pandas. Похоже на непреднамеренное последствие, которое может иметь странные и значительные последствия внеfloat32 против точности float64, например. Крайний пример OP.

jpp 05.11.2018 16:23

Ну, поведение NumPy тоже довольно глупое. Тот факт, что np.ones((10**8, 1), dtype=np.float32).mean(axis=0) и np.ones((2, 10**8), dtype=np.float32).mean(axis=1) точны, а np.ones((10**8, 2), dtype=np.float32).mean(axis=0) - нет, глупо. Объяснимо, конечно, но все равно глупо.

Mark Dickinson 05.11.2018 17:47

Ответ @Matt Messersmith - отличное расследование, но я хотел бы добавить, на мой взгляд, важный момент: оба результата (numpy и pandas) неверны. Однако numpy имеет более высокую вероятность ошибиться, чем panda.

Нет принципиальной разницы между использованием float32 и float64, однако для float32 проблемы могут наблюдаться для меньших наборов данных, чем для float64.

На самом деле не определено, как следует рассчитывать mean - данное математическое определение однозначно только для бесконечно точных чисел, но не для операций с плавающей запятой, которые используют наши ПК.

Итак, какова «правильная» формула?

    mean = (x0+..xn)/n 
  or 
    mean = [(x0+x1)+(x2+x3)+..]/n
  or
    mean = 1.0/n*(x0+..xn)
  and so on...

Очевидно, что при вычислении на современном оборудовании все они дадут разные результаты - в идеале можно было бы взглянуть на формулу, которая дает наименьшую ошибку по сравнению с теоретическим правильным значением (которое рассчитывается с бесконечной точностью).

Numpy использует слегка измененный попарное суммирование, то есть (((x1+x2)+(x3+x4))+(...)), который, даже если не идеален, как известно, неплох. С другой стороны, узкое место использует наивное суммирование x1+x2+x3+...:

REDUCE_ALL(nanmean, DTYPE0)
{
    ...
    WHILE {
        FOR {
            ai = AI(DTYPE0);
            if (ai == ai) {
                asum += ai;   <---- HERE WE GO
                count += 1;
            }
        }
        NEXT
    }
    ...
}

и мы можем легко увидеть, что происходит: после некоторых шагов bottleneck суммирует один большой (сумма всех предыдущих элементов, пропорциональная -9.8*number_of_steps) и один маленький элемент (около -9.8), что приводит к довольно большой ошибке округления около big_number*eps, где eps будет около 1e-7 для float32. Это означает, что после суммирования 10 ^ 6 у нас может быть относительная ошибка около 10% (eps*10^6, это верхняя граница).

Для float64 и eps, близких к 1e-16, относительная ошибка будет только около 1e-10 после суммирования 10 ^ 6. Нам это может показаться точным, но в сравнении с возможной точностью это все равно фиаско!

С другой стороны, Numpy (по крайней мере, для данной серии) добавит два почти равных элемента. В этом случае верхняя граница результирующей относительной ошибки равна eps*log_2(n), что составляет

  • максимальный 2e-6 для float32 и 10 ^ 6 элементов
  • максимальный 2e-15 для float64 и 10 ^ 6 элементов.

Из вышесказанного, среди прочего, есть следующие примечательные последствия:

  • если среднее значение распределения равно 0, тогда pandas и numpy почти одинаково точны - величина суммированных чисел составляет около 0.0, и нет большой разницы между слагаемыми, которая привела бы к большой ошибке округления для наивного суммирования.
  • если кто-то знает хорошую оценку среднего, может быть более надежным вычислить сумму x'i=xi-mean_estimate, потому что x'i будет иметь среднее значение 0.0.
  • чего-то вроде x=(.333*np.ones(1000000)).astype(np.float32) достаточно, чтобы вызвать странное поведение версии pandas - нет необходимости в случайности, и мы знаем, каким должен быть результат, не так ли? Важно, что 0.333 нельзя представить точно с плавающей точкой.

NB: вышесказанное справедливо для одномерных массивов numpy. Ситуация более сложная для суммирования по оси для многомерных массивов numpy, поскольку numpy иногда переключается на наивное суммирование. Для более подробного исследования см. Этот SO-сообщение, который также объясняет @Mark Dickinson наблюдение, то есть:

np.ones((2, 10**8), dtype=np.float32).mean(axis=1) are accurate but np.ones((10**8, 2), dtype=np.float32).mean(axis=0) aren't

Другие вопросы по теме