Áp dụng vs biến đổi trên một đối tượng nhóm


173

Hãy xem xét các khung dữ liệu sau:

     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

Các lệnh sau hoạt động:

> df.groupby('A').apply(lambda x: (x['C'] - x['D']))
> df.groupby('A').apply(lambda x: (x['C'] - x['D']).mean())

nhưng không có công việc nào sau đây:

> 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

Tại sao? Ví dụ trên tài liệu dường như gợi ý rằng việc gọi transformmột nhóm cho phép một người thực hiện xử lý thao tác theo hàng:

# 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)

Nói cách khác, tôi nghĩ rằng biến đổi về cơ bản là một loại áp dụng cụ thể (loại không tổng hợp). Tôi sai ở đâu

Để tham khảo, bên dưới là việc xây dựng khung dữ liệu gốc ở trên:

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
Hàm được truyền vào transformphải trả về một số, một hàng hoặc hình dạng giống như đối số. nếu đó là một số thì số đó sẽ được đặt thành tất cả các thành phần trong nhóm, nếu đó là một hàng, nó sẽ được phát cho tất cả các hàng trong nhóm. Trong mã của bạn, hàm lambda trả về một cột không thể được phát cho nhóm.
HYRY

1
Cảm ơn @HYRY, nhưng tôi bối rối. Nếu bạn xem ví dụ trong tài liệu mà tôi đã sao chép ở trên (tức là với zscore), transformsẽ nhận được hàm lambda giả sử mỗi mục xlà một mục trong groupvà cũng trả về một giá trị cho mỗi mục trong nhóm. Tôi đang thiếu gì?
Amelio Vazquez-Reina

Đối với những người tìm kiếm một giải pháp cực kỳ chi tiết, xem cái này dưới đây .
Ted Petrou

@TedPetrou: tl; dr trong số đó là: 1) applytruyền trong toàn bộ df, nhưng transformtruyền từng cột riêng lẻ dưới dạng Sê-ri. 2) applycó thể trả về bất kỳ đầu ra hình dạng nào (vô hướng / Sê-ri / Khung dữ liệu / mảng / danh sách ...), trong khi transformphải trả về một chuỗi (1D Sê-ri / mảng / danh sách) có cùng độ dài với nhóm. Đó là lý do tại sao OP apply()không cần transform(). Đây là một câu hỏi hay vì tài liệu không giải thích rõ ràng cả hai sự khác biệt. (gần giống với sự khác biệt giữa apply/map/applymaphoặc những thứ khác ...)
smci

Câu trả lời:


144

Hai sự khác biệt chính giữa applytransform

Có hai sự khác biệt lớn giữa transformapplygroupby phương pháp.

  • Đầu vào:
    • applyngầm chuyển tất cả các cột cho mỗi nhóm dưới dạng DataFrame sang hàm tùy chỉnh.
    • trong khi transformchuyển từng cột cho từng nhóm riêng lẻ dưới dạng Sê-ri cho chức năng tùy chỉnh.
  • Đầu ra:
    • Hàm tùy chỉnh được truyền để applycó thể trả về vô hướng hoặc Sê-ri hoặc Khung dữ liệu (hoặc mảng numpy hoặc danh sách chẵn) .
    • Hàm tùy chỉnh được truyền vào transformphải trả về một chuỗi (Sê-ri, mảng hoặc danh sách một chiều ) có cùng độ dài với nhóm .

Vì vậy, transformhoạt động trên chỉ một Sê-ri cùng một lúc và applyhoạt động trên toàn bộ Khung dữ liệu cùng một lúc.

Kiểm tra chức năng tùy chỉnh

Nó có thể giúp khá nhiều để kiểm tra đầu vào cho chức năng tùy chỉnh của bạn được chuyển đến applyhoặc transform.

Ví dụ

Hãy tạo một số dữ liệu mẫu và kiểm tra các nhóm để bạn có thể thấy những gì tôi đang nói về:

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

Chúng ta hãy tạo một hàm tùy chỉnh đơn giản in ra loại đối tượng được truyền ngầm và sau đó đưa ra lỗi để có thể dừng thực thi.

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

Bây giờ chúng ta hãy truyền hàm này cho cả nhóm applytransformcác phương thức để xem đối tượng nào được truyền cho nó:

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

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

