pandas ได้เตรียมคำสั่งที่สะดวกสำหรับใช้ในการแบ่งกลุ่มข้อมูลที่อยู่ภายในตารางเพื่อจะนำมารวบยอดหรือทำอะไรก็ตามแบบแยกเป็นกลุ่มๆ
การแยกกลุ่มของข้อมูลสามารถทำได้ด้วยเมธอด groupby ซึ่งมีอยู่ทั้งในซีรีส์และเดตาเฟรม มีหน้าที่แบ่งกลุ่มข้อมูลเพื่อนำมาทำอะไรต่ออีกที
การใช้ groupby นั้นค่อนข้างมีความซับซ้อนเข้าใจยากอยู่แต่หากเข้าใจและใช้เป็นแล้วจะเป็นประโยชน์ในการจัดการข้อมูลเป็นอย่างมาก
การแบ่งกลุ่มข้อมูลโดยแยกตามค่าในคอลัมน์ วิธีการแบ่งกลุ่มข้อมูลนั้นมีอยู่หลากหลายแบบ แต่ที่ง่ายที่สุดก็คือแบ่งโดยใช้ค่าในคอลัมน์หนึ่ง ซึ่งทำได้โดยใส่ชื่อคอลัมน์ที่ต้องการใช้เป็นตัวแบ่งลงไป
ตัวอย่างเช่น ลองดูตารางข้อมูลของโปเกมอน ๙ สายพันธุ์
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 |
แต่ละตัวมีชนิดเหมือนกันบ้างต่างกันบ้าง เราลองมาแบ่งกลุ่มตามชนิดดูด้วย groupby
print(pokemon.groupby('ชนิด'))
ได้
ผลที่ได้ออกมาจะเป็นออบเจ็กต์ชนิด pandas.core.groupby.DataFrameGroupBy อย่างที่เห็น ซึ่งถึงสั่ง print ก็ไม่แสดงผลลัพธ์อะไร
ต่อจากนี้ไปจะเรียกออบเจ็กชนิดนี้ว่า "ข้อมูลที่จัดกลุ่มแล้ว"
เราสามารถดูผลการจัดกลุ่มได้ด้วยการเติม .groups ต่อท้าย ผลที่ได้จะออกมาเป็นดิกชันนารีที่แสดงดัชนีของแถวที่อยู่ในแต่ละกลุ่ม
print(pokemon.groupby('ชนิด').groups)
ได้
{'ไฟ/บิน': [6], 'ไฟ': [4, 5], 'น้ำ': [7, 8, 9], 'พืช/พิษ': [1, 2, 3]}
เท่านี้ก็จะเห็นได้ว่าข้อมูลถูกแบ่งกลุ่มออกตามชนิดของโปเกมอน
หากแค่ต้องการจำนวนแถวในแต่ละกลุ่มก็ใช้เมธอด size
print(pokemon.groupby('ชนิด').size())
จะได้ซีรีส์ของจำนวน
ชนิด
น้ำ 3
พืช/พิษ 3
ไฟ 2
ไฟ/บิน 1
dtype: int64
และอาจสามารถดูเนื้อในได้ด้วยการแปลงเป็น list
print(list(pokemon.groupby('ชนิด')))
ได้
[('น้ำ', สายพันธุ์ ชนิด ส่วนสูง น้ำหนัก
7 เซนิงาเมะ น้ำ 0.5 9.0
8 คาเมล น้ำ 1.0 22.5
9 คาเม็กซ์ น้ำ 1.6 101.1),
('พืช/พิษ', สายพันธุ์ ชนิด ส่วนสูง น้ำหนัก
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)]
ใช้ข้อมูลที่จัดกลุ่มแล้วเป็นอิเทอเรเตอร์ เราสามารถนำข้อมูลที่จัดกลุ่มแล้วมาใช้กับ for ในฐานะอิเทอเรเตอร์ได้ โดยจะต้องมีค่ามารับ ๒ ตัว ซึ่งจะได้ค่าเป็นชื่อของกลุ่ม ตามด้วยเดตาเฟรมข้อมูลของกลุ่มนั้น
ตัวอย่าง
for chue,klum in pokemon.groupby('ชนิด'):
print('<<'+chue+'>>')
print(klum)
ได้
<<น้ำ>>
|
สายพันธุ์ |
ชนิด |
ส่วนสูง |
น้ำหนัก |
7 |
เซนิงาเมะ |
น้ำ |
0.5 |
9.0 |
8 |
คาเมล |
น้ำ |
1.0 |
22.5 |
9 |
คาเม็กซ์ |
น้ำ |
1.6 |
101.1 |
<<พืช/พิษ>>
|
สายพันธุ์ |
ชนิด |
ส่วนสูง |
น้ำหนัก |
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 |
จัดการข้อมูลที่ถูกจัดกลุ่มแล้วด้วยเมธอดต่างๆ ข้อมูลที่ถูกจัดกลุ่มแล้วนี้สามารถใช้เมธอดต่างๆที่ใช้สำหรับการรวบยอดข้อมูลได้
เช่น max
print(pokemon.groupby('ชนิด').max())
ได้
|
สายพันธุ์ |
ส่วนสูง |
น้ำหนัก |
ชนิด |
|
|
|
น้ำ |
เซนิงาเมะ |
1.6 |
101.1 |
พืช/พิษ |
ฟุชิงิโซว |
2.4 |
155.5 |
ไฟ |
ฮิโตคาเงะ |
1.1 |
19.0 |
ไฟ/บิน |
ลิซาร์ดอน |
1.7 |
101.5 |
เมื่อใช้ max แบบนี้จะเป็นการหาค่าสูงสุดในแต่ละคอลัมน์โดยแยกตามแต่ละกลุ่ม (ในที่นี้ "สายพันธุ์" ไม่ใช่ตัวเลขจึงเป็นการเรียงตามตัวหนังสือโดยเอาตัวที่อยู่ท้ายสุด)
sum ก็ทำนองเดียวกัน เป็นการคำนวณผลรวม แต่ sum จะแสดงเฉพาะคอลัมน์ที่เป็นข้อมูลตัวเลข เพราะตัวหนังสือไม่สามารถคำนวณได้
print(pokemon.groupby('ชนิด').sum())
ได้
|
ส่วนสูง |
น้ำหนัก |
ชนิด |
|
|
น้ำ |
3.1 |
132.6 |
พืช/พิษ |
4.1 |
175.4 |
ไฟ |
1.7 |
27.5 |
ไฟ/บิน |
1.7 |
101.5 |
แล้วก็ยังมี min, prod, mean, median, std, var, ฯลฯ เหมือนกับที่เขียนไว้ใน
บทที่ ๘ อนึ่ง ในนี้ยังมีเมธอด count ซึ่งมีไว้หาจำนวนสมาชิกที่ไม่เป็น NaN อาจดูเหมือนคล้ายกับ size แต่ count จะคืนค่าเป็นเดตาเฟรมซึ่งนับจำนวนข้อมูลที่ไม่ใช่ NaN ในแต่ละคอลัมน์ ซึ่งถ้าหากทุกคอลัมน์ไม่มี NaN อยู่ก็จะได้ค่าทุกคอลัมน์เท่ากันหมด
print(pokemon.groupby('ชนิด').count())
ได้
|
สายพันธุ์ |
ส่วนสูง |
น้ำหนัก |
ชนิด |
|
|
|
น้ำ |
3 |
3 |
3 |
พืช/พิษ |
3 |
3 |
3 |
ไฟ |
2 |
2 |
2 |
ไฟ/บิน |
1 |
1 |
1 |
ข้อมูลที่จัดกลุ่มแล้วสามารถตามด้วย [ชื่อคอลัมน์] เพื่อแยกเป็นซีรีส์ของแต่ละคอลัมน์ก่อนจึงค่อยใช้เมธอดก็ได้
print(pokemon.groupby('ชนิด')['น้ำหนัก'])
ได้
แบบนี้จะได้ออบเจ็กต์ประเภท pandas.core.groupby.SeriesGroupBy ซึ่งก็เรียกว่าเป็นข้อมูลที่แยกกลุ่มแล้วเหมือนกัน แต่ต่างจากก่อนเติม [] ตรงที่ว่าเป็น SeriesGroupBy ไม่ใช่ DataFrameGroupBy
แต่คุณสมบัติก็คล้ายๆกัน เอามาใช้เมธอดได้เช่นเดียวกัน เพื่อจัดการกับข้อมูลในคอลัมน์นั้น
เช่นหาค่าเฉลี่ยภายในกลุ่มต่างๆในคอลัมน์นั้น
print(pokemon.groupby('ชนิด')['น้ำหนัก'].mean())
ได้
ชนิด
น้ำ 44.200000
พืช/พิษ 58.466667
ไฟ 13.750000
ไฟ/บิน 101.500000
Name: น้ำหนัก, dtype: float64
การจัดกลุ่มโดยแบ่งตามต้องการ นอกจากจะจัดกลุ่มตามคอลัมน์แล้วที่จริงแล้วเราสามารถจัดกลุ่มโดยใช้เกณฑ์อะไรก็ได้อีกหลายอย่าง
ค่าที่ใส่ให้กับ groupby นั้นอาจใช้แถวของข้อมูลชุดหนึ่ง อาจเป็นลิสต์หรืออาเรย์หรือซีรีส์ก็ได้ซึ่งจะต้องมีจำนวนสมาชิกเท่ากับจำนวนแถวของข้อมูล แล้วข้อมูลก็จะถูกแบ่งตามค่าของชุดข้อมูลนั้น
ตัวอย่างเช่นหากต้องการแบ่งข้อมูลออกเป็น ๓ กลุ่ม ชื่อกลุ่ม 1,2 และ 3
xyz = [1,2,3,1,2,3,1,2,3]
# หรือ xyz = pd.Series([1,2,3,1,2,3,1,2,3],index=range(1,10))
print(pokemon.groupby(xyz).groups)
ได้
{1: [1, 4, 7], 2: [2, 5, 8], 3: [3, 6, 9]}
ข้อมูลจะถูกแบ่งกลุ่มตามลิสต์ที่ใส่ไป
แท้จริงแล้วตัวอย่างก่อนหน้าที่เขียนเป็น pokemon.groupby('ชนิด') นั้นมีค่าเท่ากับเขียนเป็น
pokemon.groupby(pokemon['ชนิด'])
นั่นคือแบ่งโดยใช้ซีรีส์ของชนิดโปเกมอน เพียงแต่ว่ากรณีที่ใช้คอลัมน์เป็นตัวแบ่งสามารถเขียนให้ง่ายขึ้นดังที่เห็นนี้
เราอาจจะประยุกต์เป็นแบ่งโดยใช้ฟังก์ชันเพื่อสร้างลิสต์หรือซีรีส์สำหรับคัดกรองแบ่งกลุ่มขึ้นมา
เช่นลองแบ่งกลุ่มตามอักษร ๔ ตัวแรกของชื่อสายพันธุ์
xxxx = pokemon['สายพันธุ์'].map(lambda x:x[:4])
print(pokemon.groupby(xxxx).groups)
for chue,klum in pokemon.groupby(xxxx):
print('%s: %s'%(chue,list(klum['สายพันธุ์'])))
ได้
{'เซนิ': [7], 'ฮิโต': [4], 'ฟุชิ': [1, 2, 3], 'คาเม': [8, 9], 'ลิซา': [5, 6]}
คาเม: ['คาเมล', 'คาเม็กซ์']
ฟุชิ: ['ฟุชิงิดาเนะ', 'ฟุชิงิโซว', 'ฟุชิงิบานะ']
ลิซา: ['ลิซาร์โด', 'ลิซาร์ดอน']
ฮิโต: ['ฮิโตคาเงะ']
เซนิ: ['เซนิงาเมะ']
การแบ่งกลุ่มด้วยฟังก์ชันที่ทำกับดัชนี นอกจากจะแบ่งกลุ่มด้วยชื่อคอลัมน์หรือแบ่งด้วยลิสต์หรือซีรีส์แล้ว หากต้องการจะแบ่งกลุ่มโดยใช้ฟังก์ชันบางอย่างทำกับดัชนีก็ทำได้โดยใส่ฟังก์ชันลงไปเป็นอาร์กิวเมน์ของ groupby ได้เลย
เช่นแบ่งตามดัชนีเลขคู่เลขคี่
def khukhi(x):
if(x%2): return 'คี่'
else: return 'คู่'
print(pokemon.groupby(khukhi).groups)
for chue,klum in pokemon.groupby(khukhi):
print('%s: %s'%(chue,list(klum['สายพันธุ์'])))
ได้
{'คี่': [1, 3, 5, 7, 9], 'คู่': [2, 4, 6, 8]}
คี่: ['ฟุชิงิดาเนะ', 'ฟุชิงิบานะ', 'ลิซาร์โด', 'เซนิงาเมะ', 'คาเม็กซ์']
คู่: ['ฟุชิงิโซว', 'ฮิโตคาเงะ', 'ลิซาร์ดอน', 'คาเมล']
ที่จริงแล้ว pokemon.groupby(khukhi) ก็มีค่าเท่ากับ
pokemon.groupby(pokemon.index.map(khukhi))
เพียงแต่ในกรณีที่จะใช้ฟังก์ชันทำกับดัชนีแล้วสามารถแค่ใส่ตัวฟังก์ชันลงไปเลยโดยตรงแบบนี้ได้เลยซึ่งจะดูง่ายกว่า
ในตัวอย่างก่อนหน้าที่แบ่งตามอักษร ๔ ตัวแรกของชื่อสายพันธุ์เราอาจเขียนใหม่ได้โดยตั้งให้สายพันธุ์เป็นดัชนี จากนั้นก็จะสามารถใส่ฟังก์ชันที่ทำกับชื่อสายพันธุ์ลงไปใน groupby ได้เลย
pokemon = pokemon.set_index('สายพันธุ์')
print(pokemon)
print(pokemon.groupby(lambda x:x[:4]).groups)
ได้
|
ชนิด |
ส่วนสูง |
น้ำหนัก |
สายพันธุ์ |
|
|
|
ฟุชิงิดาเนะ |
พืช/พิษ |
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 |
{'เซนิ': ['เซนิงาเมะ'], 'ฮิโต': ['ฮิโตคาเงะ'], 'ฟุชิ': ['ฟุชิงิดาเนะ', 'ฟุชิงิโซว', 'ฟุชิงิบานะ'], 'คาเม': ['คาเมล', 'คาเม็กซ์'], 'ลิซา': ['ลิซาร์โด', 'ลิซาร์ดอน']}
การแบ่งกลุ่มด้วยสองคอลัมน์ขึ้นไป สามารถแบ่งกลุ่มโดยแบ่งตามข้อมูลของมากกว่าหนึ่งคอลัมน์ได้โดยใส่เป็นลิสต์ของชื่อคอลัมน์
ตัวอย่าง
pokemon = pd.DataFrame([
['มาดัตสึโบมิ','พืช','พิษ',0.7,4.0],
['อุตสึดง','พืช','พิษ',1.0,6.4],
['อุตสึบ็อต','พืช','พิษ',1.7,15.5],
['เมโนคุราเงะ','น้ำ','พิษ',0.9,45.5],
['โดคุคุราเงะ','น้ำ','พิษ',1.6,55.0],
['ยาดอน','น้ำ','พลังจิต',1.2,36.0],
['ยาโดรัน','น้ำ','พลังจิต',1.6,78.5],
['ทามะทามะ','พืช','พลังจิต',0.4,2.5],
['นัสซี','พืช','พลังจิต',2.0,120.0]],
columns=['สายพันธุ์','ชนิด1','ชนิด2','ส่วนสูง','น้ำหนัก'],
index=[69,70,71,72,73,79,80,102,103])
print(pokemon)
ได้
|
สายพันธุ์ |
ชนิด1 |
ชนิด2 |
ส่วนสูง |
น้ำหนัก |
69 |
มาดัตสึโบมิ |
พืช |
พิษ |
0.7 |
4.0 |
70 |
อุตสึดง |
พืช |
พิษ |
1.0 |
6.4 |
71 |
อุตสึบ็อต |
พืช |
พิษ |
1.7 |
15.5 |
72 |
เมโนคุราเงะ |
น้ำ |
พิษ |
0.9 |
45.5 |
73 |
โดคุคุราเงะ |
น้ำ |
พิษ |
1.6 |
55.0 |
79 |
ยาดอน |
น้ำ |
พลังจิต |
1.2 |
36.0 |
80 |
ยาโดรัน |
น้ำ |
พลังจิต |
1.6 |
78.5 |
102 |
ทามะทามะ |
พืช |
พลังจิต |
0.4 |
2.5 |
103 |
นัสซี |
พืช |
พลังจิต |
2.0 |
120.0 |
โปเกมอนแต่ละตัวในข้อมูลนี้มี "ชนิด1" และ "ชนิด2" ต่างกัน หากลองใช้เป็นคีย์แบ่งดู
print(pokemon.groupby(['ชนิด1','ชนิด2']).groups)
print(pokemon.groupby(['ชนิด1','ชนิด2']).size())
ได้
{('น้ำ', 'พิษ'): [72, 73], ('พืช', 'พิษ'): [69, 70, 71], ('น้ำ', 'พลังจิต'): [79, 80], ('พืช', 'พลังจิต'): [102, 103]}
ชนิด1 ชนิด2
น้ำ พลังจิต 2
พิษ 2
พืช พลังจิต 2
พิษ 3
dtype: int64
ผลที่ได้จะอยู่ในรูปของดัชนีซ้อน
เมื่อนำมาใช้กับ for ส่วนของชื่อก็จะอยู่ในรูปของลิสต์อีกทีด้วย
for chue,klum in pokemon.groupby(['ชนิด1','ชนิด2']):
print('<< '+chue[0]+' + '+chue[1]+' >>')
print(klum)
ได้
<< น้ำ + พลังจิต >>
|
สายพันธุ์ |
ชนิด1 |
ชนิด2 |
ส่วนสูง |
น้ำหนัก |
79 |
ยาดอน |
น้ำ |
พลังจิต |
1.2 |
36.0 |
80 |
ยาโดรัน |
น้ำ |
พลังจิต |
1.6 |
78.5 |
<< น้ำ + พิษ >>
|
สายพันธุ์ |
ชนิด1 |
ชนิด2 |
ส่วนสูง |
น้ำหนัก |
72 |
เมโนคุราเงะ |
น้ำ |
พิษ |
0.9 |
45.5 |
73 |
โดคุคุราเงะ |
น้ำ |
พิษ |
1.6 |
55.0 |
<< พืช + พลังจิต >>
|
สายพันธุ์ |
ชนิด1 |
ชนิด2 |
ส่วนสูง |
น้ำหนัก |
102 |
ทามะทามะ |
พืช |
พลังจิต |
0.4 |
2.5 |
103 |
นัสซี |
พืช |
พลังจิต |
2.0 |
120.0 |
<< พืช + พิษ >>
|
สายพันธุ์ |
ชนิด1 |
ชนิด2 |
ส่วนสูง |
น้ำหนัก |
69 |
มาดัตสึโบมิ |
พืช |
พิษ |
0.7 |
4.0 |
70 |
อุตสึดง |
พืช |
พิษ |
1.0 |
6.4 |
71 |
อุตสึบ็อต |
พืช |
พิษ |
1.7 |
15.5 |
อ้างอิง