ข้อมูลว่าง หรือสัญลักษณ์ที่แทนการไม่มีข้อมูลใน numpy และ pandas จะเรียกว่า NaN ซึ่งเทียบเท่ากับ None ในไพธอนมาตรฐาน แต่ไม่ได้เหมือนกันเสียทีเดียว
NaN มักจะปรากฏขึ้นมาในกรณีต่างๆที่ข้อมูลมีความผิดพลาดหรือขาดหาย และมีความพิเศษกว่าข้อมูลตัวอื่น มีเมธอดหลายตัวที่ใช้จัดการเกี่ยวกับ NaN โดยเฉพาะ ซึ่งจะพูดถึงในบทนี้
การเกิด NaN เวลาที่สร้างซีรีส์หรือเดตาเฟรมขึ้นมา ถ้าหากมีข้อมูลส่วนไหนที่ขาดหายไปก็ให้ใส่ None, np.NaN หรือ np.nan ลงไป จะกลายเป็น NaN ไปทันที
ตัวอย่าง ท่าของฟุชิงิดาเนะ
ที่เรียนรู้ได้ในเลเวลต่างๆ ๔ ท่าแรก ซึ่งในจำนวนนั้นบางท่าไม่ใช่ท่าโจมตีจึงไม่มีพลังโจมตีดังนั้นช่องพลังโจมตีก็เลยปล่อยว่าง
import numpy as np
import pandas as pd
tha = pd.DataFrame([
['ปะทะตัว','ธรรมดา',35,1],
['เสียงร้อง','ธรรมดา',None,1],
['เมล็ดยาโดริงิ','ธรรมดา',np.NaN,7],
['แส้เถาวัลย์','พืช',35,13]],
columns=['ชื่อ','ชนิด','พลังโจมตี','เลเวลที่ได้'])
print(tha)
ได้
|
ชื่อ |
ชนิด |
พลังโจมตี |
เลเวลที่ได้ |
0 |
ปะทะตัว |
ธรรมดา |
35.0 |
1 |
1 |
เสียงร้อง |
ธรรมดา |
NaN |
1 |
2 |
เมล็ดยาโดริงิ |
ธรรมดา |
NaN |
7 |
3 |
แส้เถาวัลย์ |
พืช |
35.0 |
13 |
จะเห็นว่าข้อมูลตรงที่ใส่ None หรือ np.NaN ไปผลที่ออกมาก็ได้เป็น NaN
และภายในคอลัมน์ "พลังโจมตี" ค่าภายในช่องที่ไม่ใช่ NaN ถูกเปลี่ยนเป็นจำนวนจริงที่มีทศนิยมทั้งๆที่ตอนแรกใส่เป็นจำนวนเต็มไป
ที่เป็นแบบนี้เพราะคอลัมน์ที่มี NaN ปนอยู่จะเป็นจำนวนเต็มไม่ได้ เลยถูกเปลี่ยนเป็นจำนวนจริง
หรือในกรณีที่ดึงข้อมูลจากไฟล์เมื่อเจอบางคำเช่นคำว่า NA หรือ nan ก็จะกลายเป็น NaN รายละเอียดตรงนี้ให้อ่านใน
บทที่ ๓ ซึ่งเขียนถึงไปแล้ว
นอกจากนี้ยังมีกรณีอย่างเช่นใช้ .loc แล้วใส่ดัชนีเป็นชื่อแถวหรือชื่อคอลัมน์ที่ไม่มีตัวตนอยู่ แบบนั้นก็จะได้ NaN ตลอดแถว ดังที่กล่าวไว้ใน
บทที่ ๔ และอื่นๆอีกมากมาย
การกำจัดข้อมูลที่มี NaN ในการคัดกรองข้อมูลทั่วไปเมื่อต้องการกำจัดตัวที่ไม่ต้องการเราอาจใช้วิธีการแบบที่ใช้ในบทที่ ๔ โดยตั้งเงื่อนไขเป็น
เดตาเฟรม[เดตาเฟรม[ชื่อคอลัมน์]!=ตัวที่ไม่ต้องการ]
แต่สำหรับ NaN แล้วเราจะเขียนเป็น
เดตาเฟรม[เดตาเฟรม[ชื่อคอลัมน์]!=np.NaN]
แบบนี้ไม่ได้
ลองดูตัวอย่าง สร้างตารางรายชื่อท่าของเซนิงาเมะ
ที่เรียนรู้ได้ในเวลต่างๆ ๔ ท่าแรก จากนั้นลองทำการกำจัด NaN
tha = pd.DataFrame([
['ปะทะตัว','ธรรมดา',35,1],
['สั่นหาง','ธรรมดา',None,1],
['ฟองน้ำ','น้ำ',20,8],
['กระสุนน้ำ','น้ำ',40,15]],
columns=['ชื่อ','ชนิด','พลังโจมตี','เลเวลที่ได้'],
index=[1,2,3,4])
print(tha[tha['พลังโจมตี']!=np.nan])
ได้
|
ชื่อ |
ชนิด |
พลังโจมตี |
เลเวลที่ได้ |
1 |
ปะทะตัว |
ธรรมดา |
35.0 |
1 |
2 |
สั่นหาง |
ธรรมดา |
NaN |
1 |
3 |
ฟองน้ำ |
น้ำ |
20.0 |
8 |
4 |
กระสุนน้ำ |
น้ำ |
40.0 |
15 |
จะเห็นว่าแถวที่มี NaN ก็ยังอยู่ไม่ได้หายไปไหน
กรณีนี้ต้องใช้เมธอดชื่อ notnull แทนเพื่อตรวจสอบว่าตัวไหนไม่เป็น NaN ถ้าเป็น NaN จะได้ False ถ้าไม่เป็นจะได้ True
print(tha['พลังโจมตี'].notnull())
print('---------------------------')
print(tha[tha['พลังโจมตี'].notnull()])
ได้
1 True
2 False
3 True
4 True
Name: พลังโจมตี, dtype: bool
---------------------------
|
ชื่อ |
ชนิด |
พลังโจมตี |
เลเวลที่ได้ |
1 |
ปะทะตัว |
ธรรมดา |
35.0 |
1 |
3 |
ฟองน้ำ |
น้ำ |
20.0 |
8 |
4 |
กระสุนน้ำ |
น้ำ |
40.0 |
15 |
ในทางตรงข้าม มีเมธอด isnull ที่จะได้ True เมื่อเป็น NaN และ False เมื่อไม่ใช่
print(tha[tha['พลังโจมตี'].isnull()])
ได้
|
ชื่อ |
ชนิด |
พลังโจมตี |
เลเวลที่ได้ |
2 |
สั่นหาง |
ธรรมดา |
NaN |
1 |
ในกรณีที่ต้องการลบข้อมูลทั้งแถวออกเมื่อมีคอลัมน์ใดคอลัมน์หนึ่งเป็น NaN จะมีวิธีที่สะดวกกว่านั้น คือมีเมธอดที่ใช้โดยเฉพาะ นั่นคือ dropna
เมธอด dropna จะทำการลบแถวที่มี NaN อยู่ เช่น print(tha[tha['พลังโจมตี'].notnull()]) ในตัวอย่างข้างต้นอาจเขียนเป็น
print(tha.dropna())
แบบนี้ผลที่ได้ก็จะเหมือนกัน
แต่ dropna มีการใช้ที่ยืดหยุ่นกว่านั้นโดยอาจใส่คีย์เวิร์ดเพิ่มเติมปรับเปลี่ยนผลลัพธ์ให้เป็นตามที่ต้องการ
เช่นแทนที่จะลบแถวที่มี NaN อยู่ในคอลัมน์ใดคอลัมน์หนึ่ง ก็อาจเปลี่ยนเป็นลบคอลัมน์ที่มี NaN อยู่ที่แถวใดแถวหนึ่ง ซึ่งทำได้โดยเติมคีย์เวิร์ด axis เข้าไปเป็น axis=1 ความหมายก็คือพิจารณาเป็นคอลัมน์ๆไป
ตัวอย่าง เดตาเฟรมอันเดิมถ้าต้องการลบคอลัมน์ที่มี NaN ซึ่งในที่นี้คือ "พลังโจมตี" จะเขียนได้ว่า
print(tha.dropna(axis=1))
ได้
|
ชื่อ |
ชนิด |
เลเวลที่ได้ |
1 |
ปะทะตัว |
ธรรมดา |
1 |
2 |
สั่นหาง |
ธรรมดา |
1 |
3 |
ฟองน้ำ |
น้ำ |
8 |
4 |
กระสุนน้ำ |
น้ำ |
15 |
อนึ่ง ปกติเมธอดนี้จะแค่คืนเดตาเฟรมใหม่ที่ลบ NaN ออกแล้ว แต่ตัวเดตาเฟรมเก่าก็ยังอยู่ ต้องเอาตัวแปรมารับค่าที่ได้ใหม่ ถ้าจะเขียนทับก็ใช้ตัวแปรตัวเดิม
แต่ว่านอกจากนั้นแล้วเราอาจใส่คีย์เวิร์ด inplace=True ลงไปเพื่อทำให้เมธอดนี้เป็นการเปลี่ยนแปลงแทนที่เดตาเฟรมตัวเก่าด้วยตัวที่ลบ NaN ออกไปแล้วทันที
กล่าวคือเขียนเป็น
tha.dropna(inplace=True)
แบบนี้จะมีค่าเท่ากับเขียน
tha = tha.dropna()
ปกติแล้ว dropna จะลบแถวที่มีตัวใดตัวหนึ่งเป็น NaN แต่หากต้องการเจาะจงว่ามี NaN ที่แถวไหนถึงจะลบก็ทำได้โดยเพิ่มคีย์เวิร์ด subset แล้วใส่ลิสต์ของชื่อคอลัมน์ที่ต้องการพิจารณา
ตัวอย่าง รายชื่อท่าชนิดไฟฟ้าท่าต่างๆของโปเกมอน บางท่าไม่ใช่ท่าโจมตี พลังโจมตีจึงเป็น NaN และบางท่าไม่มีวาซะแมชชีนก็เป็น NaN
tha = pd.DataFrame([
[40,100,np.NaN],
[np.NaN,100,'45'],
[95,100,'24'],
[120,70,'25']],
columns=['พลังโจมตี','ความแม่นยำ','วาซะแมชชีน'],
index=['ช็อตไฟฟ้า','คลื่นไฟฟ้า','ไฟฟ้าแสนโวลต์','ฟ้าผ่า'])
print(tha)
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
ช็อตไฟฟ้า |
40.0 |
100 |
NaN |
คลื่นไฟฟ้า |
NaN |
100 |
45 |
ไฟฟ้าแสนโวลต์ |
95.0 |
100 |
24 |
ฟ้าผ่า |
120.0 |
70 |
25 |
ลองใช้ dropna
print(tha.dropna())
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
ไฟฟ้าแสนโวลต์ |
95.0 |
100 |
24 |
ฟ้าผ่า |
120.0 |
70 |
25 |
แต่ถ้าจะให้ลบแค่เมื่อพลังโจมตีเป็น NaN ก็ทำได้โดย
print(tha.dropna(subset=['พลังโจมตี']))
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
ช็อตไฟฟ้า |
40.0 |
100 |
NaN |
ไฟฟ้าแสนโวลต์ |
95.0 |
100 |
24 |
ฟ้าผ่า |
120.0 |
70 |
25 |
หรือถ้าใช้คู่กับ axis=1 ก็ให้ระบุชื่อแถวที่ต้องการพิจารณา
print(tha.dropna(axis=1,subset=['ช็อตไฟฟ้า','ฟ้าผ่า']))
ได้
|
พลังโจมตี |
ความแม่นยำ |
ช็อตไฟฟ้า |
40.0 |
100 |
คลื่นไฟฟ้า |
NaN |
100 |
ไฟฟ้าแสนโวลต์ |
95.0 |
100 |
ฟ้าผ่า |
120.0 |
70 |
นอกจากนี้ยังมีคีย์เวิร์ด how สำหรับระบุว่าจะให้ลบทิ้งเมื่อเป็น NaN แค่ตัวเดียวหรือเมื่อเป็น NaN ทั้งหมด
โดยปกติถ้าไม่ระบุจะเป็น how='any' คือมี NaN แค่ตัวเดียวก็ลบทิ้งแล้ว แต่ถ้าใส่ how='all' จะลบทิ้งต่อเมื่อข้อมูลของทั้งแถวเป็น NaN หมดเลยเท่านั้น
การเติมเต็มส่วนที่เป็น NaN ในบางครั้งเราอาจไม่ได้อยากจะลบข้อมูลที่ขาดหายทิ้งไปเลยแต่อาจจะอยากเติมด้วยค่าอะไรบางอย่าง กรณีแบบนั้นให้ใช้ fillna
เมื่อใช้ fillna จะต้องใส่ค่าที่จะเติมให้ข้อมูลที่ขาดไปด้วย เช่นถ้าจะเติมด้วย 0 ก็ใส่ fillna(0)
ตัวอย่าง ท่าชนิดพืชของโปเกมอน
tha = pd.DataFrame([
[55,95,np.NaN],
[np.NaN,100,np.NaN],
[40,100,'21'],
[120,100,'22']],
columns=['พลังโจมตี','ความแม่นยำ','วาซะแมชชีน'],
index=['คัตเตอร์ใบไม้','สปอร์เห็ด','เมกาเดรน','โซลาร์บีม'])
print(tha)
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
คัตเตอร์ใบไม้ |
55.0 |
95 |
NaN |
สปอร์เห็ด |
NaN |
100 |
NaN |
เมกาเดรน |
40.0 |
100 |
21 |
โซลาร์บีม |
120.0 |
100 |
22 |
จากนั้นใช้ fillna
print(tha.fillna(0))
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
คัตเตอร์ใบไม้ |
55.0 |
95 |
0 |
สปอร์เห็ด |
0.0 |
100 |
0 |
เมกาเดรน |
40.0 |
100 |
21 |
โซลาร์บีม |
120.0 |
100 |
22 |
แต่ว่าบางครั้งข้อมูลในแต่ละคอลัมน์อาจมีค่าที่ต้องการเติมไม่เหมือนกัน แบบนี้อาจใส่ค่าในรูปดิกชันนารีแทนโดยระบุว่าคอลัมน์ไหนต้องการเติมด้วยอะไร
print(tha.fillna({'พลังโจมตี':0,'วาซะแมชชีน':'ไม่มี'}))
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
คัตเตอร์ใบไม้ |
55.0 |
95 |
ไม่มี |
สปอร์เห็ด |
0.0 |
100 |
ไม่มี |
เมกาเดรน |
40.0 |
100 |
21 |
โซลาร์บีม |
120.0 |
100 |
22 |
หรือจะใช้เดตาเฟรมที่มีขนาดเท่ากันมาเป็นตัวเติมก็ได้ แบบนั้นค่าจากเดตาเฟรมที่เติมจะไปแทนในส่วนที่เป็น NaN ส่วนตรงที่มีค่าอยู่แล้วก็จะเป็นค่าเดิม เช่น
tha0 = pd.DataFrame([
[0,555,'~'],
[-1,555,'='],
[-2,555,'@'],
[-3,555,'#']],
columns=['พลังโจมตี','ความแม่นยำ','วาซะแมชชีน'],
index=['คัตเตอร์ใบไม้','สปอร์เห็ด','เมกาเดรน','โซลาร์บีม'])
print(tha.fillna(tha0))
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
คัตเตอร์ใบไม้ |
55.0 |
95 |
~ |
สปอร์เห็ด |
-1.0 |
100 |
= |
เมกาเดรน |
40.0 |
100 |
21 |
โซลาร์บีม |
120.0 |
100 |
22 |
หรือบางครั้งเราอาจจะต้องการเติมส่วนขาดโดยอาศัยข้อมูลที่มีอยู่ กรณีแบบนี้อาจใช้คีย์เวิร์ด method แทน
หากต้องการเติมข้อมูลที่ขาดโดยอาศัยข้อมูลที่อยู่ด้านหน้าให้ใส่เป็น method='ffill' หรือ method='pad' เช่น
print(tha.fillna(method='ffill'))
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
คัตเตอร์ใบไม้ |
55.0 |
95 |
NaN |
สปอร์เห็ด |
55.0 |
100 |
NaN |
เมกาเดรน |
40.0 |
100 |
21 |
โซลาร์บีม |
120.0 |
100 |
22 |
ซึ่งจะเห็นว่าข้อมูลตรงไหนที่ไม่มีตัวข้างหน้าก็จะไม่ถูกเติม
ในทางตรงข้ามถ้าจะเติมด้วยข้อมูลด้านหลังก็ใส่ method='bfill' หรือ method='backfill'
print(tha.fillna(method='bfill'))
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
คัตเตอร์ใบไม้ |
55.0 |
95 |
21 |
สปอร์เห็ด |
40.0 |
100 |
21 |
เมกาเดรน |
40.0 |
100 |
21 |
โซลาร์บีม |
120.0 |
100 |
22 |
หากไม่ต้องการให้เติมทั้งหมดแต่จำกัดจำนวนที่มีการเติมก็อาจใส่คีย์เวิร์ด limit เช่น
print(tha.fillna(method='bfill',limit=1))
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
คัตเตอร์ใบไม้ |
55.0 |
95 |
NaN |
สปอร์เห็ด |
40.0 |
100 |
21 |
เมกาเดรน |
40.0 |
100 |
21 |
โซลาร์บีม |
120.0 |
100 |
22 |
แบบนี้ 21 จะถูกเติมไปที่ช่องก่อนหน้าเพียงช่องเดียว แต่ถัดไปอีกก็ยังคงเป็น NaN อยู่
กรณีข้างต้นนี้เป็นการเติมโดยอาศัยข้อมูลในต่างแถวที่อยู่ในคอลัมน์เดียวกัน แต่หากต้องการเติมด้วยข้อมูลที่อยู่ในแถวเดียวกันจากต่างคอลัมน์ก็ให้ใส่คีย์เวิร์ด axis=1 เช่น
print(tha.fillna(method='ffill',axis=1))
ได้
|
พลังโจมตี |
ความแม่นยำ |
วาซะแมชชีน |
คัตเตอร์ใบไม้ |
55 |
95 |
95 |
สปอร์เห็ด |
NaN |
100 |
100 |
เมกาเดรน |
40 |
100 |
21 |
โซลาร์บีม |
120 |
100 |
22 |
และเช่นเดียวกับ dropna เลยคือ fillna จะสร้างเดตาเฟรมอันใหม่โดยไม่เขียนทับอันเก่า หากต้องการให้เขียนทับก็ใส่คีย์เวิร์ด inplace=True ลงไป
อ้างอิง