[Pandas — Part III] Group by , Pivot Table และ Melt

Supalerk Pisitsupakarn
Data Cafe Thailand
Published in
5 min readApr 3, 2020
รูปที่ 1 : รูปแสดงความสัมพันธ์ ระหว่าง groupby, pivot_table และ melt

ถ้าหากเรามองข้อมูลเราเป็น Hamburger 1 ชิ้น ซึ่งมีวัตถุดิบประกอบไป ขนมปัง, ชีส, มะเขือเทศ, ผัก และ เนื้อ แต่ละส่วนประกอบก็จะมีปริมาณไม่เท่ากัน (เพื่อความง่าย จะขอกำหนดให้หน่วยเป็น unit ) จากรูปที่ 1 จะเห็นได้ว่ามีความสัมพันธ์กัน ระหว่าง groupby , pivot_table และ melt

  • pivot_table ก็จะคล้ายกันกับการ groupby ใน version ของคอลัมน์
  • หาก melt ข้อมูลหลังจากที่ทำ pivot_table ไปแล้ว ข้อมูลก็จะกลับมาอยู่ในรูปของ groupby (ในบางโปรแกรม melt ถูกเรียกว่า un-pivot )

เรามาเจาะดูคำสั่งแต่ละคำสั่งกันเลยดีกว่าครับ :)

Group by

รูปที่ 2 : Concept การทำงานของคำสั่ง groupby

หากใช้คำสั่ง groupby ingredient สิ่งที่เกิดขึ้นก็คือ โปรแกรมจะพยายาม แยกประเภทออกเป็นกลุ่มๆ โดยแต่ละกลุ่มจะมี ingredient เหมือนกัน จากนั้นในแต่ละกลุ่มก็จะทำการยุบรวม ให้เหลือเพียงกลุ่มละ 1 ตัว (กรณีใช้ groupby เพียง 1 คอลัมน์)ซึ่งใช้ Aggregate Function เป็นตัวรวมค่าต่างๆภายในกลุ่ม จากรูปที่ 2 Aggregate Function คือ “sum” โปรแกรมจึงนำค่า unit ในแต่ละกลุ่มมารวมกัน ผลลัพธ์จึงได้เป็น ingredient พร้อมกับ total unit เหมือนดั่งรูปขวาสุด

จากย่อหน้าด้านบน จะมีการพูดถึง Aggregate Function เราลองมาดูกันว่ามีคำสั่งอะไรบ้าง

รูปที่ 3 : ตารางแสดงคำสั่ง Aggregate Function พร้อมความหมาย

เรามาลองการใช้กับ Data จริงของฝั่ง Retail กันนะครับ สำหรับผู้ที่อยากลองทำตาม สามารถโหลดได้จากลิงค์นี้ครับ Retail Case Study Data

#Display first 10 rows
df_transac.head(10)
รูปที่ 4 : ตัวอย่างข้อมูล df_transac 10 แถวแรก

ถ้าเราอยากจะทราบว่าแต่ละ Store_type มี Qty และ total_amt รวมทั้งหมดเท่าไหร่ เราจะต้องทำการ groupby “Store_type” และใช้ Aggregate Function เป็น sum

# cols = ['your_column1' , 'your_column2', ...'your_columnN']
# dataframe.groupby(by= cols).sum()
my_cols = ['Store_type']
df_transac.groupby(by= my_cols).sum()
รูปที่ 5 : ผลลัพธ์จากคำสั่ง df_transac.groupby(by=’Store_type’).sum()

จากรูปที่ 5 : เราจะเห็นได้ว่า เมื่อใช้ Aggregate Function เป็น sum โดยไม่เลือกว่า จะใช้ คอลัมน์ใดมา sum บ้าง พี่ Pandas ของเราก็จัดให้ครบทุก คอลัมน์ที่สามารถ sum ได้ทันที ด้วยความใจดีของ Pandas เราก็จะเห็นข้อมูลประหลาดๆหลุดมาเช่นกัน นั้นก็คือ transaction_id, cust_id, prod_subcat_code และ prod_cat_code ซึ่งเป็น categorical data ไม่ควรจะ sum กันได้ หรือ sum ออกมาแล้วอธิบายต่อได้ ดังนั้นเราควรจะให้พี่ Pandas ของเราแสดงคอลัมน์ที่เราสนใจออกมาเท่านั้น เพราะจะได้ไม่กิน computing power ด้วย ซึ่งสามารถทำได้ดังนี้ครับ

my_cols = ['Store_type']
df_transac.groupby(by= my_cols)[['Qty','total_amt']].sum()
รูปที่ 6 : ผลลัพธ์จากคำสั่ง df_transac.groupby(by= my_cols)[[‘Qty’,’total_amt’]].sum()

คำสั่ง groupby สามารถทำการ group ได้มากกว่า 1 คอลัมน์ เช่น ถ้าหากต้องการจะรู้ Qty และ total_amt โดยรวม แบ่งตาม Store_type และ tran_date สามารถทำได้โดย

