Tal vez sea una pregunta de principiante, pero mi mente está realmente atascada.

Tengo un marco de datos con ciertos valores en una columna llamada x, dividida en dos grupos.

   x     group
1  1.7   a
2  0     b
3  2.3   b
4  2.7   b
5  8.6   a
6  5.4   b
7  4.2   a
8  5.7   b

Mi propósito es para cada fila, contar cuántas filas del otro grupo tienen un valor mayor que el actual. Entonces, para que quede más claro, para la primera fila (grupo a) estoy buscando cuántas filas del grupo b son mayores que 1.7 (la respuesta es 4). El resultado final debería verse como:

   x     group   result
1  1.7   a       4
2  0     b       3
3  2.3   b       2
4  2.7   b       2
5  8.6   a       0
6  5.4   b       1
7  4.2   a       2
8  5.7   b       1

Tengo varias filas en el marco de datos, por lo que idealmente también me gustaría una solución relativamente rápida.

3
kon176 11 ene. 2022 a las 22:26
2
¿Qué quieres decir con varias filas? 100 o 10 000? ¿también solo 2 grupos en tu caso real?
 – 
Ben.T
11 ene. 2022 a las 22:36
1
Tengo aproximadamente 130 000 filas. Sólo dos grupos, sí (a y b).
 – 
kon176
11 ene. 2022 a las 22:42

6 respuestas

Aquí hay una forma. Basado en ranking en orden descendente los valores x por grupo, y merge_asof df consigo mismo, después de intercambiar el nombre del grupo para fusionar a con los valores clasificados en b, viceversa.

# needed for the merge_asof
df = df.sort_values('x')

res = (
    pd.merge_asof(
        df.reset_index(), # to keep original index order
        df.assign(
            # to compare a with b in the merge
            group = df['group'].map({'a':'b', 'b':'a'}), 
            # rank descending to get the number of number above current number
            result = df.groupby('group')['x'].rank(ascending=False)),
        by='group', # same group first, knowing you exchange groups in second df
        on='x', direction='forward') # look forward on x to get the rank
      # complete the result column
      .fillna({'result':0})
      .astype({'result':int})
      # for cosmetic
      .set_index('index')
      .rename_axis(None)
      .sort_index()
)
print(res)
#      x group  result
# 1  1.7     a       4
# 2  0.0     b       3
# 3  2.3     b       2
# 4  2.7     b       2
# 5  8.6     a       0
# 6  5.4     b       1
# 7  4.2     a       2
# 8  5.7     b       1
2
Ben.T 11 ene. 2022 a las 22:57

Puede ordenar los valores y usar máscaras para cumsum por el otro grupo:

df2 = df.sort_values(by='x', ascending=False)
m = df2['group'].eq('a')
df['result'] = m.cumsum().mask(m).fillna((~m).cumsum().where(m)).astype(int)

Salida:

     x group  result
1  1.7     a       4
2  0.0     b       3
3  2.3     b       2
4  2.7     b       2
5  8.6     a       0
6  5.4     b       1
7  4.2     a       2
8  5.7     b       1
2
mozway 11 ene. 2022 a las 23:15

Una solución rápida es utilizar el DataFrame.apply método.

df['result'] = df.apply(lambda row: df[(df['group'] != row['group']) & (df['x'] > row['x'])].x.count(), axis=1)
0
oh_my_lawdy 11 ene. 2022 a las 22:57
Con filas de 100K, este método puede ser lento.
 – 
Ben.T
11 ene. 2022 a las 23:03
@Ben.T tienes toda la razón
 – 
oh_my_lawdy
11 ene. 2022 a las 23:13

Usa np.searchsorted:

df['result'] = 0

a = df.loc[df['group'] == 'a', 'x']
b = df.loc[df['group'] == 'b', 'x']

df.loc[a.index, 'result'] = len(b) - np.searchsorted(np.sort(b), a)
df.loc[b.index, 'result'] = len(a) - np.searchsorted(np.sort(a), b)

Salida:

>>> df
     x group  result
1  1.7     a       4
2  0.0     b       3
3  2.3     b       2
4  2.7     b       2
5  8.6     a       0
6  5.4     b       1
7  4.2     a       2
8  5.7     b       1

Rendimiento para 130 000 registros

>>> %%timeit
    a = df.loc[df['group'] == 'a', 'x']
    b = df.loc[df['group'] == 'b', 'x']
    len(b) - np.searchsorted(np.sort(b), a)
    len(a) - np.searchsorted(np.sort(a), b)

31.8 ms ± 319 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Configuración:

N = 130000
df = pd.DataFrame({'x': np.random.randint(1, 1000, N),
                   'group': np.random.choice(['a', 'b'], N, p=(0.7, 0.3))})
4
Corralien 11 ene. 2022 a las 23:06

Esto debería ser bastante eficiente, solo un tipo de todo x y luego solo calcular sumas acumuladas

df2 = df.sort_values('x', ascending=False).reset_index()
df2['acount'] = (df['group'] == 'a').cumsum()
df2['bcount'] = (df['group'] == 'b').cumsum()
df2 = df2.fillna(0)
df2

En este punto, df2 se ve así:

    index   x   group   acount  bcount
0   5       8.6 a       0.0     0.0
1   8       5.7 b       1.0     0.0
2   6       5.4 b       1.0     1.0
3   7       4.2 a       1.0     2.0
4   4       2.7 b       1.0     3.0
5   3       2.3 b       2.0     3.0
6   1       1.7 a       2.0     4.0
7   2       0.0 b       3.0     4.0

Ahora restaura el índice y elige acount o bcount según el grupo:

df2 = df2.set_index('index').sort_index()
df2['result'] = np.where(df['group']=='a', df2['bcount'],df2['acount']).astype(int)
df2[['x','result']]

Resultado final


    x   group   result
index           
1   1.7 a       4
2   0.0 b       3
3   2.3 b       2
4   2.7 b       1
5   8.6 a       0
6   5.4 b       1
7   4.2 a       2
8   5.7 b       1

Rendimiento (en la misma prueba de 130000 filas que @Corralien, no en el mismo hardware obvio)

65.4 ms ± 957 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
1
piterbarg 11 ene. 2022 a las 23:15

No es muy diferente de la solución de Corralien, pero puede usar la transmisión para hacer una verificación de todos los elementos en el grupo 'a' contra todos los elementos en el grupo 'b' y contar cuántos satisfacen la condición. Luego únete al resultado de nuevo.

import pandas as pd
import numpy as np

a = df.loc[df['group'] == 'a', 'x']
b = df.loc[df['group'] == 'b', 'x']

result = pd.concat([
            pd.Series(np.sum(a.to_numpy() < b.to_numpy()[:, None], axis=0), index=a.index),
            pd.Series(np.sum(b.to_numpy() < a.to_numpy()[:, None], axis=0), index=b.index)])

df['result'] = result

     x group  result
1  1.7     a       4
2  0.0     b       3
3  2.3     b       2
4  2.7     b       2
5  8.6     a       0
6  5.4     b       1
7  4.2     a       2
8  5.7     b       1
1
ALollz 11 ene. 2022 a las 23:26