Ниже приведен набор данных игрушечной панели с идентификатором панели («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 периодов времени), и приведенный выше код выполняется долго. Буду признателен за любые предложения.
Ваша проблема страдает от комбинаторного взрыва, поскольку условия, связанные с каждым идентификатором, необходимо сравнивать с условиями всех других идентификаторов. Трудно сделать это менее чем за квадратичное время (хотелось бы знать, есть ли у кого-то лучший алгоритм!), но для размера ваших данных даже этого должно быть достаточно, если критические операции векторизованы.
Чтобы заставить 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)
Определенно более эффективный, чем мой исходный код. Спасибо!