Như bạn có thể thấy, một DataFrame được truyền vào inspecthàm. Bạn có thể tự hỏi tại sao loại, DataFrame, được in ra hai lần. Gấu trúc điều hành nhóm đầu tiên hai lần. Nó làm điều này để xác định xem có cách nào nhanh chóng để hoàn thành việc tính toán hay không. Đây là một chi tiết nhỏ mà bạn không nên lo lắng.

Bây giờ, hãy làm điều tương tự với transform

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

Nó được thông qua một Series - một đối tượng Pandas hoàn toàn khác.

Vì vậy, transformchỉ được phép làm việc với một Series duy nhất tại một thời điểm. Nó không phảikhông thể cho nó hành động trên hai cột cùng một lúc. Vì vậy, nếu chúng ta thử và trừ cột atừ bbên trong hàm tùy chỉnh, chúng ta sẽ gặp lỗi transform. Xem bên dưới:

def subtract_two(x):
    return x['a'] - x['b']

df.groupby('State').transform(subtract_two)
KeyError: ('a', 'occurred at index a')

Chúng tôi nhận được KeyError khi gấu trúc đang cố gắng tìm chỉ mục Sê-ri akhông tồn tại. Bạn có thể hoàn thành thao tác này applyvì nó có toàn bộ DataFrame:

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

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

Đầu ra là một Series và hơi khó hiểu khi chỉ mục gốc được giữ, nhưng chúng tôi có quyền truy cập vào tất cả các cột.


Hiển thị đối tượng gấu trúc đã qua

Nó có thể giúp nhiều hơn để hiển thị toàn bộ đối tượng gấu trúc trong chức năng tùy chỉnh, do đó bạn có thể thấy chính xác những gì bạn đang hoạt động. Bạn có thể sử dụng các printcâu lệnh bởi tôi muốn sử dụng displayhàm từ IPython.displaymô-đun để DataFrames được xuất ra độc đáo trong HTML trong sổ ghi chép jupyter:

from IPython.display import display
def subtract_two(x):
    display(x)
    return x['a'] - x['b']

Ảnh chụp màn hình: nhập mô tả hình ảnh ở đây


Biến đổi phải trả về một chuỗi một chiều có cùng kích thước với nhóm

Sự khác biệt khác là transformphải trả về một chuỗi một chiều có cùng kích thước với nhóm. Trong trường hợp cụ thể này, mỗi nhóm có hai hàng, vì vậy transformphải trả về một chuỗi gồm hai hàng. Nếu không thì sẽ xuất hiện lỗi:

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

Thông báo lỗi không thực sự mô tả vấn đề. Bạn phải trả về một chuỗi có cùng độ dài với nhóm. Vì vậy, một chức năng như thế này sẽ hoạt động:

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

Trả về một đối tượng vô hướng duy nhất cũng hoạt động cho transform

Nếu bạn trả về chỉ một vô hướng từ hàm tùy chỉnh của mình, thì transformsẽ sử dụng nó cho từng hàng trong nhóm:

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
npkhông được xác định. Tôi cho rằng người mới bắt đầu sẽ đánh giá cao nếu bạn đưa import numpy as npvào câu trả lời của bạn.
Qaswed

187

Khi tôi cảm thấy bối rối tương tự với .transformhoạt động so với .applytôi đã tìm thấy một vài câu trả lời làm sáng tỏ vấn đề. Câu trả lời này chẳng hạn.

Takeout của tôi cho đến nay là .transformsẽ làm việc (hoặc đối phó) với Series(các cột) tách biệt với nhau . Điều này có nghĩa là trong hai cuộc gọi cuối cùng của bạn:

df.groupby('A').transform(lambda x: (x['C'] - x['D']))
df.groupby('A').transform(lambda x: (x['C'] - x['D']).mean())

Bạn đã yêu cầu .transformlấy các giá trị từ hai cột và 'nó' thực sự không 'nhìn thấy' cả hai cột cùng một lúc (có thể nói như vậy). transformsẽ lần lượt xem xét các cột khung dữ liệu và trả về một chuỗi (hoặc nhóm chuỗi) 'được tạo' của các vô hướng được lặp lại nhiều len(input_column)lần.

Vì vậy, vô hướng này, nên được sử dụng .transformđể thực hiện Serieslà kết quả của một số hàm khử được áp dụng trên một đầu vào Series(và chỉ trên MỘT loạt / cột tại một thời điểm).

