เนื้อหาต่อจาก
บทที่แล้วที่ได้อธิบายการใช้ groupby เพื่อจัดกลุ่มข้อมูลไป ในบทนี้จะพูดถึงการใช้ฟังก์ชันต่างๆสำหรับจัดการกับข้อมูลที่จัดกลุ่มแล้ว
การใช้เมธอด apply กับข้อมูลที่จัดกลุ่มแล้ว ข้อมูลที่จัดกลุ่มแล้วก็มีเมธอด apply เช่นเดียวกับซีรีส์หรือเดตาเฟรมธรรมดาดังที่ได้กล่าวไปใน
บทที่ ๙ เพียงแต่เมธอด apply ที่ทำกับออบเจ็กต์ข้อมูลที่จัดกลุ่มแล้วนี้จะเป็นการทำซ้ำโดยแยกทำตามกลุ่มที่แยก (ไม่ได้ทำโดยแยกแถวหรือคอลัมน์เหมือนอย่างใน apply ที่ทำกับเดตาเฟรมโดยตรง)
โดยเมื่อป้อนฟังก์ชันเข้าไปโดยใช้ apply ข้อมูลเป็นเดตาเฟรมที่ได้จากการแยกในแต่ละกลุ่มจะถูกส่งเข้าไปเป็นอาร์กิวเมนต์ของฟังก์ชันนั้น
ตัวอย่าง เริ่มจากสร้างตารางโปเกมอนที่เหมือนกับในบทที่แล้ว
import pandas as pd
pokemon = pd.DataFrame([
['ฟุชิงิดาเนะ','พืช/พิษ',0.7,6.9],
['ฟุชิงิโซว','พืช/พิษ',1.0,13.0],
['ฟุชิงิบานะ','พืช/พิษ',2.4,155.5],
['ฮิโตคาเงะ','ไฟ',0.6,8.5],
['ลิซาร์โด','ไฟ',1.1,19.0],
['ลิซาร์ดอน','ไฟ/บิน',1.7,101.5],
['เซนิงาเมะ','น้ำ',0.5,9.0],
['คาเมล','น้ำ',1.0,22.5],
['คาเม็กซ์','น้ำ',1.6,101.1]],
columns=['สายพันธุ์','ชนิด','ส่วนสูง','น้ำหนัก'],
index=[1,2,3,4,5,6,7,8,9])
print(pokemon)
ได้
|
สายพันธุ์ |
ชนิด |
ส่วนสูง |
น้ำหนัก |
1 |
ฟุชิงิดาเนะ |
พืช/พิษ |
0.7 |
6.9 |
2 |
ฟุชิงิโซว |
พืช/พิษ |
1.0 |
13.0 |
3 |
ฟุชิงิบานะ |
พืช/พิษ |
2.4 |
155.5 |
4 |
ฮิโตคาเงะ |
ไฟ |
0.6 |
8.5 |
5 |
ลิซาร์โด |
ไฟ |
1.1 |
19.0 |
6 |
ลิซาร์ดอน |
ไฟ/บิน |
1.7 |
101.5 |
7 |
เซนิงาเมะ |
น้ำ |
0.5 |
9.0 |
8 |
คาเมล |
น้ำ |
1.0 |
22.5 |
9 |
คาเม็กซ์ |
น้ำ |
1.6 |
101.1 |
จากนั้นลองป้อนฟังก์ชัน type ลงไปโดยใช้ apply
print(pokemon.groupby('ชนิด').apply(type))
ได้
ชนิด
น้ำ <class 'pandas.core.frame.DataFrame'>
พืช/พิษ <class 'pandas.core.frame.DataFrame'>
ไฟ <class 'pandas.core.frame.DataFrame'>
ไฟ/บิน <class 'pandas.core.frame.DataFrame'>
dtype: object
จะเห็นว่าข้อมูลในแต่ละกลุ่มถูกส่งเข้าไปเป็นอาร์กิวเมนต์ของฟังก์ชัน type ในรูปของเดตาเฟรม ในที่นี้ทำการคืนค่าคลาสซึ่งจะเห็นว่าเป็นเดตาเฟรม
วิธีที่มีประสิทธิภาพในการใช้ apply ก็คือใช้คู่กับ lambda เช่น ถ้าต้องการดัชนีของแต่ละแถวในกลุ่ม
print(pokemon.groupby('ชนิด').apply(lambda x:x.index.values))
ได้
ชนิด
น้ำ [7, 8, 9]
พืช/พิษ [1, 2, 3]
ไฟ [4, 5]
ไฟ/บิน [6]
dtype: object
หากผลจากฟังก์ชันซึ่งได้มาในแต่ละกลุ่มอยู่ในรูปของซีรีส์ มันก็จะหลอมรวมกันเป็นเดตาเฟรม เช่นถ้าใช้เมธอด max
print(pokemon.groupby('ชนิด').apply(lambda x:x.max()))
max ในที่นี้ดูจะให้ผลคล้ายกับใช้ .max() โดยตรงโดยไม่ผ่าน apply เพียงแต่ข้อมูลทั้งตารางจะถูกส่งมาดังนั้นคอลัมน์ "ชนิด" ก็ปรากฏซ้ำในนี้ด้วย
|
สายพันธุ์ |
ชนิด |
ส่วนสูง |
น้ำหนัก |
ชนิด |
|
|
|
|
น้ำ |
เซนิงาเมะ |
น้ำ |
1.6 |
101.1 |
พืช/พิษ |
ฟุชิงิโซว |
พืช/พิษ |
2.4 |
155.5 |
ไฟ |
ฮิโตคาเงะ |
ไฟ |
1.1 |
19.0 |
ไฟ/บิน |
ลิซาร์ดอน |
ไฟ/บิน |
1.7 |
101.5 |
แต่ในบางกรณีก็อาจรวมกันเป็นซีรีส์ที่มีดัชนีซ้อน เช่น
print(pokemon.groupby('ชนิด').apply(lambda x:x['สายพันธุ์']))
ได้
ชนิด
น้ำ 7 เซนิงาเมะ
8 คาเมล
9 คาเม็กซ์
พืช/พิษ 1 ฟุชิงิดาเนะ
2 ฟุชิงิโซว
3 ฟุชิงิบานะ
ไฟ 4 ฮิโตคาเงะ
5 ลิซาร์โด
ไฟ/บิน 6 ลิซาร์ดอน
Name: สายพันธุ์, dtype: object
การใช้ apply แม้อาจจะดูซับซ้อนสักหน่อย แต่ก็ยืดหยุ่นสามารถเอาไปประยุกต์ใช้ทำอะไรต่างๆได้มากมาย
การใช้เมธอด agg มีเมธอดอีกอันที่คล้ายกับ apply คือเอาไว้ป้อนฟังก์ชันเข้าไปให้กับข้อมูลในแต่ละกลุ่ม นั่นคือ agg
แต่มีข้อแตกต่างกันคือในขณะที่เมธอด apply จะส่งตารางข้อมูลเดตาเฟรมของข้อมูลที่ถูกแยกกลุ่มไปเป็นอาร์กิวเมนต์ของฟังก์ชันทั้งหมด agg นั้นจะแยกย่อยแต่ละเดตาเฟรมออกเป็นคอลัมน์ (เป็นซีรีส์) แล้วค่อยส่งให้ฟังก์ชันทำงานแยกกัน
เรื่องนี้ค่อนข้างซับซ้อน การอธิบายเป็นคำพูดน่าจะเข้าใจยากมาก เพื่อให้เห็นภาพชัดจำเป็นจะต้องยกตัวอย่าง น่าจะพอช่วยได้
ตัวอย่างเช่นใช้ฟังก์ชัน lambda x:x.shape ซึ่งเป็นฟังก์ชันที่จะคืนรูปร่างขนาดของอาเรย์หรือเดตาเฟรม
เมื่อใช้กับ apply (เดตาเฟรมยังคงใช้ข้อมูลเดิมจากตัวอย่างก่อนๆ)
print(pokemon.groupby('ชนิด').apply(lambda x:x.shape))
ได้
ชนิด
น้ำ (3, 4)
พืช/พิษ (3, 4)
ไฟ (2, 4)
ไฟ/บิน (1, 4)
dtype: object
ผลที่ได้จะแบ่งเป็นแต่ละกลุ่ม ซึ่งในแต่ละกลุ่มจะมีหนึ่งเดตาเฟรมเอามาเข้าฟังก์ชันเพื่อหาค่า shape เดตาเฟรมมีสองมิติจึงได้ค่าออกมาเป็นเลข ๒ ตัว ซึ่งคือจำนวนข้อมูลในแต่ละกลุ่มและจำนวนคอลัมน์
แต่เมื่อใช้กับ agg
print(pokemon.groupby('ชนิด').agg(lambda x:x.shape))
ได้
|
สายพันธุ์ |
ส่วนสูง |
น้ำหนัก |
ชนิด |
|
|
|
น้ำ |
(3,) |
(3,) |
(3,) |
พืช/พิษ |
(3,) |
(3,) |
(3,) |
ไฟ |
(2,) |
(2,) |
(2,) |
ไฟ/บิน |
(1,) |
(1,) |
(1,) |
จะเห็นว่า agg นั้นนอกจากจะคิดแยกย่อยแต่ละกลุ่มแล้วยังแบ่งย่อยเป็นคอลัมน์ โดยแต่ละตัวที่แบ่งออกมาจะเป็นซีรีส์ ซึ่งมีมิติเดียว จึงได้ค่า shape เป็นเลขตัวเดียว
นอกจากนี้ยังจะเห็นได้ว่าคอลัมน์ "ชนิด" ซึ่งใช้เป็นตัวจัดกลุ่มจะไม่ถูกรวมอยู่ในนี้ด้วย ซึ่งต่างจาก apply ที่จะมาหมดทุกคอลัมน์
นอกจากใส่ชื่อฟังก์ชันแล้ว บางฟังก์ชันยังสามารถใส่ในรูปสายอักขระได้ ไม่จำเป็นต้องใส่ตัวฟังก์ชัน เช่นถ้าจะหาค่าเฉลี่ยก็ใส่ mean หาผลรวมก็ใส่ sum เป็นต้น นอกจากนั้นก็ยังมีอีกหลายตัวเช่น min, max, std, var, describe, ฯลฯ
เช่นใช้ mean (หมายเหตุ mean ไม่สามารถใช้กับสายอักขระได้ ดังนั้นคอลัมน์ "สายพันธุ์" จะไม่ปรากฏ)
print(pokemon.groupby('ชนิด').agg('mean'))
# มีค่าเท่ากับ print(pokemon.groupby('ชนิด').agg(lambda x:x.mean()))
ได้
|
ส่วนสูง |
น้ำหนัก |
ชนิด |
|
|
น้ำ |
1.033333 |
44.200000 |
พืช/พิษ |
1.366667 |
58.466667 |
ไฟ |
0.850000 |
13.750000 |
ไฟ/บิน |
1.700000 |
101.500000 |
สามารถใช้หลายฟังก์ชันพร้อมกันได้ด้วยโดยใส่เป็นลิสต์ของฟังก์ชัน (หรือสายอักขระชื่อฟังก์ชันนั้น) ผลที่ได้จะออกมาเป็นคอลัมน์ซ้อน เช่น
print(pokemon.groupby('ชนิด').agg(['sum',len]))
ได้
|
สายพันธุ์ |
ส่วนสูง |
น้ำหนัก |
|
sum |
len |
sum |
len |
sum |
len |
ชนิด |
|
|
|
|
|
|
น้ำ |
เซนิงาเมะคาเมลคาเม็กซ์ |
3 |
3.1 |
3.0 |
132.6 |
3.0 |
พืช/พิษ |
ฟุชิงิดาเนะฟุชิงิโซวฟุชิงิบานะ |
3 |
4.1 |
3.0 |
175.4 |
3.0 |
ไฟ |
ฮิโตคาเงะลิซาร์โด |
2 |
1.7 |
2.0 |
27.5 |
2.0 |
ไฟ/บิน |
ลิซาร์ดอน |
1 |
1.7 |
1.0 |
101.5 |
1.0 |
จะเห็นว่าชื่อคอลัมน์ที่ซ้อนอยู่นั้นจะเป็นไปตามชื่อฟังก์ชัน
เพียงแต่ว่ากรณีที่ใช้ lambda เพื่อเป็นฟังก์ชัน ฟังก์ชันนั้นจะไม่มีชื่อแต่จะออกมาเป็น หากมีฟังก์ชันแบบนี้อยู่ ๒ อันก็จะเกิดการซ้ำซ้อนและเกิดข้อผิดพลาดขึ้น
แต่เราสามารถกำหนดชื่อคอลัมน์ได้ด้วยโดยการใส่ในรูปของลิสต์ของทูเพิล โดยสมาชิกในทูเพิลด้านในนั้นเป็นชื่อฟังก์ชันที่ต้องการตั้ง ตามด้วยฟังก์ชันที่ต้องการใช้
ตัวอย่าง
import numpy as np
f1 = lambda x:len('%s'%np.sum(x))
f2 = lambda x:'%s,%s'%(x.min(),x.max())
print(pokemon.groupby('ชนิด').agg([('ฟ1',f1),('ฟ2',f2)]))
ได้
|
สายพันธุ์ |
ส่วนสูง |
น้ำหนัก |
|
ฟ1 |
ฟ2 |
ฟ1 |
ฟ2 |
ฟ1 |
ฟ2 |
ชนิด |
|
|
|
|
|
|
น้ำ |
22 |
คาเมล,เซนิงาเมะ |
3.0 |
0.5,1.6 |
5.0 |
9.0,101.1 |
พืช/พิษ |
30 |
ฟุชิงิดาเนะ,ฟุชิงิโซว |
3.0 |
0.7,2.4 |
5.0 |
6.9,155.5 |
ไฟ |
17 |
ลิซาร์โด,ฮิโตคาเงะ |
18.0 |
0.6,1.1 |
4.0 |
8.5,19.0 |
ไฟ/บิน |
9 |
ลิซาร์ดอน,ลิซาร์ดอน |
3.0 |
1.7,1.7 |
5.0 |
101.5,101.5 |
และยิ่งไปกว่านั้น agg ยังสามารถให้ฟังก์ชันทำกับแต่ละคอลัมน์ไม่เหมือนกันได้ด้วย กำหนดโดยใส่ในรูปของดิกชันนารีของ ชื่อคอลัมน์: ฟังก์ชันที่จะใช้กับคอลัมน์นั้น
โดยจะใส่แค่เฉพาะคอลัมน์ที่ต้องการก็ได้ คอลัมน์ที่ไม่ใส่ก็จะไม่ได้แสดง
ตัวอย่าง นำคอลัมน์ "ส่วนสูง" มาหาค่าผลรวม ส่วนคอลัมน์ "สายพันธุ์" ให้หาความยาวรวมของชื่อสายพันธุ์ทุกตัวในกลุ่ม ส่วนคอลัมน์ "น้ำหนัก" ไม่ใช้ก็ไม่ต้องใส่
f = lambda x:len(np.sum(x))
print(pokemon.groupby('ชนิด').agg({'สายพันธุ์':f,'ส่วนสูง':'sum'}))
ได้
|
สายพันธุ์ |
ส่วนสูง |
ชนิด |
|
|
น้ำ |
22 |
3.1 |
พืช/พิษ |
30 |
4.1 |
ไฟ |
17 |
1.7 |
ไฟ/บิน |
9 |
1.7 |
และซับซ้อนยิ่งขึ้นไปอีกคืออาจใช้หลายฟังก์ชันทำต่อแต่ละคอลัมน์พร้อมกันก็ยังได้ โดยให้ใส่เป็นดิกชันนารีของ ชื่อคอลัมน์: ลิสต์ของฟังก์ชันที่จะใช้กับคอลัมน์นั้น
ตัวอย่าง
f1 = ['sum',('lensum',lambda x:len(np.sum(x)))]
f2 = ['sum','mean']
print(pokemon.groupby('ชนิด').agg({'สายพันธุ์':f1,'ส่วนสูง':f2}))
ได้
|
สายพันธุ์ |
ส่วนสูง |
|
sum |
lensum |
sum |
mean |
ชนิด |
|
|
|
|
น้ำ |
เซนิงาเมะคาเมลคาเม็กซ์ |
22 |
3.1 |
1.033333 |
พืช/พิษ |
ฟุชิงิดาเนะฟุชิงิโซวฟุชิงิบานะ |
30 |
4.1 |
1.366667 |
ไฟ |
ฮิโตคาเงะลิซาร์โด |
17 |
1.7 |
0.850000 |
ไฟ/บิน |
ลิซาร์ดอน |
9 |
1.7 |
1.700000 |
อ้างอิง