Может быть, немного вопрос новичка, но мой разум действительно застрял.

У меня есть кадр данных с определенными значениями в столбце с именем x, разделенным на две группы.

   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

Моя цель состоит в том, чтобы для каждой строки подсчитать, сколько строк другой группы имеют значение больше, чем текущее. Итак, чтобы сделать это более ясным, для первой строки (группа a) я пытаюсь найти, сколько строк группы b больше 1,7 (ответ — 4). Конечный результат должен выглядеть так:

   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

У меня есть несколько строк в кадре данных, поэтому в идеале мне также хотелось бы относительно быстрое решение.

3
kon176 11 Янв 2022 в 22:26
2
Что вы имеете в виду под несколькими рядами? 100 или 10 000? также только 2 группы в вашем реальном случае?
 – 
Ben.T
11 Янв 2022 в 22:36
1
У меня примерно 130 000 строк. Только две группы, да (а и б).
 – 
kon176
11 Янв 2022 в 22:42

6 ответов

Вот один из способов. На основе rankраспределения в порядке убывания значений x для каждой группы и merge_asof df для самого себя, после замены имени группы для слияния a с ранжированными значениями в b, и наоборот.

# 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 Янв 2022 в 22:57

Вы можете сортировать значения и использовать маски для cumsum в другой группе:

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)

Выход:

     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 Янв 2022 в 23:15

Быстрое решение — использовать панды DataFrame.apply метод.

df['result'] = df.apply(lambda row: df[(df['group'] != row['group']) & (df['x'] > row['x'])].x.count(), axis=1)
0
oh_my_lawdy 11 Янв 2022 в 22:57
Со 100 тыс. строк этот метод может быть медленным.
 – 
Ben.T
11 Янв 2022 в 23:03
@Ben.T ты абсолютно прав
 – 
oh_my_lawdy
11 Янв 2022 в 23:13

Используйте 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)

Выход:

>>> 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

Производительность для 130 тыс. записей

>>> %%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)

Настроить:

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 Янв 2022 в 23:06

Это должно быть довольно эффективно, всего один вид всех x, а затем просто вычисление совокупных сумм

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

В этот момент df2 выглядит так:

    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

Теперь восстановите индекс и выберите acount или bcount в зависимости от группы:

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

Окончательный результат


    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

Производительность (в том же тесте на 130000 строк, что и @Corralien, а не на том же аппаратном наблюдении)

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

Не слишком отличается от решения Corralien, но вы можете использовать широковещательную рассылку, чтобы проверить все элементы в группе «a» на соответствие всем элементам в группе «b» и подсчитать, сколько из них удовлетворяют условию. Затем присоедините результат обратно.

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 Янв 2022 в 23:26