Применить против преобразования к объекту группы

175

Рассмотрим следующий кадр данных:

     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должна возвращать число, строку или ту же форму, что и аргумент. если это число, то число будет установлено для всех элементов в группе, если это строка, она будет транслироваться на все строки в группе. В вашем коде лямбда-функция возвращает столбец, который нельзя передать группе.
HYRY
1
Спасибо @HYRY, но я в замешательстве. Если вы посмотрите на пример в документации, которую я скопировал выше (то есть, с zscore), transformполучит лямбда-функцию, которая предполагает, что каждый xявляется элементом внутри group, и также возвращает значение для элемента в группе. Чего мне не хватает?
Амелио Васкес-Рейна
Для тех, кто ищет чрезвычайно подробное решение, смотрите это ниже .
Тед Петру
@TedPetrou: tl; dr этого: 1) applyпроходит весь df, но transformпередает каждый столбец индивидуально как Серию. 2) applyможет возвращать любой вывод формы (скаляр / серия / массив данных / массив / список ...), тогда как transformдолжен возвращать последовательность (серия 1D / массив / список) той же длины, что и группа. Вот почему ОП apply()не нужно transform(). Это хороший вопрос, так как документ не объяснил оба различия четко. (сродни apply/map/applymap
разнице

Ответы:

147

Два основных различия между applyиtransform

Есть два основных различия между transformи applyгрупповыми методами.

  • Входные данные:
    • applyнеявно передает все столбцы для каждой группы как DataFrame пользовательской функции.
    • В то время как transformкаждый столбец для каждой группы индивидуально в виде серии в пользовательскую функцию.
  • Вывод:
    • applyПереданная пользовательская функция может возвращать скаляр или Series или DataFrame (или массив numpy или даже список) .
    • Переданная пользовательская функция transformдолжна возвращать последовательность (одномерный ряд, массив или список) той же длины, что и группа .

Таким образом, transformработает только с одной серией за раз и applyработает со всем DataFrame одновременно.

Проверка пользовательской функции

Это может помочь немного проверить входные данные для вашей пользовательской функции, переданной applyили transform.

Примеры

Давайте создадим пример данных и проверим группы, чтобы вы могли понять, о чем я говорю:

import pandas as pd
import numpy as np
df = pd.DataFrame({'State':['Texas', 'Texas', 'Florida', 'Florida'], 
                   'a':[4,5,1,3], 'b':[6,10,3,11]})

     State  a   b
0    Texas  4   6
1    Texas  5  10
2  Florida  1   3
3  Florida  3  11

Давайте создадим простую пользовательскую функцию, которая распечатывает тип неявно переданного объекта, а затем выдает ошибку, чтобы выполнение могло быть остановлено.

def inspect(x):
    print(type(x))
    raise

Теперь давайте передадим эту функцию как groupby, так applyи transformметодам, чтобы увидеть, какой объект ей передан:

df.groupby('State').apply(inspect)

<class 'pandas.core.frame.DataFrame'>
<class 'pandas.core.frame.DataFrame'>
RuntimeError

Как видите, DataFrame передается в inspectфункцию. Вы можете быть удивлены, почему тип DataFrame был напечатан дважды. Панды управляют первой группой дважды. Это делает это, чтобы определить, есть ли быстрый способ завершить вычисление или нет. Это небольшая деталь, о которой вам не стоит беспокоиться.

Теперь давайте сделаем то же самое с transform

df.groupby('State').transform(inspect)
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>
RuntimeError

Передано Серию - совершенно другой объект Панд.

Таким образом, 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 пытается найти индекс Series, aкоторый не существует. Вы можете выполнить эту операцию, так applyкак она содержит весь DataFrame:

df.groupby('State').apply(subtract_two)

State     
Florida  2   -2
         3   -8
Texas    0   -2
         1   -5
dtype: int64

Вывод представляет собой серию и немного сбивает с толку, поскольку исходный индекс сохраняется, но у нас есть доступ ко всем столбцам.


Отображение переданного объекта панды

Это может помочь еще больше отобразить весь объект 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
Тед Петру
источник
3
npне определен. Я предполагаю, что новички будут благодарны, если вы включите import numpy as npв свой ответ.
Qaswed
187

Поскольку я чувствовал себя так же запутанным с .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рассмотрим столбцы данных один за другим и вернут обратно серию (или группу рядов), «сделанную» из скаляров, которые повторяются len(input_column).

Таким образом, этот скаляр, который должен использоваться .transformдля создания, Seriesявляется результатом некоторой функции сокращения, применяемой к входу Series(и только к ОДНОЙ серии / столбцу за раз).

Рассмотрим этот пример (на вашем фрейме данных):

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

Я надеюсь, что это добавляет немного большей ясности.

грунтовка
источник
4
О, МОЙ БОГ. Разница такая тонкая.
Давэй
3
.transform()может также использоваться для заполнения пропущенных значений. Особенно, если вы хотите транслировать среднее значение группы или групповую статистику для NaNзначений в этой группе. К сожалению, документация панд также не помогла мне.
cyber-math
Я думаю, что в последнем случае, .groupby().filter()делает то же самое. Спасибо за ваше объяснение, .apply()и .transform()меня это тоже сильно смущает.
Цзясян
это объясняет, почему df.groupby().transform()не может работать для подгруппы df, я всегда получаю сообщение об ошибке, ValueError: transform must return a scalar value for each groupпотому что transformвидит столбцы один за другим
jerrytim
Мне очень понравился последний пример .transform, используемый для фильтрации данных. супер приятно!
Риши Джайн
13

Я собираюсь использовать очень простой фрагмент, чтобы проиллюстрировать разницу:

test = pd.DataFrame({'id':[1,2,3,1,2,3,1,2,3], 'price':[1,2,3,2,3,1,3,1,2]})
grouping = test.groupby('id')['price']

DataFrame выглядит следующим образом:

    id  price   
0   1   1   
1   2   2   
2   3   3   
3   1   2   
4   2   3   
5   3   1   
6   1   3   
7   2   1   
8   3   2   

В этой таблице 3 идентификатора клиента, каждый из которых совершил три транзакции и заплатил 1,2,3 доллара каждый раз.

Теперь я хочу найти минимальный платеж для каждого клиента. Есть два способа сделать это:

  1. Использование apply:

    grouping.min ()

Возвращение выглядит так:

id
1    1
2    1
3    1
Name: price, dtype: int64

pandas.core.series.Series # return type
Int64Index([1, 2, 3], dtype='int64', name='id') #The returned Series' index
# lenght is 3
  1. Использование transform:

    grouping.transform (мин)

Возвращение выглядит так:

0    1
1    1
2    1
3    1
4    1
5    1
6    1
7    1
8    1
Name: price, dtype: int64

pandas.core.series.Series # return type
RangeIndex(start=0, stop=9, step=1) # The returned Series' index
# length is 9    

Оба метода возвращают Seriesобъект, но lengthпервый из них равен 3, а lengthвторой - 9.

Если вы хотите ответить What is the minimum price paid by each customer, то applyметод является более подходящим для выбора.

Если вы хотите ответить What is the difference between the amount paid for each transaction vs the minimum payment, то вы хотите использовать transform, потому что:

test['minimum'] = grouping.transform(min) # ceates an extra column filled with minimum payment
test.price - test.minimum # returns the difference for each row

Apply здесь не работает просто потому, что возвращает серию размера 3, но длина исходного df равна 9. Вы не можете легко интегрировать его обратно в исходный df.

Cheng
источник
3
Я думаю, что это отличный ответ! Спасибо, что нашли время, чтобы ответить более чем через четыре года после того, как вопрос был задан!
Бенджамин
4
tmp = df.groupby(['A'])['c'].transform('mean')

как

tmp1 = df.groupby(['A']).agg({'c':'mean'})
tmp = df['A'].map(tmp1['c'])

или

tmp1 = df.groupby(['A'])['c'].mean()
tmp = df['A'].map(tmp1)
шуй
источник