web-dev-qa-db-fra.com

moyenne de pandas et numpy diffèrent

J'ai un MEMS IMU sur lequel j'ai collecté des données et j'utilise pandas pour en obtenir des données statistiques. Il y a 6 flotteurs 32 bits collectés à chaque cycle. Les taux de données sont fixe pour un cycle de collecte donné. Les débits de données varient entre 100 Hz et 1 000 Hz et les temps de collecte durent jusqu'à 72 heures. Les données sont enregistrées dans un fichier binaire plat. J'ai lu les données de cette façon:

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 est le résultat correct. Je peux créer un résultat similaire que quelqu'un devrait pouvoir répéter de cette façon:

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

J'ai répété cela sur Linux et Windows, sur les processeurs AMD et Intel, en Python 2.7 et 3.5. Je suis perplexe. Que fais-je de mal? Et obtenez ceci:

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

Je pourrais accepter cette différence. C'est à la limite de la précision des flottants 32 bits.

ÇA NE FAIT RIEN. J'ai écrit cela vendredi et la solution m'a frappé ce matin. Il s'agit d'un problème de précision en virgule flottante exacerbé par la grande quantité de données. J'avais besoin de convertir les données en float 64 bits lors de la création de la trame de données de cette façon:

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

Je quitterai le message si quelqu'un d'autre rencontre un problème similaire.

29
Rob

Version courte:

La raison en est différente parce que pandas utilise bottleneck (si elle est installée) lors de l'appel de l'opération mean, au lieu de simplement s'appuyer sur numpy. bottleneck est vraisemblablement utilisé car il semble être plus rapide que numpy (au moins sur ma machine), mais au détriment de la précision. Ils correspondent à la version 64 bits, mais diffèrent en 32 bits (ce qui est la partie intéressante).

Version longue:

