Python groupby: найти групповую сумму на основе условия для значений строки

Ниже приведен набор данных игрушечной панели с идентификатором панели («id»), временем («time»), значением («value») и некоторыми значениями, которые будут использоваться в качестве условий («cond»).

df = pd.DataFrame({'id' : [1,1,1,1,1,2,2,2,2,2,3,3,3,3,3,4,4,4,4,4], 
                   'time' : [1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5],
                   'value' : [0,1,0,1,1,0,0,1,0,1,1,0,0,1,1,1,0,1,1,1]
                   }) 
cond = np.array([['A','B'],['A','C'],['C','D'],['D','E']])
df['cond'] = pd.Series(list(np.repeat(cond, repeats=[5,5,5,5], axis=0)))

print(df)
    id  time  value    cond
0    1     1      0  [A, B]
1    1     2      1  [A, B]
2    1     3      0  [A, B]
3    1     4      1  [A, B]
4    1     5      1  [A, B]
5    2     1      0  [A, C]
6    2     2      0  [A, C]
7    2     3      1  [A, C]
8    2     4      0  [A, C]
9    2     5      1  [A, C]
10   3     1      1  [C, D]
11   3     2      0  [C, D]
12   3     3      0  [C, D]
13   3     4      1  [C, D]
14   3     5      1  [C, D]
15   4     1      1  [D, E]
16   4     2      0  [D, E]
17   4     3      1  [D, E]
18   4     4      1  [D, E]
19   4     5      1  [D, E]

По сути, я хочу добавить новый столбец, показывающий сумму значений (в столбце «значение») по времени (в столбце «время»), т. е. groupby('time')['value'].transform('sum'), но одна сложность заключается в том, что для каждого идентификатора я хотите суммировать значения других идентификаторов, которые имеют хотя бы один общий элемент в столбце «cond»: например, для id==1 это будет id==2 (поскольку «A» — это общий элемент); для id==2 это будет id==1 (поскольку буква «А» распространена) и id==3 (поскольку буква «С» распространена).

Итак, мой желаемый результат показан в столбце cond_sum_by_time:

    id  time  value    cond  cond_sum_by_time
0    1     1      0  [A, B]                 0
1    1     2      1  [A, B]                 1
2    1     3      0  [A, B]                 1
3    1     4      1  [A, B]                 1
4    1     5      1  [A, B]                 2
5    2     1      0  [A, C]                 1
6    2     2      0  [A, C]                 1
7    2     3      1  [A, C]                 1
8    2     4      0  [A, C]                 2
9    2     5      1  [A, C]                 3
10   3     1      1  [C, D]                 2
11   3     2      0  [C, D]                 0
12   3     3      0  [C, D]                 2
13   3     4      1  [C, D]                 2
14   3     5      1  [C, D]                 3
15   4     1      1  [D, E]                 2
16   4     2      0  [D, E]                 0
17   4     3      1  [D, E]                 1
18   4     4      1  [D, E]                 2
19   4     5      1  [D, E]                 2

Я думаю, что могу достичь своей цели, используя, например, цикл for, но я хотел бы знать, есть ли лучший/более эффективный способ сделать это. Заранее спасибо.

ОБНОВЛЯТЬ:

Ниже приведен мой текущий код, в котором используется цикл for:

# to save the desired new dataframe
new_df = pd.DataFrame() 
# convert the condition list to set for comparison
df['cond'] = df['cond'].apply(lambda x: set(x)) 
# only id and condition
id_cond_df = df.groupby('id').last().reset_index()[['id','cond']] 

# for each id and its condition...
for i, row in id_cond_df.iterrows():
    id = row['id']
    cond = row['cond']
    # find the row indices in the original dataframe (df) where there is at least one same element in the 'cond' column
    idx = df['cond'].apply(lambda x: not x.isdisjoint(cond))    
    common_df = df[idx].reset_index(drop=True)
    # sum by time
    common_df['cond_sum_by_time'] = common_df.groupby('time')['value'].transform('sum') 
    # only the data for the focal id
    common_df = common_df.loc[common_df['id']==id] 
    # store the data in the new dataframe
    new_df = pd.concat([new_df, common_df], axis=0).reset_index(drop=True)
new_df['cond'] = new_df['cond'].apply(lambda x: list(x))

Мой фактический набор данных большой (например, около 20 000 идентификаторов, и каждый идентификатор имеет 240 периодов времени), и приведенный выше код выполняется долго. Буду признателен за любые предложения.

