¿Cómo agrupar una columna sesgada en varios intervalos de otra en pandas?

Dec 12 2020

Tengo el siguiente pd.DataFrame:

source = pd.DataFrame([[0.99, 0.98, 0.93, 0.81, 0.85, 0.71, 0.7, 0.69, 0.68, 0.66], 
              [100, 12, 312, 23, 2, 12, 32, 21, 21, 21]]).T

Me gustaría convertirlo lo más rápido posible a:

desired_result = pd.DataFrame([[0.9, 0.8, 0.7, 0.6], [424, 25, 44, 63]]).T

Donde arriba defino un intervalo del 0.1cual aplico a la columna 0del sourcemarco de datos y sumo la 1columna del mismo marco de datos. La idea es que esto debería funcionar con diferentes intervalos.

Lo que probé:

  1. Pensé en usar pd.cutpero eso no parece ser lo que estoy buscando.

  2. Sé que si agrego una nueva columna a la sourceque tiene valores duplicados de 0.9, 0.8, 0.7 y 0.6 en las filas correspondientes, entonces puedo usar groupbyen esta nueva columna y luego sum, pero me pregunto si hay una forma más limpia y rápida ¿para hacer esto? por ejemplo, algo como esto:

interval = 0.1
source['ints'] = (source[0] / interval).astype(int)
result = source.groupby(source['ints']).sum().reset_index()
result

Sin embargo, lo anterior no funcionaría si cambiara el intervalo de 0,1 a 0,05, por ejemplo.

Cualquier ayuda sería apreciada.

Respuestas

3 PierreD Dec 12 2020 at 21:28

Para velocidad: intenta siempre vectorizar todo lo que puedas y evita apply tanto como puedas .

Aquí hay una forma más rápida (crédito a @DavidErickson por sort=False):

interval = 0.1
source.groupby(np.trunc(source[0] / interval) * interval, sort=False)[1].sum().reset_index()
# out:
     0      1
0  0.9  424.0
1  0.8   25.0
2  0.7   12.0
3  0.6   95.0

La diferencia de velocidad puede ser bastante dramática para grandes df.

Pruebe con 1 millón de filas, agrupadas en contenedores de 10K:

source = pd.DataFrame(np.random.normal(scale=1000, size=(int(1e6), 2)))

%%timeit
# ... (as above)
26.7 ms ± 292 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Con un applyen su lugar:

1.51 s ± 11 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

(50 veces más lento).

1 DavidErickson Dec 12 2020 at 20:55

Puedes usar una custom_roundfunción de la que hice 3 modificacioneshttps://stackoverflow.com/a/40372261/6366770:

  1. Usé en np.floorlugar de roundcomo quieras bajar.
  2. Esto estropea los valores que están en el "borde" de un contenedor, por lo que agrego + base/100(así 0.9sería 0.9 + .009 = 0.909y redondeo hacia abajo a .9 en lugar de incorrectamente a 0.8), de modo que esté justo encima del borde y redondee hacia abajo correctamente. Creo que esto te cubrirá. Puede hacer 1 / 1000para estar seguro.
  3. La respuesta que comparto estaba buscando int, tan eliminada int, ya que estamos viendo flotadores de redondeo

source = pd.DataFrame(np.random.normal(scale=1000, size=(int(1e6), 2)))

def custom_round(x, y, base):
    return source.groupby((base * np.floor((x + (base / 100)) / base)), sort=False)[y].sum()


%timeit custom_round(source[0], 1, .1)
89.8 ms ± 1.14 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

En mi computadora, la respuesta aceptada es más lenta:

102 ms ± 1.86 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)