Il existe de nombreuses questions ( 1 , 2 , ) traitant des valeurs de comptage dans une série unique .
Cependant, il y a moins de questions sur la meilleure façon de compter les combinaisons de deux séries ou plus . Des solutions sont présentées ( 1 , 2 ), mais la question de savoir quand et pourquoi utiliser chacune d'elles n'est pas discutée.
Vous trouverez ci-dessous une analyse comparative de trois méthodes potentielles. J'ai deux questions spécifiques:
grouper
est-il plus efficace que count
? Je m'attendais à ce que count
soit le plus efficace, car il est implémenté en C. Les performances supérieures de grouper
persistent même si le nombre de colonnes est passé de 2 à 4.value_counter
underperform grouper
par tant? Est-ce dû au coût de construction d'une liste ou d'une série à partir d'une liste?Je comprends que les sorties sont différentes, et cela devrait également éclairer le choix. Par exemple, le filtrage par nombre est plus efficace avec des tableaux numpy
contigus par rapport à une compréhension de dictionnaire:
x, z = grouper(df), count(df)
%timeit x[x.values > 10] # 749µs
%timeit {k: v for k, v in z.items() if v > 10} # 9.37ms
Cependant, l'objectif de ma question est la performance de la construction de résultats comparables dans une série par rapport au dictionnaire. Mes connaissances en C sont limitées, mais j'apprécierais toute réponse pouvant indiquer la logique sous-jacente à ces méthodes.
Code de référence
import pandas as pd
import numpy as np
from collections import Counter
np.random.seed(0)
m, n = 1000, 100000
df = pd.DataFrame({'A': np.random.randint(0, m, n),
'B': np.random.randint(0, m, n)})
def grouper(df):
return df.groupby(['A', 'B'], sort=False).size()
def value_counter(df):
return pd.Series(list(Zip(df.A, df.B))).value_counts(sort=False)
def count(df):
return Counter(Zip(df.A.values, df.B.values))
x = value_counter(df).to_dict()
y = grouper(df).to_dict()
z = count(df)
assert (x == y) & (y == z), "Dictionary mismatch!"
for m, n in [(100, 10000), (1000, 10000), (100, 100000), (1000, 100000)]:
df = pd.DataFrame({'A': np.random.randint(0, m, n),
'B': np.random.randint(0, m, n)})
print(m, n)
%timeit grouper(df)
%timeit value_counter(df)
%timeit count(df)
Résultats de l'analyse comparative
Exécuter sur python 3.6.2, pandas 0.20.3, numpy 1.13.1
Spécifications de la machine: Windows 7 64 bits, double cœur 2,5 GHz, 4 Go de RAM.
Clé: g = grouper
, v = value_counter
, c = count
.
m n g v c
100 10000 2.91 18.30 8.41
1000 10000 4.10 27.20 6.98[1]
100 100000 17.90 130.00 84.50
1000 100000 43.90 309.00 93.50
1 Ce n'est pas une faute de frappe.
Il y a en fait un peu de surcharge cachée dans Zip(df.A.values, df.B.values)
. La clé ici se résume à des tableaux numpy stockés en mémoire d'une manière fondamentalement différente de celle des objets Python.
Un tableau numpy, tel que np.arange(10)
, est essentiellement stocké en tant que bloc de mémoire contigu et non en tant qu'objets Python individuels. Inversement, une liste Python, telle que list(range(10))
, est stockée en mémoire en tant que pointeurs vers des objets Python individuels (c'est-à-dire des nombres entiers de 0 à 9). Cette différence explique pourquoi les tableaux numpy sont plus petits en mémoire que les listes équivalentes Python, et pourquoi vous pouvez effectuer des calculs plus rapides sur les tableaux numpy.
Donc, comme Counter
consomme Zip
, les tuples associés doivent être créés en tant qu'objets Python. Cela signifie que Python doit extraire les valeurs de Tuple des données numpy et créer des objets Python correspondants en mémoire. Il y a une surcharge notable à cela, c'est pourquoi vous voulez être très prudent lorsque vous combinez des fonctions Python pures avec des données numpy. Un exemple de base de cet écueil que vous pourriez souvent voir est l'utilisation du Python sum
intégré sur un tableau numpy: sum(np.arange(10**5))
est en fait un peu plus lent que le pur Python sum(range(10**5))
, et les deux sont bien sûr beaucoup plus lents que np.sum(np.arange(10**5))
.
Voir cette vidéo pour une discussion plus approfondie de ce sujet.
À titre d'exemple spécifique à cette question, observez les synchronisations suivantes en comparant les performances de Counter
sur des tableaux numpy zippés par rapport aux listes zippées Python correspondantes.
In [2]: a = np.random.randint(10**4, size=10**6)
...: b = np.random.randint(10**4, size=10**6)
...: a_list = a.tolist()
...: b_list = b.tolist()
In [3]: %timeit Counter(Zip(a, b))
455 ms ± 4.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [4]: %timeit Counter(Zip(a_list, b_list))
334 ms ± 4.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
La différence entre ces deux horaires vous donne une estimation raisonnable des frais généraux discutés précédemment.
Ce n'est pas tout à fait la fin de l'histoire. La construction d'un objet groupby
dans pandas implique également une surcharge, au moins en ce qui concerne ce problème, car il existe des métadonnées groupby
qui ne sont pas strictement nécessaires simplement pour obtenir size
, tandis que Counter
fait la seule chose qui vous tient à cœur. Habituellement, cette surcharge est bien inférieure à la surcharge associée à Counter
, mais à partir d'une expérimentation rapide, j'ai constaté que vous pouvez réellement obtenir des performances légèrement meilleures de Counter
lorsque la majorité de vos groupes se composent simplement d'éléments uniques.
Considérez les horaires suivants (en utilisant la suggestion de @ BallpointBen sort=False
) Qui vont dans le spectre de quelques grands groupes <--> de nombreux petits groupes:
def grouper(df):
return df.groupby(['A', 'B'], sort=False).size()
def count(df):
return Counter(Zip(df.A.values, df.B.values))
for m, n in [(10, 10**6), (10**3, 10**6), (10**7, 10**6)]:
df = pd.DataFrame({'A': np.random.randint(0, m, n),
'B': np.random.randint(0, m, n)})
print(m, n)
%timeit grouper(df)
%timeit count(df)
Ce qui me donne le tableau suivant:
m grouper counter
10 62.9 ms 315 ms
10**3 191 ms 535 ms
10**7 514 ms 459 ms
Bien sûr, tout gain provenant de Counter
serait compensé par une reconversion en Series
, si c'est ce que vous voulez comme objet final.