my_cols = ['Store_type','tran_date']
df_transac.groupby(by = my_cols)[['Qty','total_amt']].sum()
รูปที่ 7 : ผลลัพธ์จากคำสั่ง df_transac.groupby(by= [‘Store_type’,’tran_date’])[[‘Qty’,’total_amt’]].sum()

แต่เอ๊ะ ! แล้วถ้าเราอยากจะได้ aggregate function แต่ละ column ไม่เหมือนกันจะทำได้ไหม ? คำตอบคือทำได้ครับ !

Group by with multiple Aggregate Function

สมมติว่า มีโจทย์ดังนี้ : เราอยากจะดูว่าลูกค้าแต่ละคน มีเข้ามามี transaction วันแรกเมื่อไหร่ และวันล่าสุดเมื่อไหร่ มีทั้งหมดกี่ transaction และมียอดซื้อทั้งหมดเท่าไหร่

จากโจทย์ลองมาดูว่าจะต้องใช้ คอลัมน์อะไร คู่กันกับ Aggregate Function อะไรบ้าง

  1. อยากจะรู้ Transaction วันแรก → ‘tran_date’ : ‘min’
  2. อยากจะรู้ Transaction วันสุดท้าย → ‘tran_date’ : ‘max’
  3. มีทั้งหมดกี่ Transaction → ‘transaction_id’ : ‘nunique’
  4. ยอดซื้อรวม → ‘total_amt’ : ‘sum’

เริ่ม !!!

สิ่งที่แตกต่างจากการ groupby ปกติเพียง เปลี่ยน Aggregate Function เดิมให้เป็นคำสั่ง .agg() และใส่ตัว argument เป็น dictionary ที่มี key เป็นชื่อ column และมี Aggregate Function เป็น value

