python - dict - pandas dataframe
Pandas groupby.size vs series.value_counts vs collections.Counter con mĂșltiples series (1)
Hay muchas preguntas ( 1 , 2 , 3 ) relacionadas con los valores de conteo en una sola serie .
Sin embargo, hay menos preguntas sobre la mejor manera de contar combinaciones de dos o más series . Las soluciones se presentan ( 1 , 2 ), pero no se discute cuándo y por qué se debe usar cada una.
A continuación hay algunos puntos de referencia para tres métodos posibles. Tengo dos preguntas específicas:
-
¿Por qué el
grouper
es más eficiente quecount
? Esperaba que lacount
fuera más eficiente, ya que se implementa en C. El rendimiento superior delgrouper
persiste incluso si el número de columnas aumenta de 2 a 4. -
¿Por qué
value_counter
grouper
en tanto? ¿Esto se debe al costo de construir una lista, o series de la lista?
Entiendo que los resultados son diferentes, y esto también debería informar la elección.
Por ejemplo, el filtrado por recuento es más eficiente con matrices de
numpy
contiguos en
numpy
un diccionario de comprensión:
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
Sin embargo, mi pregunta se centra en el rendimiento de la construcción de resultados comparables en una serie en comparación con el diccionario. Mi conocimiento de C es limitado, pero apreciaría cualquier respuesta que pueda apuntar a la lógica subyacente de estos métodos.
Código de referencia
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)
Resultados de benchmarking
Ejecutar en python 3.6.2, pandas 0.20.3, numpy 1.13.1
Especificaciones de la máquina: Windows 7 64 bits, doble núcleo 2.5 GHz, 4 GB de RAM.
Clave: g =
value_counter
, 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 Esto no es un error tipográfico.
En realidad, hay un poco de sobrecarga oculta en
zip(df.A.values, df.B.values)
.
La clave aquí se reduce a la cantidad de arreglos que se almacenan en la memoria de una manera fundamentalmente diferente a los objetos de Python.
Una matriz numpy, como
np.arange(10)
, se almacena esencialmente como un bloque de memoria contiguo, y no como objetos individuales de Python.
A la inversa, una lista de Python, como la
list(range(10))
, se almacena en la memoria como punteros a objetos Python individuales (es decir, números enteros 0-9).
Esta diferencia es la base de por qué las matrices numpy son más pequeñas en memoria que las listas equivalentes de Python, y por qué puede realizar cálculos más rápidos en matrices numpy.
Por lo tanto, como
Counter
está consumiendo el
zip
, las tuplas asociadas deben crearse como objetos de Python.
Esto significa que Python necesita extraer los valores de la tupla de los datos numpy y crear los objetos Python correspondientes en la memoria.
Hay una sobrecarga notable en esto, por lo que debe tener mucho cuidado al combinar funciones de Python puras con datos numpy.
Un ejemplo básico de esta trampa que puede ver comúnmente es usar la
sum
Python incorporada en una matriz numpy:
sum(np.arange(10**5))
es en realidad un poco más lenta que la
sum(range(10**5))
Python pura
sum(range(10**5))
, y ambos son, por supuesto, significativamente más lentos que
np.sum(np.arange(10**5))
.
Vea este video para una discusión más detallada de este tema.
Como ejemplo específico de esta pregunta, observe los siguientes tiempos que comparan el rendimiento de
Counter
en matrices numpy comprimidas con las listas de Python comprimidas correspondientes.
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 diferencia entre estos dos tiempos le da una estimación razonable de la sobrecarga analizada anteriormente.
Sin embargo, este no es el final de la historia.
Construir un objeto
groupby
en pandas también implica cierta sobrecarga, al menos en relación con este problema, ya que hay algunos metadatos
groupby
que no son estrictamente necesarios solo para obtener el
size
, mientras que
Counter
hace la única cosa que le interesa.
Por lo general, esta sobrecarga es mucho menor que la asociada con
Counter
, pero a partir de una experimentación rápida, descubrí que en realidad puede obtener un rendimiento ligeramente mejor de
Counter
cuando la mayoría de sus grupos solo constan de elementos individuales.
Considere los siguientes tiempos (utilizando la
sort=False
@ BallpointBen
sort=False
sugerencia) que van a lo largo del espectro de algunos grupos grandes <--> muchos grupos pequeños:
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)
Lo que me da la siguiente tabla:
m grouper counter
10 62.9 ms 315 ms
10**3 191 ms 535 ms
10**7 514 ms 459 ms
Por supuesto, cualquier ganancia de
Counter
se compensaría volviendo a una
Series
, si eso es lo que quiere como su objeto final.