ในภาษาไพธอนปกติเวลามีคำสั่งอะไรบางอย่างที่ต้องการทำซ้ำๆเดิมกับออบเจ็กต์กลุ่มหนึ่งๆที่อยู่ภายในลิสต์ก็มักจะใช้ for
แต่ในบางกรณีก็มีฟังก์ชันที่สะดวกกว่านั้นเมื่อแค่ต้องการเปลี่ยนค่าในลิสต์หนึ่งๆแบบพร้อมกันทีเดียวแล้วคืนลิสต์ใหม่กลับมา ฟังก์ชันที่ใช้ในกรณีนี้คือ map
ใน pandas เองก็มีเมธอดที่ใช้กับซีรีส์และเดตาเฟรมซึ่งมีความสามารถทำนองเดียวกับฟังก์ชัน map ในไพธอนมาตรฐาน
ก่อนที่จะเข้าใจเนื้อหาของบทนี้ได้อาจต้องเข้าใจเรื่อง map และ lambda ที่เขียนไว้ในเนื้อหา
ไพธอนพื้นฐานบทที่ ๒๑ การใช้ map ในซีรีส์ ซีรีส์มีเมธอด map ซึ่งเอาไว้ทำอะไรบางอย่างเหมือนๆกันกับสมาชิกทุกตัวในซีรีส์แล้วคืนค่ากลับออกมาเป็นซีรีส์ใหม่ที่มีจำนวนสมาชิกเท่าเดิม
ตัวอย่าง ลองสร้างซีรีส์ที่เก็บชื่อของโปเกมอนขึ้นมาแล้วสร้างฟังก์ชันสำหรับเอาแค่อักษรสองตัวแรกมาเติมคำต่อท้าย
import pandas as pd
def chan(p):
return p[0:2]+'จัง'
pokemon = pd.Series(['ฟุชิงิดาเนะ','ฮิโตคาเงะ','เซนิงาเมะ','คาเตอร์ปี','บีเดิล'])
# 0 ฟุชิงิดาเนะ
# 1 ฮิโตคาเงะ
# 2 เซนิงาเมะ
# 3 คาเตอร์ปี
# 4 บีเดิล
print(pokemon.map(chan))
ได้
0 ฟุจัง
1 ฮิจัง
2 เซจัง
3 คาจัง
4 บีจัง
dtype: object
สามารถเขียนสั้นๆโดยใช้ lambda เพื่อให้ไม่ต้องใช้ def เพื่อสร้างฟังก์ชันขึ้นมาก่อนได้
pokemon.map(lambda x:x[0:2]+'จัง')
การใช้ lambda ค่อนข้างสะดวกกะทัดรัดกว่ามาก ดังนั้นส่วนใหญ่จะใช้แบบนี้มากกว่า หากฟังก์ชันที่ใช้นั้นไม่ได้ซับซ้อนอะไรมาก
ที่จริงจะใช้ฟังก์ชัน map แล้วค่อยนำผลที่ได้มาสร้างซีรีส์ใหม่แบบนี้ก็ได้
pd.Series(map(lambda x:x[0:2]+'จัง',pokemon))
แต่ยังไงก็ไม่สะดวกเท่าใช้เมธอด map ที่มีอยู่ในตัวซีรีส์อยู่แล้ว
ส่วนของดัชนีเองก็สามารถใช้เมธอด map ได้เช่นกัน เช่นลองเปลี่ยนค่าดัชนีของซีรีส์เดิมในตัวอย่างที่แล้วที่เดิมเป็น 0 ถึง 4
pokemon.index = pokemon.index.map(lambda x: x*3+1)
print(pokemon)
ได้
1 ฟุชิงิดาเนะ
4 ฮิโตคาเงะ
7 เซนิงาเมะ
10 คาเตอร์ปี
13 บีเดิล
dtype: object
เมธอด map นี้นอกจากจะใช้กับฟังก์ชันแล้วยังใช้กับดิกชันนารีได้ด้วย โดยในกรณีนี้จะเป็นการเปลี่ยนค่าจากที่มีอยู่ในคีย์ให้เป็นค่าในดักชันนารี
p = {151:'มิว',152:'ชิโครีตา',153:'เบย์ลีฟ',154:'เมกาเนียม',155:'ฮิโนอาราชิ',
156:'แม็กมาราชิ',157:'บักฟูน',158:'วานิโนโกะ',159:'อาลิเกตซ์',160:'ออร์ไดล์'}
pokemon = pd.Series([152,155,158,161])
print(pokemon.map(p))
ได้
0 ชิโครีตา
1 ฮิโนอาราชิ
2 วานิโนโกะ
3 NaN
dtype: object
จะเห็นว่าถ้าหากค่าในซีรีส์ไม่มีอยู่ในคีย์ของดิกชันนารีก็จะคืนค่า NaN
นอกจากจะใช้ดิกชันนารีแล้วจะใช้เป็นซีรีส์ก็ได้ เช่นในที่นี้ดิก p อาจเขียนใหม่เป็นซีรีส์ เป็น
p = pd.Series(['มิว','ชิโครีตา','เบย์ลีฟ','เมกาเนียม','ฮิโนอาราชิ',
'แม็กมาราชิ','บักฟูน','วานิโนโกะ','อาลิเกตซ์','ออร์ไดล์'],
index=range(151,161))
การใช้ applymap ในเดตาเฟรม สำหรับเดตาเฟรมจะไม่มีเมธอดที่ชื่อว่า map แต่จะมีเมธอดชื่อ applymap ซึ่งมีลักษณะการทำงายใกล้เคียงกัน
โดย applymap จะเป็นการใช้ฟังก์ชันทำกับทุกสมาชิกในนั้นเหมือนกันหมดไม่ว่าจะอยู่แถวไหนคอลัมน์ไหนก็ตาม
เพียงแต่ applymap จะใช้ได้แค่ฟังก์ชันเท่านั้น ใช้ดิกชันนารีหรือซีรีส์ไม่ได้
ตัวอย่าง เปลี่ยนข้อมูลน้ำหนักส่วนสูงของโปเกมอนให้เป็นสายอักขระที่แสดงผลทศนิยม ๔ ตัว
pokemon = pd.DataFrame(
[[0.6,7.8],[0.8,13.3],[1.4,61.5]],
index=['เมรีป','โมโกโกะ','เดนริว'],
columns=['ส่วนสูง','น้ำหนัก'],
dtype='float32')
print(pokemon.applymap(lambda x:'%.4f'%x))
ได้
|
ส่วนสูง |
น้ำหนัก |
เมรีป |
0.6000 |
7.8000 |
โมโกโกะ |
0.8000 |
13.3000 |
เดนริว |
1.4000 |
61.5000 |
applymap จะทำกับสมาชิกในทุกแถวทุกคอลัมน์ แต่หากต้องการใช้แค่แถวใดแถวหนึ่งหรือคอลัมน์ใดคอลัมน์หนึ่งก็ให้เข้าถึงแถวหรือคอลัมน์นั้นแล้วใช้ map เอา
การใช้เมธอด apply ในเดตาเฟรม ในขณะที่ applymap จะทำกับสมาชิกทั้งหมดในเดตาเฟรมอย่างเท่าเทียมกันทั้งหมด แต่หากต้องการทำอะไรบางอย่างกับข้อมูลพร้อมกันทั้งแถวในคอลัมน์หนึ่ง หรือทั้งคอลัมน์ในแถวหนึ่ง กรณีแบบนั้นจะใช้เมธอด apply
เมธอด apply นั้นที่จริงแล้วสามารถใช้กับซีรีส์ได้ด้วย ซึ่งผลที่ได้จะเหมือนกับใช้ map เพียงแต่จะไม่สามารถใช้ดิกชันนารีหรือซีรีส์ ใช้ได้แค่ฟังก์ชันเท่านั้น
คีย์เวิร์ด axis จะเป็นตัวกำหนดว่าจะทำกับทั้งแถวในแต่ละคอลัมน์ หรือกับทั้งคอลัมน์ในแต่ละแถว
หากไม่ใส่คีย์เวิร์ด axis เลยหรือใส่ axis=0 จะเป็นการทำทั้งแถวในแต่ละคอลัมน์ แต่ถ้าใส่ axis=1 จะเป็นการทำทั้งคอลัมน์ในแต่ละแถว
กรณี axis=0 (หรือไม่ใส่) จะแยกแต่ละคอลัมน์ออกมาเป็นคอลัมน์ละซีรีส์ ฟังก์ชันที่ใส่ใน apply จะรับซีรีส์นั้นไปเป็นอาร์กิวเมนต์เพื่อทำอะไรๆต่อ ผลที่ได้จะออกมาในรูปของซีรีส์ของผลลัพธ์ที่ได้จากแต่ละคอลัมน์
ตัวอย่าง สร้างเดตาเฟรมเก็บค่าน้ำหนักส่วนสูงของโปเกมอน
pokemon = pd.DataFrame(
[[1.9,178.0],[2.1,198.0],[2.0,187.0],[5.2,216.0],[3.8,199.0]],
columns=['น้ำหนัก','ส่วนสูง'],
index=['ไรโคว','เอนเทย์','ซุยคูน','ลูเกีย','โฮวโอว'])
print(pokemon)
ได้
|
น้ำหนัก |
ส่วนสูง |
ไรโคว |
1.9 |
178.0 |
เอนเทย์ |
2.1 |
198.0 |
ซุยคูน |
2.0 |
187.0 |
ลูเกีย |
5.2 |
216.0 |
โฮวโอว |
3.8 |
199.0 |
แล้วทำการหาค่าเฉลี่ยและผลรวม
print(pokemon.apply(lambda x:'รวม = '+str(x.sum())+' เฉลี่ย = '+str(x.mean())))
ได้
น้ำหนัก รวม = 15.0 เฉลี่ย = 3.0
ส่วนสูง รวม = 978.0 เฉลี่ย = 195.6
dtype: object
กรณี axis=1 จะแยกแต่ละแถวออกมาเป็นแถวละซีรีส์ ผลที่ได้ก็จะออกมาในรูปของซีรีส์ของผลลัพธ์ในแต่ละแถว
ตัวอย่าง หาค่าน้ำหนักต่อส่วนสูงของโปเกมอนแต่ละตัว
print(pokemon.apply(lambda x:str(x['น้ำหนัก']/x['ส่วนสูง'])+' kg/m',axis=1))
ได้
ไรโคว 0.0106741573034 kg/m
เอนเทย์ 0.0106060606061 kg/m
ซุยคูน 0.0106951871658 kg/m
ลูเกีย 0.0240740740741 kg/m
โฮวโอว 0.0190954773869 kg/m
dtype: object
กรณีที่ฟังก์ชันที่ใช้นั้นไม่ได้ทำการลดรูปของซีรีส์ลง คือทำแล้วก็ยังคืนผลออกมาเป็นซีรีส์ขนาดเท่าเดิม ผลที่ได้ก็จะยังคงเป็นเดตาเฟรม ไม่ได้ลดรูปลงเป็นซีรีส์
ตัวอย่าง เอาซีรีส์ที่ได้จากแต่ละคอลัมน์มาคูณ 1000 แล้วเปลี่ยนชนิดข้อมูลเป็น int
print(pokemon.apply(lambda x:(x*1000).astype(int)))
ได้
|
น้ำหนัก |
ส่วนสูง |
ไรโคว |
1900 |
178000 |
เอนเทย์ |
2100 |
198000 |
ซุยคูน |
2000 |
187000 |
ลูเกีย |
5200 |
216000 |
โฮวโอว |
3800 |
199000 |
หากใช้ apply แล้ว map ด้านในก็จะมีค่าเท่ากับการใช้เมธอด applymap และนั่นก็เป็นที่มาของชื่อ applymap ด้วย
ตัวอย่าง เปลี่ยนค่าในตารางให้กลายเป็นสายอักขระที่แสดงค่าด้วยเลขทศนิยมถึง ๓ ตำแหน่ง
print(pokemon.apply(lambda x:x.map(lambda x:'%.3f'%x)))
print(pokemon.applymap(lambda x:'%.3f'%x))
สองอันนี้ได้ผลเหมือนกันเป็น
|
น้ำหนัก |
ส่วนสูง |
ไรโคว |
1.900 |
178.000 |
เอนเทย์ |
2.100 |
198.000 |
ซุยคูน |
2.000 |
187.000 |
ลูเกีย |
5.200 |
216.000 |
โฮวโอว |
3.800 |
199.000 |
ปกติแต่ละคอลัมน์หรือแต่ละแถวที่แยกออกมานั้นจะอยู่ในรูปของซีรีส์ แต่เราสามารถทำให้อยู่ในรูปของอาเรย์ก็ได้โดยใส่คีย์เวิร์ด raw=1 การทำแบบนี้จะทำให้สูญเสียคุณสมบัติของซีรีส์ แต่หากต้องการแค่สิ่งที่อาเรย์สามารถทำได้การทำแบบนี้จะเร็วกว่า
ตัวอย่างเปรียบเทียบความแตกต่าง
print(pokemon.apply(type))
print('------------------------')
print(pokemon.apply(type,raw=1))
ได้
น้ำหนัก <class 'pandas.core.series.Series'>
ส่วนสูง <class 'pandas.core.series.Series'>
dtype: object
------------------------
น้ำหนัก <class 'numpy.ndarray'>
ส่วนสูง <class 'numpy.ndarray'>
dtype: object
การประยุกต์ใช้เพื่อคัดกรองข้อมูล หากใช้ map หรือ apply ด้วยฟังก์ชันที่คืนค่าเป็นบูลก็จะได้ผลเป็นซีรีส์ของบูล จากนั้นนำไปใช้เป็นดัชนีเพื่อคัดกรองข้อมูลได้
ตัวอย่าง คัดกรองโปเกมอนที่เป็นชนิดพิษ
pokemon = pd.DataFrame(
[['ซูแบ็ต','พิษ,บิน'],
['นาโซโนะคุสะ','พืช,พิษ'],
['พาราส','แมลง,พืช'],
['คงปัง','แมลง,พิษ'],
['ดิกดา','ดิน']],
columns=['สายพันธุ์','ชนิด'],
index=[41,43,46,48,50])
print(pokemon)
print(pokemon['ชนิด'].map(lambda x: 'พิษ' in x))
ได้
|
สายพันธุ์ |
ชนิด |
41 |
ซูแบ็ต |
พิษ,บิน |
43 |
นาโซโนะคุสะ |
พืช,พิษ |
46 |
พาราส |
แมลง,พืช |
48 |
คงปัง |
แมลง,พิษ |
50 |
ดิกดา |
ดิน |
41 True
43 True
46 False
48 True
50 False
Name: ชนิด, dtype: bool
นำมาใช้คัดกรอง
print(pokemon[pokemon['ชนิด'].map(lambda x: 'พิษ' in x)])
ได้
|
สายพันธุ์ |
ชนิด |
41 |
ซูแบ็ต |
พิษ,บิน |
43 |
นาโซโนะคุสะ |
พืช,พิษ |
48 |
คงปัง |
แมลง,พิษ |
หากใช้ apply(axis=1) จะสามารถใช้ข้อมูลจากสองคอลัมน์ขึ้นไปมาเป็นเงื่อนไขในการคัดกรองได้
ตัวอย่างสร้างตารางข้อมูลโปเกมอนขึ้นมา
pokemon = pd.DataFrame(
[['ฮาเน็กโกะ',0.4,0.5],
['โปโปกโกะ',0.6,1.0],
['วาตักโกะ',0.8,3.0],
['เอย์ปาม',0.8,11.5],
['ฮิมานัตส์',0.3,1.8],
['คิมาวาริ',0.8,8.5]],
columns=['สายพันธุ์','ส่วนสูง','น้ำหนัก'],
index=[187,188,189,190,191,192])
print(pokemon)
ได้
|
สายพันธุ์ |
ส่วนสูง |
น้ำหนัก |
187 |
ฮาเน็กโกะ |
0.4 |
0.5 |
188 |
โปโปกโกะ |
0.6 |
1.0 |
189 |
วาตักโกะ |
0.8 |
3.0 |
190 |
เอย์ปาม |
0.8 |
11.5 |
191 |
ฮิมานัตส์ |
0.3 |
1.8 |
192 |
คิมาวาริ |
0.8 |
8.5 |
แล้วคัดกรองโปเกมอนที่ค่าน้ำหนัก (กก.) มากกว่าส่วนสูง (ม.) ๑๐ เท่า
print(pokemon.apply(lambda x:x['น้ำหนัก']/x['ส่วนสูง'],axis=1))
print(pokemon[pokemon.apply(lambda x:x['น้ำหนัก']/x['ส่วนสูง']>10,axis=1)])
ได้
187 1.250000
188 1.666667
189 3.750000
190 14.375000
191 6.000000
192 10.625000
dtype: float64
|
สายพันธุ์ |
ส่วนสูง |
น้ำหนัก |
190 |
เอย์ปาม |
0.8 |
11.5 |
192 |
คิมาวาริ |
0.8 |
8.5 |
อ้างอิง