web-dev-qa-db-ja.com

pandasとnumpyは異なる

データを収集しているMEMS IMUがあり、pandasを使用して統計データを取得しています。各サイクルで6つの32ビット浮動小数点が収集されています。データレートはデータレートは100Hzから1000Hzの間で変化し、収集時間は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ビットのfloatに変換する必要がありました。

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

他の誰かが同様の問題に遭遇した場合、私は投稿を残します。

29
Rob

ショートバージョン:

異なる理由は、単にpandasに依存するのではなく、bottleneck操作を呼び出すときに、meannumpy(インストールされている場合)を使用するためです。 bottleneckは、(少なくとも私のマシンでは)numpyよりも高速であるように見えますが、精度が犠牲になります。それらはたまたま64ビットバージョンに一致しますが、32ビットランドでは異なります(興味深い部分です)。

ロングバージョン:

これらのモジュールのソースコードを調べるだけでは、何が起こっているのかを知ることは非常に困難です(meanのような単純な計算であっても、数値計算は難しいことがわかります)。頭脳のコンパイルおよびそれらのタイプの間違いを避けるためにデバッガーを使用するために最もよい。デバッガーはロジックを間違えず、何が起こっているかをexactlyで知らせます。

スタックトレースの一部を次に示します(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を出力することに注意してください。not-9.0は、pandasnanmeanと同様です。少し調べてみると、pandas.core.nanopspandasnanmeanのソースを見つけることができます。興味深いことに、実際にはshouldが最初に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)

これはaltpandasnanmean関数として呼び出されるため、bn_name'nanmean'であり、これは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がここにフラグを導入するのはいいかもしれません(1つは速度のため、もう1つはより良い精度のため、デフォルトは現在の実装なので速度のためです)。一部のユーザーは、計算の精度よりも計算の精度を重視しています。

HTH。

19

@Matt Messersmithの答えは素晴らしい調査ですが、重要な点を追加したいと思います:両方の結果(numpyとパンダ)が間違っています。ただし、numpyはパンダよりも間違いが少ない確率が高くなっています。

_float32_と_float64_の使用に基本的な違いはありませんが、_float32_の場合、_float64_の場合よりも小さいデータセットの場合に問題が発生する可能性があります。

meanの計算方法は実際には定義されていません。指定された数学的な定義は、PCが使用している浮動小数点演算ではなく、無限に正確な数値に対して明確です。

それでは、「正しい」式とは何ですか?

_    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は1つの大きな要素(以前のすべての要素の合計、_-9.8*number_of_steps_に比例)と1つの小さな要素(約_-9.8_)を合計します_big_number*eps_のepsが_1e-7_の周りにある、およそ_float32_のかなりの丸め誤差につながります。これは、10 ^ 6の合計後、約10%の相対誤差が発生する可能性があることを意味します(_eps*10^6_、これは上限です)。

_float64_およびepsが約_1e-16_の場合、相対誤差は10 ^ 6の合計後の_1e-10_のみになります。それは私たちには正確に見えるかもしれませんが、可能な精度に対して測定すると、それは大失敗です!

一方、Numpyは(少なくとも当面のシリーズでは)ほぼ等しい2つの要素を追加します。この場合、結果の相対誤差の上限はeps*log_2(n)です。

  • _2e-6_および10 ^ 6要素の最大_float32_
  • _2e-15_および10 ^ 6要素の場合は最大_float64_。

上記から、とりわけ、次の重要な意味があります。

  • 分布の平均が_0_の場合、pandasとnumpyはほぼ同じ精度です-合計数の大きさは約_0.0_であり、大きな違いはありません単純な加算の大きな丸め誤差につながる被加数。
  • 平均の適切な推定値がわかっている場合は、_x'i=xi-mean_estimate_の平均が_x'i_になるため、_0.0_の合計を計算する方がより堅牢になる可能性があります。
  • x=(.333*np.ones(1000000)).astype(np.float32)のようなものは、パンダのバージョンの奇妙な振る舞いを引き起こすのに十分です-ランダム性の必要はなく、結果がどうあるべきかを知っていますか? _0.333_を浮動小数点で正確に提示できないことが重要です。

NB:上記は1次元のnumpy-arrayに当てはまります。 numpyは時々単純な加算に切り替わるため、多次元のnumpy-arraysの軸に沿って合計する状況はより複雑です。より詳細な調査については、この SO-post を参照してください。これは@Mark Dickinsonも説明しています observation 、つまり:

np.ones((2, 10**8), dtype=np.float32).mean(axis=1)は正確ですが、np.ones((10**8, 2), dtype=np.float32).mean(axis=0)は正確ではありません

16
ead