Getting Started with Pandas in Python

วันนี้จะพามารู้จัก สัตว์ อีกชนิดหนึ่ง (นอกจาก งู 🐍) ที่จะช่วยให้เราทำงานกับข้อมูลใน Python ได้ง่ายขึ้นนั่นคือ Pandas 🐼

Pandas คืออะไร ?

via GIPHY

Pandas เป็นหมีชนิดนึงที่มักอาศัยอยู่ อยู่ตามภูเขาในประเทศจีน ไม่ใช่ละ คนละแพนด้าละ !!! แพนด้าที่เราจะพูดถึงในวันนี้เป็น Library ที่มี Data Structure และ Function สำหรับการจัดการและเตรียมข้อมูลใน Python ที่กำลังเป็นที่นิยมโคตร ๆ

ถามว่า Pandas เข้ามาอยู่ตรงไหนของการทำงานเรา ก็คงตอบได้ว่า มันอยู่ในช่วงแรกของ Workflow เลยก็ว่าได้ (ถ้าเราต้องการเอาข้อมูลมาทำ Classification และวิเคราห์ในขั้นตอนต่อ ๆ ไป)

Getting Started with Pandas

หลังจากเราเข้าใจแล้วว่า Pandas คืออะไร เรามาลองเริ่มใช้กันเลยดีกว่า โดยเริ่มจากการติดตั้งเข้าหมียักษ์ตัวนี้เข้าไปในเครื่องของเรากันก่อน โดยเราสามารถติดตั้งได้หลายวิธีมาก ๆ แต่วันนี้ผมจะใช้ pip ในการติดตั้งบน Python3 ละกัน โดยใช้คำสั่งดังนี้ (สำหรับ Python 2.7 ก็สามารถใช้ pip ได้เช่นกัน แค่เอาเลข 3 ออก)

pip3 install pandas

รันแค่คำสั่งเดียว เราก็ได้หมียักษ์เข้ามาในเครื่องเราเป็นที่เรียบร้อยแล้ว หลังจากนั้น เวลาจะใช้เราก็ต้อง Import มันเข้ามาในไฟล์ของเราโดยใช้คำสั่ง

import pandas as pd

ก็จริง ๆ แล้วตรง as pd ข้างหลังไม่ต้องใส่มาก็ได้ แต่เพื่อความพิมพ์แล้วมือไม่พังซะก่อน กับทำให้ Code เราดูสั้นขึ้นเขียนแบบนี้ก็ไม่เลวเท่าไหร่ ง่ายดี 😃

DataFrame vs Series

ทีนี้เรามาดูกันบ้างดีกว่าว่า ใน Pandas นั่นมี Data Structure อะไรมาให้เราเลือกใช้บ้าง ?

ในตัวของหมียักษ์มันจะมีข้อมูลอยู่ 2 ประเภทด้วยกัน อย่างแรกคือ Series ให้เรานึกภาพถึง 1D Array ที่เราสามารถเข้าถึงตำแหน่งไหนก็ได้ของข้อมูล

กับอีกอันคือ DataFrame อันนี้น่าจะเป็นอะไรที่เราน่าจะใช้บ่อยมาก ๆ ใน Pandas แล้ว อันนี้ให้เรานึกถึง Grid เหมือนใน Excel เลยที่จะมี Row และ Column เป็น 2D

ถ้าเราบอกว่า DataFrame มันเป็นข้อมูลที่เป็นตาราง 2D ที่เราเคยเห็นกันในกระดาษ มันก็ไม่น่าจะแปลกเท่าไหร่ ถ้าเราตัดสัก 1 Column ออกมาแล้วเราจะได้ตารางออกมา ตรงนี้อยากให้จำไว้หน่อย เพราะถ้า งง ตรงนี้ เวลาเราไปอ่าน Document แล้วไปเจอบางคำสั่งที่เราเอา DataFrame เข้าแต่ได้ Series ออกมาแทน

โดยที่ทั้ง 2 Data Structure นี้มันจะมีคำสั่งให้เราสามารถจัดการข้อมูลได้เยอะมาก ๆ ถ้าใครเคยใช้ SQL ใน Database มาก่อน ให้นึกถึง Query ได้เลย เพราะมันทำได้ขนาดนั้นเลยละใรแง่ของการ Select, Aggregate และ Search ข้อมูล

Accessing Data

เราน่าจะเข้าใจกันไปแล้วว่าข้อมูลที่หมียักษ์มันเก็บหน้าตาเป็นยังไง ตอนนี้เรามาลองใช้กันเลยดีกว่า !

