Застосувати проти перетворення на об'єкті групи


174

Розглянемо наступний фрейм даних:

     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може повернути будь-який вихід фігури (скалярний / Серія / DataFrame / масив / список ...), тоді як transformповинен повернути послідовність (1D серія / масив / список) тієї ж довжини, що і група. Ось чому ОП apply()не потрібно transform(). Це гарне запитання, оскільки доктор не пояснив обох відмінностей чітко. (схоже на відмінність між apply/map/applymapчи іншими речами ...)
smci

Відповіді:


146

Дві основні відмінності між applyтаtransform

Існують дві основні відмінності між методами transformта applyгруповими методами.

  • Вхід:
    • applyнеявно передає всі стовпці для кожної групи як DataFrame до спеціальної функції.
    • при цьому transformпередає кожен стовпчик для кожної групи окремо як Серія до спеціальної функції.
  • Вихід:
    • Спеціальна функція, передана до, applyможе повернути скаляр, або серію або DataFrame (або масивний масив або навіть список) .
    • Спеціальна функція, передана тому, 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

Тепер передамо цю функцію і груповому методу, 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

Це передається Серія - абсолютно інший об'єкт 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, коли панда намагається знайти індекс Series, aякий не існує. Ви можете завершити цю операцію, applyоскільки вона має весь DataFrame:

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

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

Вихід - це серія та трохи заплутана, оскільки зберігається початковий індекс, але ми маємо доступ до всіх стовпців.


Відображення переданого об'єкта панд

Це може допомогти ще більше відобразити весь об’єкт панди в межах спеціальної функції, так що ви можете точно бачити, з чим ви працюєте. Ви можете використовувати printзаяви, які я люблю використовувати displayфункцію з IPython.displayмодуля, щоб DataFrames добре виводився в HTML у зошиті з юпітера:

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
О БОЖЕ МІЙ. Різниця настільки тонка.
Dawei

3
.transform()може також використовуватися для заповнення пропущених значень. Особливо, якщо ви хочете транслювати середню групу або групову статистику для NaNзначень у цій групі. На жаль, документація на панди також не була корисною для мене.
кібер-математика

Я думаю, що в останньому випадку .groupby().filter()робить те саме. Дякую за ваше пояснення, .apply()і .transform()мене дуже багато плутає.
Jiaxiang

це пояснює, чому df.groupby().transform()не можна працювати для підгрупи df, я завжди отримую помилку, ValueError: transform must return a scalar value for each groupоскільки transformбачить стовпці по черзі
jerrytim

Мені дуже сподобався останній приклад .transform, який використовується для фільтрації даних. супер приємно!
rishi jain

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.


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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.