# dataframe.groupby(by=['column']).agg(dic_column_aggfunc)
df_transac.groupby(by=['cust_id']).agg({'tran_date' : ['min','max'],
'transaction_id' : 'nunique',
'total_amt' :'sum})
รูปที่ 8 : เป็นผลลัพธ์จากคำสั่ง groupby cust_id และมี aggfunction หลายแบบ

จากรูปที่ 8 จะได้ว่าลูกค้าแต่ละคนนั้นเข้ามาวันแรกเมื่อไหร่ วันสุดท้ายเมื่อไหร่ เคยเข้ามาทำ transaction ทั้งหมดกี่ครั้ง และมียอดซื้อรวมเท่าไหร่ ตามโจทย์ แต่ถ้าหากเราอยากจะนำข้อมูลชุดนี้ไปใช้ต่อ เราก็สามารถนำตัวแปรมารับ และเก็บค่าเหล่านี้ไว้ได้ และเพื่อที่จะให้ใช้งานง่ายขึ้นการควรจะเปลี่ยนชื่อ column ด้วย

df_sum_cust = df_transac.groupby(by='cust_id')\
.agg({'tran_date' : ['min','max'],
'transaction_id' : 'nunique',
'total_amt' :'sum'})
df_sum_cust.columns = ['first_tran_date',
'last_tran_date',
'total_tran' ,
'total_spend']
df_sum_cust
รูปที่ 9 : เป็นผลลัพธ์จากคำสั่ง groupby cust_id และมี aggfunction หลายแบบ พร้อมทั้งเปลี่ยนชื่อคอลัมน์

Pivot Table

pivot_table เป็นคำสั่งที่คล้ายๆกันกับ groupby ซึ่งสิ่งที่แตกต่างกันคือใน pivot_table จะมีให้ใส่ค่าของ columns และ ค่าของ values ด้วย เพื่อเป็นการระบุว่า หลังจากคำนวน Aggregate Function ของ values ในแต่ละค่า ใน columns แล้ว ให้นำไป transpose (เปลี่ยนจากแถว ไปเป็น คอลัมน์)

ฟังดูงงๆใช่ไหมครับ เรามาลองดูตัวอย่างกันดีกว่า

รูปที่ 10 : ภาพอธิบายความต่างระหว่าง groupby และ pivot_table v1

จากรูปที่ 10 จะเห็นได้ว่าทั้ง 2 คำสั่งใกล้เคียงกันมากทีเดียว แต่จริงๆแล้วความสามารถของ pivot_table ยังไม่ได้หมดเท่านี้ เพราะสามารถกำหนดตัว index ได้อีกด้วย ให้เราคิดง่ายๆว่าตัว index ของ pivot_table ก็คือการ groupby ละกัน

เช่นถ้าหากอยากจะได้ยอดรวม total_amt รายวัน และแบ่งตาม Store_type เราจะสามารถหาคำตอบได้จากทั้ง groupby และ pivot_table ดังนี้

groupby method

# groupby method
cols = ['Store_type' ,'tran_date']
df_tran_gb = df_transac.groupby(by=cols)[['total_amt']].sum()
df_tran_gb

pivot_table method

# pivot_table method
df_tran_pv = df_transac.pivot_table(index='tran_date' ,
columns = 'Store_type' ,
values = 'total_amt' ,
aggfunc = 'sum')
df_tran_pv
รูปที่ 11 : ภาพอธิบายความต่างระหว่าง groupby และ pivot_table v2

จากรูปที่ 11 จะสังเกตได้ว่าคำตอบที่ออกมานั้นไม่ได้แตกต่างกันในด้านของตัวเลข แต่จะแตกต่างกันในมุมของ DataFrame format เท่านั้น ถ้าจากเราต้องการจะนำข้อมูลหลังจาก pivot_table มาใช้งานต่อก็สามารถสร้างตัวแปรมารองรับได้ และถ้าหากต้องการจะใช้ tran_date ซึ่งปัจจุบัน ถูกกำหนดให้เป็น index จากคำสั่ง pivot_table เมื่อสักครู่ เราก็สามารถเติมคำสั่ง .reset_index() ลงไปต่อท้ายได้ เพื่อเปลี่ยน tran_date ที่เป็น index อยู่ ให้กลับมาเป็นคอลัมน์ดังเดิม

df_tran_pv.reset_index(inplace = True)
df_tran_pv
รูปที่ 12 : เปรียบเทียบ DataFrame ก่อนและหลังคำสั่ง reset_index()

จากรูปที่ 12 : จะเห็นได้ว่าหลังจากที่ทำการ reset_index() DataFrame จะได้คอลัมน์ที่ชื่อว่า tran_date กลับมา

ข้อสังเกต

จากรูป 11 ถ้า groupby และ pivot_table เหมือนกันจริง ทำไมจำนวน rows * columns ที่ออกมาจาก 2 คำสั่งถึงไม่เท่ากัน รูปซ้าย 4,431 cells รูปขวา 1,129 * 4 = 4,516 cells เพิ่มมาจากไหนตั้ง 85 cells ??

คำตอบ !! การที่ใช้ pivot_table จะเป็นการขึง columns ที่กำหนด เช่น หากมีวันใดวันหนึ่งที่ไม่มียอดขายใน Flagship store หรือ MBR หรือ TeleShop หรือ e-Shop อย่างใดอย่างหนึ่ง เมื่อเรานำ rows ที่เป็น tran_date มา cross กับ Store_type แล้วไม่เจอสิ่งที่จะได้มาจาก Aggregate Function ก็คือค่า null นั่นเอง เรามาลองพิสูจกัน ว่าหลังจาก pivot_table ไปแล้วมีค่า null เกิดขึ้นจรึงหรือไม่ แล้วเป็นจำนวนเท่าไหร่

# Check null values in each column
df_tran_pv.isnull().sum()
รูปที่ 13 : จำนวนค่า Null values ในแต่ละคอลัมน์ของ df_tran_pv

เห็นไหมครับ จำนวน cell ที่เพิ่มขึ้นมา 85 cells นั้น ล้วนเป็นค่า Null values จากคำสั่ง pivot_table ถ้าเราตัด 85 แถวที่เป็น null ไปสุดท้าย ท้ายสุดคือค่าก็จะเท่ากับ groupby นั่นเอง

Melt

Melt ในบางโปรแกรมอาจจะเรียกว่า un-pivot หรือถ้าจะให้ง่าย melt ก็คือส่วนกลับของ pivot_table เดิม การทำ pivot_table คือการ groupby และปรับ format จากแถว ให้เป็นคอลัมน์ และถ้าหากเรานำ DataFrame ที่ผ่านการทำ pivot_table มา melt ผลลัพธ์ที่ออกมา ก็จะเหมือนกับ format การ groupby เช่น หากเรานำ df_tran_pv มาทำการ melt ผลลัพธ์ DataFrame ก็จะกลับไปหน้าตาเหมือนกับตอนที่ groupby หลังจาก reset_index()

df_tran_pv.melt(id_vars= 'tran_date' , 
values_name ='total_amt').dropna()
รูปที่ 14 : ผลลัพธ์จากการนำ df_tran_pv ไป melt และตัด null values ออก

จากรูปที่ 14 DataFrame ใหม่ที่หลังจากมีการตัดค่า null values ออกไป ขนาด = 4,431 rows และ 3 columns ซึ่งมีขนาดเท่ากับการ groupby และ reset_index()

รูปที่ 15 : แสดงความสัมพันธ์ระหว่าง groupby , pivot_table และ melt

จากรูปที่ 15 : ในกรอบสีแดงมีความแตกต่างกันเพียงแค่ตำแหน่งของ คอลัมน์เท่านั้น จึงสามารถสรุปได้ว่า groupby , pivot_table และ melt มีความสัมพันธ์กันจริงๆ

ซึ่ง ไม่ว่าจะเป็น groupby , pivot_table หรือ melt เป็นคำสั่งที่สำคัญและใช้งานบ่อย มากๆในการทำ Data Preparation และ Exploratory Data Analysis เราจะไม่มีทางหนี 3 คำสั่งนี้พ้นอย่างแน่นอน ครับ 555+

สำหรับโพสนี้ก็คงจะสิ้นสุดเพียงเท่านี้ แล้วพบกันใหม่ในโพสหน้าที่จะเป็นเรื่องการ Merge , Join และ Concatenate สำหรับผู้ที่สนใจ ก็อย่าลืมมาติดตามกันนะครับ :)

--

--