Подтвердить что ты не робот

Применить 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)})
4b9b3361

Ответ 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