Потяните за рычаг выброса энергососущих проектов
Потяните за рычаг выброса энергососущих проектов
На этой неделе моя команда отменила проект, над которым я работал. Неделя усилий пошла насмарку.
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Веб-скрейпинг, как мы все знаем, это дисциплина, которая развивается с течением времени. Появляются все более сложные средства борьбы с ботами, а...
Библиотека для работы с мороженым
Библиотека для работы с мороженым
Лично я попрощался с операторами print() в python. Без шуток.
Эмиссия счетов-фактур с помощью Telegram - Python RPA (BotCity)
Эмиссия счетов-фактур с помощью Telegram - Python RPA (BotCity)
Привет, люди RPA, это снова я и я несу подарки! В очередном моем приключении о том, как создавать ботов для облегчения рутины. Вот, думаю, стоит...
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Шаг 1: Создание приложения Slack Чтобы создать Slackbot, вам необходимо создать приложение Slack. Войдите в свою учетную запись Slack и перейдите на...
Учебник по веб-скрапингу
Учебник по веб-скрапингу
Привет, ребята... В этот раз мы поговорим о веб-скрейпинге. Целью этого обсуждения будет узнать и понять, что такое веб-скрейпинг, а также узнать, как...
1
0
84
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ваша проблема страдает от комбинаторного взрыва, поскольку условия, связанные с каждым идентификатором, необходимо сравнивать с условиями всех других идентификаторов. Трудно сделать это менее чем за квадратичное время (хотелось бы знать, есть ли у кого-то лучший алгоритм!), но для размера ваших данных даже этого должно быть достаточно, если критические операции векторизованы.

Чтобы заставить NumPy делать это быстро, я решил преобразовать ваши списки условий в целые числа, где каждый бит сообщает, выполняется ли соответствующее условие. Сначала мы используем взорвать , затем кросс-таблицу:

>>> df_expl = df.groupby('id').last().explode('cond')  # Un-nest lists
>>> df_expl
    time  value cond
id
1      5      1    A
1      5      1    B
2      5      1    A
2      5      1    C
3      5      1    C
3      5      1    D
4      5      1    D
4      5      1    E
>>> cross = pd.crosstab(df_expl.index, df_expl.cond)  # Convert to flags
cond   A  B  C  D  E
row_0
1      1  1  0  0  0
2      1  0  1  0  0
3      0  0  1  1  0
4      0  0  0  1  1
>>> conds = (2**np.arange(cross.shape[1])).dot(cross.values.T)  # Combine flags as bits
array([ 3,  5, 12, 24], dtype=int64)

Теперь мы можем обобщить отношения между состояниями разных идентификаторов с помощью простой бинарной матрицы:

>>> common = conds & conds[:, None] != 0
>>> common
array([[ True,  True, False, False],
       [ True,  True,  True, False],
       [False,  True,  True,  True],
       [False, False,  True,  True]])

Остается просто перебрать все временные шаги (к счастью, в вашем случае меньшее измерение) и использовать матричное умножение для суммирования соответствующих значений вместе.

Как полная функция:

def conditional_sum(df):
    df_expl = df.groupby('id').last().explode('cond')
    cross = pd.crosstab(df_expl.index, df_expl.cond)
    conds = (2**np.arange(cross.shape[1])).dot(cross.values.T)
    common = conds & conds[:, None] != 0
    df['cond_sum_by_time'] = 0
    
    for time in df['time'].unique():
        time_vals = df[df['time'] == time]['value'].values
        df.loc[df['time'] == time, 'cond_sum_by_time'] = time_vals.dot(common)
    
    return df

Некоторые тайминги для промежуточных размеров фреймов данных (orig — ваша реализация):

In []: %timeit orig(df_small)  # 500 ids, 50 time steps
5.8 s ± 73.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In []: %timeit conditional_sum(df_small)
103 ms ± 2.46 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In []: %timeit conditional_sum(df_medium)  # 2000 ids, 100 time steps
2.69 s ± 32.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Когда я создаю фрейм данных с 20 000 идентификаторов и 240 временными шагами, функция завершается на моей машине примерно за 16,5 минут (для сравнения, ваша первоначальная реализация оценивалась примерно в 10 часов 20 минут):

In []: %timeit conditional_sum(df_large)  # 20000 ids, 240 time steps
16min 33s ± 20.1 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

Определенно более эффективный, чем мой исходный код. Спасибо!

IJN81 18.02.2023 14:15

Другие вопросы по теме