Pandas นอกจากที่เราจะจัดการกับข้อมูลได้แล้ว เรายังสามารถ Import ข้อมูลจากไฟล์ได้ในตัวเลย โดยไม่ต้องลง Library เพิ่ม โดยผ่าน Function ที่ชื่อว่า read_csv

Data = pd.read_csv('./happiness_2017.csv')

เพียงเท่านี้เราก็ได้ข้อมูลมาแล้ว ~ แต่บางทีข้อมูลที่เราเอาเข้าอาจจะไม่ใช่ CSV พูดง่าย ๆ คือมันไม่ได้ใช้ Comma ในการเว้นช่อง ที่อาจจะใช้ Space ในการเว้นช่อง เราก็สามารถบอกมันให้อ่าน Space เป็นช่องแทนได้ โดยการเพิ่ม sep ดั่งตัวอย่างนี้

Data = pd.read_csv('./happiness_2017.csv', sep=" ")

โดยที่ข้อมูลที่เอาเข้ามา มันจะตัดแถวแรกออกไปเป็น Header หรือหัวตารางโดยอัตโนมัติ แต่ถ้าข้อมูลของเราไม่มีหัวตาราง เราก็สามารถใช้ header ในการบอกว่าเราไม่มีหัวตาราง หรือหัวตารางเราอยู่ตรงไหนได้ และเรายังสามารถกำหนดหัวตารางเองได้อีกด้วย

Data = pd.read_csv('./happiness_2017.csv', header=None, names=['col1', 'col2', 'col3'])

จะเห็นว่าผมมีการเติม Option เข้าไปอีก 2 ตัวคือ header และ names สำหรับตัวแรกเป็นการบอกว่าหัวตารางของเราอยู่แถวที่เท่าไหร่ และตัวสุดท้ายคือจะใส่เมื่อเราต้องการใส่ชื่อหัวตารางเอง

คำเตือน! ถ้าเราใส่ Option names ลงไป เราต้องเติม header=None ลงไปด้วย เพราะเราต้องการกำหนดหัวตารางเองโดยที่ไม่เอาแถวแรกมาเป็นหัวตาราง

ปัญหามันจะเริ่มเกิด เมื่อปริมาณข้อมูลของเราเยอะมากเกินกว่าที่ Ram เราจะเอาอยู่ การใช้ read_csv เฉย ๆ เกรงว่าจะไม่น่ารอด จึงมีอีก Pattern ในการอ่านไฟล์ดังนี้

chunk_size = 10000
chunks = []

for chunk in read_csv('./happiness_2017.csv', chunksize=chunksize) :
	//do sth
	chunks.append(chunk)

data.concat(chunks, axis=0)

จากตัวอย่างด้านบนนี้ทำให้เราเห็นว่า แทนที่เราจะ Import มาทั้งก้อน เราก็แยกมันเป็น Chunk ก่อน ๆ เพื่อให้เราทำอะไรสักอย่างในการตัด Data ที่เราไม่ได้ใช้ออกก่อน เพื่อไม่ให้ Memory เต็ม หรือ เราสามารถกำหนดได้เลยว่า เราอยากให้ Data เข้ามากี่ Chunk ก็ได้เช่นกัน

เท่านี้เราก็สามารถเอาข้อมูลเข้าปากหมีได้ละ ! 🎍 นอกจากนั้นเรายังสามารถที่จะดูได้ด้วยว่าใน DataFrame ของเรามี Column อะไรบ้างโดยใช้คำสั่งดังนี้

print(Data.columns)

เราก็จะได้อะไรแบบด้านล่างมา

Index(['Country', 'Happiness.Rank', 'Happiness.Score', 'Whisker.high',
       'Whisker.low', 'Economy..GDP.per.Capita.', 'Family',
       'Health..Life.Expectancy.', 'Freedom', 'Generosity',
       'Trust..Government.Corruption.', 'Dystopia.Residual'],
      dtype='object')

ใช่ครับ เราจะได้ออกมาเป็น Array โดยที่เราสามารถเข้าถึงชื่อผ่าน Index ได้เลยโดยตรง พูดถึง Column แล้ว เราก็ยังสามารถเอาข้อมูลใน Column นั้น ๆ ออกมาได้ด้วย