Xem xét ví dụ này (trên khung dữ liệu của bạn):

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)

sẽ mang lại:

       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

Điều này hoàn toàn giống như khi bạn chỉ sử dụng nó trên một cột tại một thời điểm:

df.groupby('A')['C'].transform(zscore)

năng suất:

0    0.989
1   -0.478
2    0.889
3   -0.671
4    0.034
5    1.149
6   -1.404
7   -0.509

Lưu ý rằng .applytrong ví dụ trước ( df.groupby('A')['C'].apply(zscore)) sẽ hoạt động theo cùng một cách, nhưng sẽ thất bại nếu bạn thử sử dụng nó trên một khung dữ liệu:

df.groupby('A').apply(zscore)

đưa ra lỗi:

ValueError: operands could not be broadcast together with shapes (6,) (2,)

Vậy nơi nào khác là .transformhữu ích? Trường hợp đơn giản nhất là cố gắng gán kết quả của hàm giảm trở lại khung dữ liệu gốc.

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

năng suất:

     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

Cố gắng tương tự với .applysẽ cho NaNsvào sum_C. Bởi vì .applysẽ trả lại giảm Series, mà nó không biết làm thế nào để phát lại:

df.groupby('A')['C'].apply(sum)

cho:

A
bar    3.973
foo    4.373

Cũng có trường hợp khi .transformđược sử dụng để lọc dữ liệu:

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

Tôi hy vọng điều này thêm một chút rõ ràng hơn.


4
CHÚA ƠI. Sự khác biệt là rất tinh tế.
Dawei

3
.transform()cũng có thể được sử dụng để điền vào các giá trị còn thiếu. Đặc biệt nếu bạn muốn truyền phát ý nghĩa nhóm hoặc thống kê nhóm đến NaNcác giá trị trong nhóm đó. Thật không may, tài liệu về gấu trúc cũng không hữu ích cho tôi.
toán học mạng

Tôi nghĩ rằng trong trường hợp cuối cùng, .groupby().filter()làm điều tương tự. Cảm ơn lời giải thích của bạn, .apply().transform()làm cho tôi bối rối rất nhiều quá.
Gia Hương

điều đó giải thích tại sao df.groupby().transform()không thể làm việc cho một nhóm phụ df, tôi luôn gặp lỗi ValueError: transform must return a scalar value for each grouptransformthấy từng cột một
jerrytim

Tôi thực sự thích ví dụ cuối cùng .transform được sử dụng để lọc dữ liệu. rất tuyệt!
rishi

13

Tôi sẽ sử dụng một đoạn rất đơn giản để minh họa sự khác biệt:

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 trông như thế này:

    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   

Có 3 ID khách hàng trong bảng này, mỗi khách hàng thực hiện ba giao dịch và được trả 1,2,3 đô la mỗi lần.

Bây giờ, tôi muốn tìm khoản thanh toán tối thiểu được thực hiện bởi mỗi khách hàng. Có hai cách để làm điều đó:

  1. Sử dụng apply:

    nhóm.min ()

Sự trở lại trông như thế này:

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. Sử dụng transform:

    phân nhóm.transform (phút)

Sự trở lại trông như thế này:

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    

Cả hai phương thức đều trả về một Seriesđối tượng, nhưng phương lengththức thứ nhất là 3 và phương lengththức thứ hai là 9.

Nếu bạn muốn trả lời What is the minimum price paid by each customer, thì applyphương pháp là lựa chọn phù hợp hơn.

Nếu bạn muốn trả lời What is the difference between the amount paid for each transaction vs the minimum payment, thì bạn muốn sử dụng transform, bởi vì:

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

Apply không hoạt động ở đây đơn giản vì nó trả về Sê-ri kích thước 3, nhưng độ dài của df ban đầu là 9. Bạn không thể tích hợp nó trở lại df ban đầu một cách dễ dàng.


3
Tôi nghĩ rằng đây là một câu trả lời tuyệt vời! Cảm ơn bạn đã dành thời gian để trả lời hơn bốn năm sau khi câu hỏi được hỏi!
Benjamin Dubreu

4
tmp = df.groupby(['A'])['c'].transform('mean')

giống như

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

hoặc là

tmp1 = df.groupby(['A'])['c'].mean()
tmp = df['A'].map(tmp1)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.