Il est extrêmement difficile de dire ce qui se passe simplement en inspectant le code source de ces modules (ils sont assez complexes, même pour des calculs simples comme mean, il s'avère que le calcul numérique est difficile). Il est préférable d'utiliser le débogueur pour éviter la compilation du cerveau et ces types d'erreurs. Le débogueur ne fera pas d'erreur de logique, il vous dira exactement ce qui se passe.

Voici une partie de ma trace de pile (les valeurs diffèrent légèrement car aucune graine pour RNG):

Peut se reproduire (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

Rien d'extraordinaire avec la version de numpy. C'est la version pandas qui est un peu farfelue.

Jetons un œil à l'intérieur de 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)

Nous avons donc trouvé le problème, mais maintenant les choses deviennent un peu bizarres:

(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

Notez que delegate.mean() et np.nanmean Produisent -9.0000029 Avec le type float32, pas-9.0 Comme pandasnanmean le fait. Avec un peu de fouille, vous pouvez trouver la source de pandasnanmean dans pandas.core.nanops. Fait intéressant, il apparaît en fait comme ça devrait correspondre à numpy au début. Jetons un œil à 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)

Voici une (courte) version du décorateur 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)

Ceci est appelé avec alt comme fonction pandasnanmean, donc bn_name Est 'nanmean', Et c'est l'attr qui est récupéré à partir du bottleneck module:

(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

Imaginez que bottleneck_switch() décorateur n'existe pas pendant une seconde. Nous pouvons voir qu'en appelant cette étape manuelle de cette fonction (sans bottleneck), vous obtiendrez le même résultat que 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

Cependant, cela n'est jamais appelé si vous avez bottleneck installé. Au lieu de cela, le décorateur bottleneck_switch() passe à la place sur la fonction nanmean avec la version de bottleneck. C'est là que réside la différence (il est intéressant de noter qu'elle correspond au cas float64, Cependant):

(Pdb) import bottleneck as bn
(Pdb) bn.nanmean(delegate)
-9.0
(Pdb) bn.nanmean(delegate.astype(np.float64))
-9.000003749978807

bottleneck est utilisé uniquement pour la vitesse, pour autant que je sache. Je suppose qu'ils prennent un type de raccourci avec leur fonction nanmean, mais je n'y ai pas beaucoup réfléchi (voir la réponse de @ ead pour plus de détails sur ce sujet). Vous pouvez voir que c'est généralement un peu plus rapide que numpy par leurs repères: https://github.com/kwgoodman/bottleneck . De toute évidence, le prix à payer pour cette vitesse est la précision.

Le goulot d'étranglement est-il réellement plus rapide?

Bien sûr, ça y ressemble (au moins sur ma machine).

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

Il serait peut-être bien que pandas introduise un drapeau ici (un pour la vitesse, l'autre pour une meilleure précision, la valeur par défaut est pour la vitesse puisque c'est l'impl courant). Certains utilisateurs se soucient beaucoup plus de la précision du calcul que de la vitesse à laquelle il se produit.

HTH.

19
Matt Messersmith

La réponse de @Matt Messersmith est une grande enquête, mais je voudrais ajouter un point important à mon avis: les deux résultats (numpy's et pandas ') sont faux. Cependant, numpy a plus de chances d'être moins faux que panda.

Il n'y a pas de différence fondamentale entre l'utilisation de float32 Et float64, Cependant, pour float32, Des problèmes peuvent être observés pour des ensembles de données plus petits que pour float64.

Il n'est pas vraiment défini, comment le mean doit être calculé - la définition mathématique donnée n'est claire que pour les nombres infiniment précis, mais pas pour les opérations en virgule flottante que nos PC utilisent.

Alors, quelle est la "bonne" formule?

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

Évidemment, lorsqu'ils sont calculés sur du matériel moderne, ils donneront tous des résultats différents - on pourrait idéalement jeter un œil à une formule qui fait la plus petite erreur par rapport à une bonne valeur théorique (qui est calculée avec une précision infinie).

Numpy utilise légèrement alterné sommation par paire , c'est-à-dire (((x1+x2)+(x3+x4))+(...)), Qui, même s'il n'est pas parfait, est connu pour être assez bon. D'autre part, goulot d'étranglement utilise la sommation naïve x1+x2+x3+...:

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

et nous pouvons facilement voir ce qui se passe: Après quelques étapes, bottleneck additionne un grand (somme de tous les éléments précédents, proportionnel à -9.8*number_of_steps) et un petit élément (environ -9.8 ), ce qui entraîne une erreur d'arrondi d'environ big_number*eps, avec eps autour de 1e-7 pour float32. Cela signifie qu'après 10 ^ 6 sommations, nous pourrions avoir une erreur relative d'environ 10% (eps*10^6, Il s'agit d'une limite supérieure).

Pour float64 Et eps étant d'environ 1e-16 L'erreur relative ne serait que d'environ 1e-10 Après 10 ^ 6 sommations. Cela peut nous sembler précis, mais mesuré par rapport à la précision possible, c'est toujours un fiasco!

Numpy d'autre part (au moins pour la série en question) ajoutera deux éléments qui sont presque égaux. Dans ce cas, la limite supérieure de l'erreur relative résultante est eps*log_2(n), qui est

  • 2e-6 maximal pour float32 et 10 ^ 6 éléments
  • 2e-15 maximal pour float64 et 10 ^ 6 éléments.

De ce qui précède, entre autres, il y a les implications notables suivantes:

  • si la moyenne de la distribution est 0, alors pandas et numpy sont presque aussi précis - l'ampleur des nombres sommés est d'environ 0.0 et il n'y a pas de grand différence entre les sommations, ce qui entraînerait une grande erreur d'arrondi pour la sommation naïve.
  • si l'on connaît une bonne estimation de la moyenne, il pourrait être plus robuste de calculer la somme de x'i=xi-mean_estimate, car x'i aura une moyenne de 0.0.
  • quelque chose comme x=(.333*np.ones(1000000)).astype(np.float32) suffit pour déclencher l'étrange comportement de la version des pandas - pas besoin d'aléatoire, et nous savons quel devrait être le résultat, n'est-ce pas? Il est important que 0.333 Ne puisse pas être présenté avec précision en virgule flottante.

NB: Ce qui précède est valable pour les tableaux numpy à 1 dimension. La situation est plus compliquée pour la sommation le long d'un axe pour les matrices multidimensionnelles numpy, car numpy passe parfois à la sommation naïve. Pour une enquête plus détaillée, voir ceci SO-post , qui explique également @Mark Dickinson observation , c'est-à-dire:

np.ones((2, 10**8), dtype=np.float32).mean(axis=1) sont précis mais np.ones((10**8, 2), dtype=np.float32).mean(axis=0) ne le sont pas

16
ead