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.
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).
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 pandas
nanmean
le fait. Avec un peu de fouille, vous pouvez trouver la source de pandas
nanmean
dans pandas.core.nanops
. Fait intéressant, il apparaît en fait comme ça devrait correspondre à numpy
au début. Jetons un œil à 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)
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 pandas
nanmean
, 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.
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éments2e-15
maximal pour float64
et 10 ^ 6 éléments.De ce qui précède, entre autres, il y a les implications notables suivantes:
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.x'i=xi-mean_estimate
, car x'i
aura une moyenne de 0.0
.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 maisnp.ones((10**8, 2), dtype=np.float32).mean(axis=0)
ne le sont pas