33 votes

Pandas groupby.size vs series.value_counts vs collections.Counter avec plusieurs séries

Il y a beaucoup de questions ( 1 , 2 , 3 ) traitant du comptage des valeurs dans un série unique .

Cependant, les questions portant sur la meilleure façon de compter sont moins nombreuses. des combinaisons de deux ou plusieurs séries . Des solutions sont présentées ( 1 , 2 ), mais la question de savoir quand et pourquoi il faut les utiliser n'est pas abordée.

Vous trouverez ci-dessous une analyse comparative de trois méthodes potentielles. J'ai deux questions spécifiques :

  1. Pourquoi est-ce que grouper plus efficace que count ? J'attendais count est le plus efficace, car il est implémenté en C. La performance supérieure de grouper persiste même si le nombre de colonnes passe de 2 à 4.
  2. Pourquoi est-ce que value_counter sous-performance grouper à ce point ? Cela est-il dû au coût de la construction d'une liste ou d'une série à partir d'une liste ?

Je comprends que les résultats sont différents, et cela devrait également éclairer le choix. Par exemple, le filtrage par nombre est plus efficace avec des données contiguës. numpy des tableaux par rapport à la compréhension d'un 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, ma question porte sur performance du bâtiment résultats comparables dans une série contre le dictionnaire. Mes connaissances en C sont limitées, mais j'apprécierais toute réponse qui pourrait m'indiquer la logique sous-jacente à ces méthodes.

Code d'évaluation comparative

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écuté sur python 3.6.2, pandas 0.20.3, numpy 1.13.1

Spécifications de la machine : Windows 7 64-bit, Dual-Core 2.5 GHz, 4GB 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.

2 votes

Une petite barre latérale - pd.Series(list(zip(df.A, df.B))).value_counts(sort=False) améliore un petit - donc je suppose que le tri à contribuer en tant que frais généraux en plus du list moulage

0 votes

Je ne suis pas du tout surpris que la fonction conçue sur mesure pour ce cas d'utilisation précis soit la plus performante. pandas en sait bien plus sur la structure de ses données que Counter fait. en outre, pandas est probablement beaucoup moins gourmand en mémoire puisqu'il sait réutiliser sa mémoire existante.

0 votes

@BallpointBen, D'un point de vue philosophique, votre commentaire est parfaitement logique. Pouvez-vous identifier les raisons spécifiques sous-jacentes (par exemple, le hachage, le coût de l'itération, etc.) en vous référant au code source ?

19voto

root Points 15363

Il y a en fait un peu de frais généraux cachés dans zip(df.A.values, df.B.values) . La clé ici réside dans le fait que les tableaux numpy sont stockés en mémoire d'une manière fondamentalement différente des objets Python.

Un tableau numpy, tel que np.arange(10) est essentiellement stocké comme un bloc de mémoire contigu, et non comme des objets Python individuels. Inversement, une liste Python, telle que list(range(10)) Les tableaux numpy sont stockés en mémoire sous forme de pointeurs vers des objets Python individuels (c'est-à-dire des entiers de 0 à 9). Cette différence explique pourquoi les tableaux numpy sont plus petits en mémoire que les listes équivalentes de Python, et pourquoi vous pouvez effectuer des calculs plus rapides sur des tableaux numpy.

Donc, comme Counter consomme le zip les tuples associés doivent être créés en tant qu'objets Python. Cela signifie que Python doit extraire les valeurs des tuple des données numpy et créer les objets Python correspondants en mémoire. Cela entraîne une surcharge notable, c'est pourquoi il faut être très prudent lorsque l'on combine des fonctions Python pures avec des données numpy. Un exemple de base de ce piège que vous pouvez rencontrer couramment est l'utilisation de la fonction intégrée Python sum 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 tous deux sont bien sûr nettement plus lents que np.sum(np.arange(10**5)) .

Voir cette vidéo pour une discussion plus approfondie sur ce sujet.

A titre d'exemple spécifique à cette question, observez les chronologies suivantes comparant les performances de Counter sur les tableaux numpy zippés par rapport aux listes Python zippées 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 temps vous donne une estimation raisonnable de l'overhead discuté précédemment.

Mais ce n'est pas tout à fait la fin de l'histoire. La construction d'un groupby dans pandas implique également une certaine surcharge, du moins en ce qui concerne ce problème, puisqu'il y a une certaine groupby des métadonnées qui ne sont pas strictement nécessaires juste pour obtenir size alors que Counter fait la seule chose singulière dont vous vous souciez. Habituellement, ces frais généraux sont bien moins élevés que ceux associés à Counter mais après une rapide expérimentation, j'ai découvert que vous pouvez obtenir des performances légèrement meilleures avec Counter lorsque la majorité de vos groupes ne sont constitués que d'éléments uniques.

Considérez les chronométrages suivants (à l'aide de @BallpointBen's sort=False suggestion) qui vont le long du spectre de quelques grands groupes <--> beaucoup de 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 Counter serait compensée par la reconversion en un Series si c'est ce que vous voulez comme objet final.

0 votes

Excellente réponse et timings supplémentaires, merci. Une question, avez-vous une référence pour when materializing the zip you're creating tuples of Python objects ? Je pensais que les objets tuple ne sont produits que lorsque l'on appelle list , next etc. Mais je ne savais pas que tuples sont créés en interne avant d'être consommés par Counter .

2 votes

Ma formulation n'était pas claire, je voulais dire que Counter consomme le zip les tuples associés doivent être créés en mémoire. Les tuples sont donc créés tandis que consommé par Counter . En gros, Counter itère sur les zip dans un for de sorte qu'à chaque itération de la boucle, le tuple associé provenant de l'objet zip doit être créé. Ce site _count_elements (ou un équivalent en C) est essentiellement la façon dont la fonction Counter c'est compter les choses.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X