ใช้การแปลงกับวัตถุกลุ่ม


174

พิจารณา dataframe ต่อไปนี้:

     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และยังส่งคืนค่าต่อรายการในกลุ่ม ฉันพลาดอะไรไป
Amelio Vazquez-Reina

สำหรับผู้ที่กำลังมองหาวิธีการแก้ปัญหาที่มีรายละเอียดมากเห็นหนึ่งด้านล่างนี้
Ted Petrou

@TedPetrou: the tl; dr of นั่นคือ: 1) applyส่งผ่านใน df ทั้งหมด แต่transformส่งผ่านแต่ละคอลัมน์เป็นชุด 2) applyสามารถส่งคืนรูปร่างใด ๆ (scalar / Series / DataFrame / array / list ... ) ในขณะที่transformต้องส่งคืนลำดับ (1D Series / array / list) ความยาวเท่ากับกลุ่ม นั่นเป็นเหตุผลที่ OP ต้องการไม่ได้apply() transform()นี่เป็นคำถามที่ดีเนื่องจากเอกสารไม่ได้อธิบายความแตกต่างทั้งสองอย่างชัดเจน (คล้ายกับความแตกต่างระหว่างapply/map/applymapหรือสิ่งอื่น ๆ ... )
smci

คำตอบ:


146

สองความแตกต่างที่สำคัญระหว่างapplyและtransform

มีสองความแตกต่างที่สำคัญระหว่างที่มีtransformและapplyวิธีการ GroupBy

  • การป้อนข้อมูล:
    • applyส่งผ่านคอลัมน์ทั้งหมดสำหรับแต่ละกลุ่มโดยนัยเป็นDataFrameไปยังฟังก์ชันที่กำหนดเอง
    • ในขณะที่transformผ่านแต่ละคอลัมน์สำหรับแต่ละกลุ่มทีละชุดเป็นฟังก์ชั่นที่กำหนดเอง
  • เอาท์พุท:
    • ฟังก์ชั่นที่กำหนดเองที่ส่งผ่านไปapplyสามารถกลับเกลาหรือซีรีส์หรือ DataFrame (หรืออาร์เรย์ numpy หรือแม้กระทั่งรายการ)
    • ฟังก์ชั่นที่กำหนดเองที่ส่งผ่านไปtransformต้องกลับลำดับ (ซีรีส์หนึ่งมิติอาร์เรย์หรือรายการ) ยาวเช่นเดียวกับกลุ่ม

ดังนั้นใช้transformงานได้เพียงหนึ่งซีรีส์ในแต่ละครั้งและapplyทำงานกับ DataFrame ทั้งหมดในครั้งเดียว

ตรวจสอบฟังก์ชันที่กำหนดเอง

มันสามารถช่วยให้ไม่น้อยในการตรวจสอบการป้อนข้อมูลเพื่อฟังก์ชั่นที่กำหนดเองของคุณผ่านไปหรือapplytransform

ตัวอย่าง

ลองสร้างตัวอย่างข้อมูลและตรวจสอบกลุ่มเพื่อให้คุณเห็นสิ่งที่ฉันกำลังพูดถึง:

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และtransformmethod เพื่อดูว่า object ใดที่ถูกส่งผ่านไป:

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 เนื่องจากหมีแพนด้าพยายามค้นหาดัชนีซีรี่ส์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 ในสมุดบันทึก 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จะดูที่คอลัมน์ dataframe ทีละหนึ่งและส่งคืนชุดข้อมูล (หรือกลุ่มชุดข้อมูล) 'ทำ' ของสเกลาร์ซึ่งมีการทำซ้ำlen(input_column)ครั้ง

ดังนั้นเซนต์คิตส์และเนวิสที่ควรใช้.transformเพื่อทำให้Seriesเป็นผลมาจากฟังก์ชั่นการลดบางอย่างที่ใช้กับอินพุตSeries(และเฉพาะในหนึ่งชุด / คอลัมน์ในเวลา)

ลองพิจารณาตัวอย่างนี้ (ใน dataframe ของคุณ):

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)) จะทำงานในลักษณะเดียวกันทุกประการ แต่มันจะล้มเหลวหากคุณลองใช้มันใน dataframe:

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ค่าในกลุ่มนั้น น่าเสียดายที่เอกสารของนุ่นก็ไม่ได้ช่วยฉันเช่นกัน
ไซเบอร์คณิตศาสตร์

ฉันคิดว่าในกรณีสุดท้าย.groupby().filter()ทำในสิ่งเดียวกัน ขอบคุณสำหรับคำอธิบายของคุณ.apply()และ.transform()ทำให้ฉันสับสนมากเช่นกัน
Jiaxiang

ที่อธิบายว่าทำไมdf.groupby().transform()ไม่สามารถทำงานกับกลุ่มย่อย df ฉันมักจะได้รับข้อผิดพลาดValueError: transform must return a scalar value for each groupเพราะtransformเห็นคอลัมน์หนึ่งโดยหนึ่ง
jerrytim

ฉันชอบตัวอย่างล่าสุด. transform ที่ใช้ในการกรองข้อมูล สุดยอดมาก!
ฤrษีเชน

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
ฉันคิดว่านี่เป็นคำตอบที่ยอดเยี่ยม! ขอบคุณที่สละเวลาตอบคำถามเกินสี่ปีหลังจากถามคำถาม!
Benjamin Dubreu

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)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.