print(Data['Economy..GDP.per.Capita.']

โดยที่เราก็ใส่ชื่อ Column ลงไปเหมือนเราเรียก Array ได้เลย หรือเราสามารถใช้ชื่อตามด้วยจุดและชื่อ Columns ก็ได้เช่นกัน แต่ถ้าเป็นในกรณีตัวอย่างนี้มันจะพัง เพราะในชื่อ Column ของเรามันดันมีจุดอยู่ มันจะนึกว่ามันไปเรียกลูกของมันทำให้พังได้ วิธีที่ชัวร์ที่สุดคือการเข้าถึงเหมือน Array ที่ยกตัวอย่างนี่แหละ

ถ้าเราต้องการเข้าถึงข้อมูลเป็น Row ก็ได้เช่นกัน โดยเราอาจจะบอกว่า เราอยากได้ข้อมูลใน Row ที่ 0 ถึง 10 เราก็สามารถทำได้ดังนี้

print(Data[0:10:5])

เราก็จะได้ Data ตั้งแต่ Row ที่ 0-10 ทีนี้เลข 5 ตัวสุดท้ายมาจากไหน มันคือตัวเลขที่บอกว่า เราจะ Sampling ทีละเท่าไหร่ เช่นถ้าเราใส่ 5 มันก็จะเอาข้อมูลตัวที่ 0,5 ออกมาให้เราแทนที่จะเป็น 0,1,2,...,9 ให้เรานั่นเอง ทีน้ีถามว่า ถ้าเราอยากจะเอา Row เดิมนี่แหละ แต่อยากได้แค่ Column เดียวเราจะทำยังไง

print(Data[0:10:5]['Country'])

ก็ทำได้ง่ายมาก ๆ โดยการเติมอีกกล่องเข้าไปข้างหลังหรือใช้จุดเหมือนกับที่ได้เล่าไปในตอนที่เราเข้าถึง Column

แล้วถ้า Row และ Column ของเรามันไม่มีชื่อละ ?

ปัญหานี้มันจะเกิดเมื่อ Dataset ที่เราเอาเข้ามา มันไม่มีหัวตารางฉะนั้น เราก็จะไม่สามารถใช้ชื่อในการเข้าถึงได้แล้ว อีกวิธีหนึ่งที่เหล่าโปรแกรมเมอร์ชอบกันก็คือการเข้าถึงผ่าน Index โดยตรงเลย ซึ่งใน Pandas ก็สามารถทำได้โดยการใช้คำสั่ง iloc

# Example
# DataFrame.iloc[FromRow:ToRow, FromCol:toCol]

# Print The First Row with 20 Columns
print(Data.iloc[0:1,0:20])

จากตัวอย่างด้านบนนี้ เราก็จะได้ข้อมูลใน Row แรกที่มี 20 Columns ออกมา และแน่นอนว่า เราสามารถเรียก Row โดยที่เราสามารถใช้การบอกตำแหน่ง 3 หลักในการบอก Row ได้เหมือนกับตัวอย่างด้านบนเลย

หมายเหตุ จริง ๆ แล้วมันยังมีคำสั่ง loc ธรรมดาอยู่สำหรับการเข้าถึง Index ที่เป็น String และ ix สำหรับการเข้าถึง Index ที่เป็นทั้ง String และตัวเลข

ฟิลลิ่งตรงนี้ ถ้าเป็นคนที่เขียนโปรแกรมมาก่อน น่าจะรู้สึกแปลกนิด ๆ เพราะเหมือนมันเรียก Function อะไรสักอย่าง แล้วแทนที่เราจะป้อน Argument ที่ครอบอยู่ในวงเล็บเข้าไป กลายเป็น [] ซะงั้น 😫

Selecting Data

ถ้าเราบอกว่า เราต้องการเลือกข้อมูลตาม เงื่อนไขบางอย่าง เราก็ทำได้เช่นกัน จากตัวอย่างด้านล่างนี้

print(data.loc[lambda data: data.Country == 'Norway', :])

จากใน Code ด้านบน เราจะเห็นว่าเราสามารถใส่ Callable ลงไปในการสร้างเงื่อนไขเพื่อเลือกข้อมูลขึ้นมาได้ เช่นในตัวอย่างด้านบนที่ผมต้องการเลือก Row มี Country เป็น Norway นั่นเอง ซึ่งเราก็สามารถนำเทคนิคตรงนี้ไปใช้งานได้อย่างมากมาย ถ้าใช้คล่อง ๆ ก็เหมือนเรา Query ข้อมูลจาก Database โดยใช้ SQL เลยละ

Data Exploration

ตอนนี้เราลองมาดูกันดีกว่า ว่า Data ของเราเป็นยังไง โดยที่เราสามารถดึง n rows แรกออกของข้อมูลออกมาได้

Data.head(n)

นอกจากนั้น ถ้าเราอยากได้ Statistics ของแต่ละ Column เราก็สามารถใช้คำสั่ง describe ในการดูได้ โดยผลจะออกมาเป็นแบบนี้

       Happiness.Rank  Happiness.Score  Whisker.high  Whisker.low
count      155.000000       155.000000    155.000000   155.000000   
mean        78.000000         5.354019      5.452326     5.255713   
std         44.888751         1.131230      1.118542     1.145030   
min          1.000000         2.693000      2.864884     2.521116   
25%         39.500000         4.505500      4.608172     4.374955   
50%         78.000000         5.279000      5.370032     5.193152   
75%        116.500000         6.101500      6.194600     6.006527   
max        155.000000         7.537000      7.622030     7.479556

โดยที่เราจะได้ count, mean, std, min, 25%, 50%, 75% และ max ออกมา โดยที่ 25%, 50% และ 75% คือค่าของแค่ละใน Quartiles นั่นเอง (ถ้า งง ก็กลับไปอ่าน Statistics ก่อนนะ)

ใน Dataset บางตัวมันก็จะแอบมี Missing Data อยู่ เราก็สามารถดูในช่อง count ได้ โดยการที่เราดูว่า จำนวนของ count เท่ากับจำนวนของข้อมูลรึยัง ได้เช่นกัน

แน่นอนว่า มันแสดง mean กับ std ออกมาได้ มันก็ต้องคำนวณออกมาให้เราเป็นค่าเปล่า ๆ ได้เหมือนกันผ่านคำสั่งในตัวอย่างนี้

mean_val = Data['Whisker.high'].mean()
std_val = Data['Whisker.high'].std()

แน่นอนว่ากับ max และ min ก็ไม่ต่างเลย

Imputing Data

จากหัวข้อก่อนหน้านี้ได้มีการพูดถึงในเรื่องของการ หา Missing Data ไปแล้ว ตอนนี้เรามาดูกันว่า เราจะสามารถเติมส่วนที่หายไปได้อย่างไรบ้าง

ซึ่งในทางสถิติแล้ว เรื่องของการเติมข้อมูลที่หายไปนั้นทำได้หลายวิธีมาก แนะนำให้ลองไปอ่านดู แต่ในวันนี้ผมจะลองทำในแบบที่ง่ายที่สุดกันนั่นคือการเอา Mean เข้าไปใส่ตรง ๆ เลย ซึ่งมันก็มีข้อเสียที่สามารถใช้ได้กับแค่ข้อมูลที่เป็นตัวเลขเท่านั้น สมมุติว่าเราต้องการที่จะ Impute ช่อง Freedom เราก็สามารถทำได้ดังนี้

data.Freedom.fillna(data.Freedom.mean(), inplace=True)

แล้วทีนี้เราถ้าลองเช็คหา Missing Data ของช่อง Freedom มันก็จะหายไปหมดแล้ว เพราะเราแทนที่มันด้วยค่า Mean ไปเรียบร้อยแล้ว ซึ่งวิธีการเติมข้อมูลอื่น ๆ ก็จะใช้ลักษณะนี้ในการจัดการ คล้าย ๆ กัน ขึ้นกับความซับซ้อนในการเติม

หรือถ้าไม่อยากจะ Fill ข้อมูลเข้าไป เราก็สามารถเอามันออกไปได้เช่นกัน โดยผ่านคำสั่งดังนี้

data.Freedom.dropna()

มันก็จะเอา Row ที่ในช่อง Freedom ว่างออกไปจากข้อมูลเรา

แต่ถ้าเราต้องการที่จะทำ Smoothing เช่นการบวก 1 เข้าไปในทุก Row ของสัก Column นึงก็ทำได้เช่นกัน

def smooth_data (number) :
	return number + 1

Data.Price = Data.price.apply(smooth_data)

# OR
Data.Price = Data.Price.apply(lambda price: price + 1)

หรือถ้าเราต้องการที่จะ Filter บางอย่าง เราก็แค่เติม Condition ลงไปใน Function เราก็เป็นอันจบแล้ว

สรุป

via GIPHY

Pandas เป็นหมียักษ์หลายตัว ที่อาศัยอยู่แถบตอนกลางของประเทศจีน (ยังอีก ยังไม่เลิกเล่นอีก !! 🐼) ก็เป็น Library ที่ช่วยให้เราสามารถจัดการกับ Dataset ของเราได้ง่ายขึ้น ตั้งแต่ Import ยัน Data Preparation กันเลยทีเดียว จริง ๆ แล้ว Pandas ยังมีอีกหลายความสามารถมาก ๆ ที่ไม่ได้ยกมาเล่าในวันนี้ แนะนำให้ลองไปอ่าน Documentation ดูแล้วจะรู้ว่า หมียักษ์ตัวนี้มันทำอะไรได้มากกว่ากิน กับนอน และ Entertain ผู้คนได้ วันนี้สวัสดีฮ่ะ 😃