Применить vs transform на объект группы
Рассмотрим следующий файл данных:
A B C D
0 foo one 0.162003 0.087469
1 bar one -1.156319 -1.526272
2 foo two 0.833892 -1.666304
3 bar three -2.026673 -0.322057
4 foo two 0.411452 -0.954371
5 bar two 0.765878 -0.095968
6 foo one -0.654890 0.678091
7 foo three -1.789842 -1.130922
Работают следующие команды:
> df.groupby('A').apply(lambda x: (x['C'] - x['D']))
> df.groupby('A').apply(lambda x: (x['C'] - x['D']).mean())
но ни одна из следующих работ:
> df.groupby('A').transform(lambda x: (x['C'] - x['D']))
ValueError: could not broadcast input array from shape (5) into shape (5,3)
> df.groupby('A').transform(lambda x: (x['C'] - x['D']).mean())
TypeError: cannot concatenate a non-NDFrame object
Почему? Пример в документации, кажется, предполагает, что вызов transform
в группе позволяет выполнить обработку по строке:
# Note that the following suggests row-wise operation (x.mean is the column mean)
zscore = lambda x: (x - x.mean()) / x.std()
transformed = ts.groupby(key).transform(zscore)
Другими словами, я думал, что преобразование - это, по сути, конкретный тип применения (тот, который не агрегируется). Где я ошибаюсь?
Для справки ниже приведена конструкция исходного фрейма данных выше:
df = pd.DataFrame({'A' : ['foo', 'bar', 'foo', 'bar',
'foo', 'bar', 'foo', 'foo'],
'B' : ['one', 'one', 'two', 'three',
'two', 'two', 'one', 'three'],
'C' : randn(8), 'D' : randn(8)})
Ответы
Ответ 1
Так же, как я чувствовал себя смутно с операцией .transform
против .apply
, я нашел несколько ответов, проливающих свет на эту проблему. Этот ответ был очень полезен.
До сих пор моя выгода заключается в том, что .transform
будет работать (или иметь дело) с Series
(столбцами) изолированно друг от друга. Это означает, что в последних двух вызовах:
df.groupby('A').transform(lambda x: (x['C'] - x['D']))
df.groupby('A').transform(lambda x: (x['C'] - x['D']).mean())
Вы попросили .transform
взять значения из двух столбцов, а "он" на самом деле не "видит" оба из них одновременно (так сказать). transform
будет поочередно рассматривать столбцы dataframe и возвращать серию (или группу рядов), "сделанных" из скаляров, которые повторяются len(input_column)
раз.
Таким образом, этот скаляр, который должен использоваться .transform
для создания Series
, является результатом некоторой редукционной функции, применяемой на входе Series
(и только на ONE series/column за раз).
Рассмотрим этот пример (на вашем фрейме):
zscore = lambda x: (x - x.mean()) / x.std() # Note that it does not reference anything outside of 'x' and for transform 'x' is one column.
df.groupby('A').transform(zscore)
даст:
C D
0 0.989 0.128
1 -0.478 0.489
2 0.889 -0.589
3 -0.671 -1.150
4 0.034 -0.285
5 1.149 0.662
6 -1.404 -0.907
7 -0.509 1.653
Это точно так же, как если бы вы использовали его только по одному столбцу за раз:
df.groupby('A')['C'].transform(zscore)
получая:
0 0.989
1 -0.478
2 0.889
3 -0.671
4 0.034
5 1.149
6 -1.404
7 -0.509
Обратите внимание, что .apply
в последнем примере (df.groupby('A')['C'].apply(zscore)
) будет работать точно так же, но он не сработает, если вы попытаетесь использовать его на фрейме данных:
df.groupby('A').apply(zscore)
дает ошибку:
ValueError: operands could not be broadcast together with shapes (6,) (2,)
Итак, где еще .transform
полезно? Простейшим случаем является попытка вернуть результаты функции восстановления обратно к исходному фрейму.
df['sum_C'] = df.groupby('A')['C'].transform(sum)
df.sort('A') # to clearly see the scalar ('sum') applies to the whole column of the group
получая:
A B C D sum_C
1 bar one 1.998 0.593 3.973
3 bar three 1.287 -0.639 3.973
5 bar two 0.687 -1.027 3.973
4 foo two 0.205 1.274 4.373
2 foo two 0.128 0.924 4.373
6 foo one 2.113 -0.516 4.373
7 foo three 0.657 -1.179 4.373
0 foo one 1.270 0.201 4.373
Попытка того же с .apply
даст NaNs
в sum_C
.
Потому что .apply
вернет уменьшенный Series
, который не знает, как передавать назад:
df.groupby('A')['C'].apply(sum)
даяние:
A
bar 3.973
foo 4.373
Также существуют случаи, когда .transform
используется для фильтрации данных:
df[df.groupby(['B'])['D'].transform(sum) < -1]
A B C D
3 bar three 1.287 -0.639
7 foo three 0.657 -1.179
Надеюсь, это добавит немного большей ясности.
Ответ 2
Два основных различия между apply
и transform
Существуют два основных отличия между методами transform
и apply
groupby.
-
apply
неявно передает все столбцы для каждой группы в качестве DataFrame для пользовательской функции, а transform
передает каждый столбец для каждой группы как Series пользовательская функция
- Пользовательская функция, переданная в
apply
, может возвращать скаляр, или Series или DataFrame (или массив numpy или даже список). Пользовательская функция, переданная в transform
, должна возвращать последовательность (одномерную серию, массив или список) той же длины, что и группа.
Итак, transform
работает только по одной серии за раз, а apply
работает сразу со всем DataFrame.
Проверка пользовательской функции
Это может немного помочь проверить входные данные вашей пользовательской функции, переданной в apply
или transform
.
Примеры
Позвольте создать некоторые примеры данных и осмотреть группы, чтобы вы могли видеть, о чем я говорю:
df = pd.DataFrame({'State':['Texas', 'Texas', 'Florida', 'Florida'],
'a':[4,5,1,3], 'b':[6,10,3,11]})
df
Позвольте создать простую настраиваемую функцию, которая выводит тип неявно переданного объекта, а затем вызывает ошибку, так что выполнение может быть остановлено.
def inspect(x):
print(type(x))
raise
Теперь передайте эту функцию методам groupto apply
и transform
, чтобы увидеть, какой объект передан ей:
df.groupby('State').apply(inspect)
<class 'pandas.core.frame.DataFrame'>
<class 'pandas.core.frame.DataFrame'>
RuntimeError
Как вы можете видеть, DataFrame передается в функцию inspect
. Возможно, вам интересно, почему тип DataFrame дважды распечатывается. Pandas выполняет первую группу дважды. Он делает это, чтобы определить, есть ли быстрый способ завершить вычисление или нет. Это небольшая деталь, о которой вы не должны беспокоиться.
Теперь сделаем то же самое с transform
df.groupby('State').transform(inspect)
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>
RuntimeError
Проходит серию - совершенно другой объект Pandas.
Таким образом, transform
разрешено работать только с одной серией за раз. Невозможно, чтобы он действовал на двух столбцах одновременно. Итак, если мы попытаемся вычесть столбец a
из b
внутри нашей пользовательской функции, мы получим ошибку с transform
. См. Ниже:
def subtract_two(x):
return x['a'] - x['b']
df.groupby('State').transform(subtract_two)
KeyError: ('a', 'occurred at index a')
Мы получаем KeyError, поскольку Pandas пытается найти индекс серии a
, который не существует. Вы можете выполнить эту операцию с помощью apply
, поскольку у нее есть весь DataFrame:
df.groupby('State').apply(subtract_two)
State
Florida 2 -2
3 -8
Texas 0 -2
1 -5
dtype: int64
Выход - это серия и немного запутанная, поскольку исходный индекс сохраняется, но у нас есть доступ ко всем столбцам.
Отображение пройденного объекта Pandas
Это может помочь еще больше отобразить весь объект Pandas в пользовательской функции, чтобы вы могли точно видеть, с чем работаете. Вы можете использовать операторы print
, мне нравится использовать функцию display
из модуля IPython.display
, чтобы DataFrames получал красиво выводимый в HTML в ноутбуке jupyter:
from IPython.display import display
def subtract_two(x):
display(x)
return x['a'] - x['b']
Снимок экрана:
![введите описание изображения здесь]()
Преобразование должно возвращать одномерную последовательность того же размера, что и группа
Другое отличие состоит в том, что transform
должен возвращать одномерную последовательность того же размера, что и группа. В этом конкретном случае каждая группа имеет две строки, поэтому transform
должна возвращать последовательность из двух строк. Если это не так, то возникает ошибка:
def return_three(x):
return np.array([1, 2, 3])
df.groupby('State').transform(return_three)
ValueError: transform must return a scalar value for each group
Сообщение об ошибке на самом деле не описывает проблему. Вы должны вернуть последовательность той же длины, что и группа. Таким образом, будет работать такая функция:
def rand_group_len(x):
return np.random.rand(len(x))
df.groupby('State').transform(rand_group_len)
a b
0 0.962070 0.151440
1 0.440956 0.782176
2 0.642218 0.483257
3 0.056047 0.238208
Возврат одного скалярного объекта также работает для transform
Если вы возвращаете только один скаляр из вашей настраиваемой функции, то transform
будет использовать его для каждой из строк в группе:
def group_sum(x):
return x.sum()
df.groupby('State').transform(group_sum)
a b
0 9 16
1 9 16
2 4 14
3 4 14