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 есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
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

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