values dict data convert column python pandas dictionary dataframe counter

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:

  1. ¿Por qué el grouper es más eficiente que count ? Esperaba que la count fuera más eficiente, ya que se implementa en C. El rendimiento superior del grouper persiste incluso si el número de columnas aumenta de 2 a 4.
  2. ¿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.