Предположим, у меня есть DataFrame Pandas, я приведу простой пример:
import pandas as pd
df = pd.DataFrame(columns=["A", "B"], data = [(1, 2), (4, 5), (7, 8), (10, 11)])
У меня есть набор индексов, давайте сделаем его простым и случайным:
inds = [(0, 1, 3), (0, 1, 2), (1, 2, 3)]
Я хочу агрегировать данные в соответствии с этими индексами следующим образом, например, если операция агрегирования является средним значением, которое я бы получил:
df.loc[inds[0], "A"].mean()
df.loc[inds[0], "B"].mean()
df.loc[inds[1], "A"].mean()
df.loc[inds[1], "B"].mean()
df.loc[inds[2], "A"].mean()
df.loc[inds[2], "B"].mean()
Есть ли способ выполнить это в чистых пандах без написания цикла?
Это очень похоже на операцию типа df.groupby
, а затем .agg
, но я не нашел способа создать объект GroupBy из пользовательского набора индексов.
DataFrame, соответствующий таблице в сообщении.
Редактировать: показано, как добиться этого с помощью groupby
, но, конечно, «значительно проще думать об этом как о проблеме выбора по индексу»; см. ответ от @HenryEcker.
Вариант 1 (reindex
+ groupby
)
s = pd.Series(inds).explode()
out = df.reindex(s).groupby(s.index).mean()
out
A B
0 5.0 6.0 # i.e. A: (1+4+10)/3, B: (2+5+11)/3, etc.
1 4.0 5.0
2 7.0 8.0
Объяснение
inds
, чтобы создать pd.Series (здесь: s
), и примените series.explode. Значения индекса функционируют как идентификаторы групп:# intermediate series ('group 0, 1, 2')
0 0
0 1
0 3
1 0
1 1
1 2
2 1
2 2
2 3
dtype: object
s
, используйте df.groupby с s.index
и получите groupby.mean.Вариант 2 (merge
+ groupby
)
out = (
df.merge(
pd.Series(inds, name='g').explode(),
left_index=True,
right_on='g',
how='right'
)
.drop(columns=['g'])
.groupby(level=0)
.mean()
)
# same result
Объяснение
pd.Series
и разбираем его, но на этот раз мы добавляем имя, которое нам понадобится для слияния на следующем шаге.how=right
, чтобы добавить значения из df
, используя значения g
из нашей серии и индекс из df
в качестве ключей.level=0
) и получите groupby.mean
.В этом случае, хотя решение через призму группировки панд, безусловно, возможно, значительно проще думать об этом как о проблеме выбора по индексу.
Функция pandas groupby в первую очередь предназначена для упрощения преобразования данных в индексы, которые можно использовать для идентификации групп. Однако в этом случае мы начинаем с жизнеспособных значений индекса, поэтому нам не нужна помощь функции для повторного создания этих значений.
Мы можем просто преобразовать наш DataFrame в_numpy, а затем использовать inds
, чтобы выбрать нужные значения.
df.to_numpy()[inds, :]
# [[[ 1 2]
# [ 4 5]
# [10 11]]
#
# [[ 1 2]
# [ 4 5]
# [ 7 8]]
#
# [[ 4 5]
# [ 7 8]
# [10 11]]]
Отсюда легко взять среднее по любому измерению, которое мы хотим.
a = df.to_numpy()[inds, :].mean(axis=1)
# [[5. 6.]
# [4. 5.]
# [7. 8.]]
Это можно превратить обратно в DataFrame, обернув его в конструктор.
new_df = pd.DataFrame(df.to_numpy()[inds, :].mean(axis=1), columns=df.columns)
# A B
# 0 5.0 6.0
# 1 4.0 5.0
# 2 7.0 8.0
При желании можно даже использовать inds
в качестве index
нашего DataFrame:
new_df = pd.DataFrame(df.to_numpy()[inds, :].mean(axis=1), columns=df.columns, index=inds)
# A B
# (0, 1, 3) 5.0 6.0
# (0, 1, 2) 4.0 5.0
# (1, 2, 3) 7.0 8.0
Существует множество готовых агрегаций ndarray, но если необходимо выполнить какую-то пользовательскую агрегацию, используйте apply_along_axis с функцией или лямбда-выражением вместо означает:
def custom_fn(a):
# Whatever calculations
return np.mean(a) / np.std(a) + np.ptp(a)
new_df = pd.DataFrame(
np.apply_along_axis(custom_fn, 1, df.to_numpy()[inds, :]),
columns=df.columns,
index=inds
)
# A B
# (0, 1, 3) 10.336306 10.603567
# (0, 1, 2) 7.632993 8.041241
# (1, 2, 3) 8.857738 9.265986
+1. И вот я читал о проблеме XY буквально вчера... Это действительно приятно! Очевидно, это то, что должен использовать ОП.
Отличное решение проблемы, о которой я заявил, однако мне следовало взять индексы, которые, возможно, не одинакового размера, например inds = [(0, 1), (0, 1, 2), (0, 1, 2, 3)]
потому что моя реальная проблема требует именно таких, и это решение не работает в этом случае, в то время как решение @ouroboros1 работает . Извините за эту ошибку
Да, этот подход масштабируется только в том случае, если индексы имеют обычную длину. Вы, конечно, можете вернуться к некоторой итерации: a = df.to_numpy()
и создать из нее новый DF new_df = pd.DataFrame([a[i, :].mean(axis=0) for i in inds])
. Вы можете рассмотреть возможность проведения некоторых тестов производительности. Наверняка существуют размеры DF, где понимание массива numpy происходит быстрее, чем операции pandas. Это, конечно, зависит от вашей фактической агрегации и данных.
Можете ли вы предоставить, как выглядит ожидаемый результат?