Feature Engineering for AI and Machine Learning (การทำ Feature Engineering ด้วย Pandas)

ภาพจาก https://towardsdatascience.com/feature-engineering-translating-the-dataset-to-a-machine-ready-format-af4788d15d6c

บทความโดย ผศ.ดร.ณัฐโชติ พรหมฤทธิ์
ภาควิชาคอมพิวเตอร์
คณะวิทยาศาสตร์
มหาวิทยาลัยศิลปากร

"Data is the new oil!" หลายคนคงเคยได้ยินประโยคนี้ใช่ไหมครับ แต่อันที่จริงหากเป็นเรื่องเกี่ยวกับ AI และ Machine Learning แล้ว Data น่าจะเป็นเหมือนกับน้ำมันดิบ (Petroleum) มากกว่า เพราะโดยมากแล้วก่อนที่จะนำ Data มาใช้ในการวิเคราะห์ รวมทั้งใช้ในการ Train Model ให้มี Accuracy ที่สูงได้ เราจะต้องมีการกลั่น หรือสกัดเอา Features ที่เกี่ยวข้องออกมาเสียก่อน โดยเราเรียกกระบวนการในการสกัด Features จากข้อมูลดิบ (Raw Data) รวมทั้งการทำความสะอาดข้อมูล (Data cleansing) ว่าการทำ Feature Engineering

ซึ่งประโยชน์หลักๆ ของการทำ Feature Engineering คือ

  • เพื่อเตรียม Dataset ให้พร้อมสำหรับการทำ Data Analytics หรือ เป็น Input ของ Machine Learning Algorithm
  • เพื่อเพิ่มประสิทธิภาพให้ Machine Learning Model

ในบทความนี้ผู้อ่านจะได้ฝึกปฏิบัติการทำ Feature Engineering กับข้อมูล Wine Magazine Dataset โดยใช้เทคนิคต่างๆ 5 เทคนิค ได้แก่

  • Imputation
  • Handling Outliers
  • Binning
  • Log Transform
  • One-hot Encoding

Understanding Data Quality

แต่ก่อนจะสกัด Features ของข้อมูล เราจะต้องมีการสำรวจข้อมูล เพื่อพิจารณาคุณภาพของมันในแง่ต่างๆ แล้วจึงเลือกเทคนิคในการทำ Feature Engineering ที่เหมาะสม

Pandas Profiling เป็น Library หนึ่งที่ช่วยให้เราทำความเข้าใจคุณภาพของข้อมูลได้อย่างรวดเร็ว โดยมันจะคำนวนค่าทางสถิติต่างๆ ด้วย ProfileReport Function และแสดงผลลัพธ์ที่เข้าใจง่าย

เราจะติดตั้ง Pandas Profiling Library ด้วยคำสั่ง pip install ดังตัวอย่างด้านล่าง แล้ว คลิ๊กที่เมนู Kernel->Restart

pip install 'pandas-profiling[notebook]'

หลังจากนั้นจึง Load Dataset และเรียกใช้ ProfileReport Function บน Jupyter Notebook เพื่อทำความเข้าใจคุณภาพของข้อมูลของ Wine Magazine Dataset

import numpy as np
import pandas as pd
from pandas_profiling import ProfileReport
df = pd.read_csv('winemag-data_first150k.csv', sep = ';')
profile = ProfileReport(df, title="Pandas Profiling Report")
profile
`

ซึ่งจากภาพด้านบนเราพบค่าที่หายไป (Missing Values) ถึง 166,750 Cell หรือคิดเป็น 11.6%

เราจะใช้ Function isnull() และ sum() คำนวนจำนวน (Missing Value ในแต่ละ Column

print(df.isnull().sum())

นอกจากนี้เราจะดูข้อมูล 5 แถวแรกได้จากคำสั่ง Function head()

df.head()

Imputation

จากคำสั่ง df.isnull().sum() เราพบ Column price เป็น Missing Values ถึง 13,396 Cell ซึ่งเราจะทดลองแทนที่ Missing Value เหล่านั้นด้วยค่าเฉลี่ยของราคาไวน์ทั้งหมด ใน Column price โดยใช้ Function fillna()

new_df = df.copy()
new_df['price'].fillna(df['price'].mean(), inplace = True)
print(new_df.isnull().sum())

จากภาพด้านบน Missing Values ของ Column price ถูกแทนที่ด้วยค่าเฉลี่ย จึงทำให้พบจำนวน Missing Value เป็น 0

อย่างไรก็ตามในบางกรณีการแทนที่ Missing Value ด้วยค่า Mean ก็อาจจะทำให้ได้ข้อมูลที่ไม่ตรงกับที่ต้องการ ซึ่งเราอาจจะใช้การลบ Row หรือ Column  ทิ้ง จากการกำหนดค่า Threshold ดังตัวอย่างด้านล่าง จะทำให้ Column ที่มี Missing Value มากกว่า 50% คือ Column region_2 ถูกลบ

df.isnull().mean()
threshold = 0.5
new_df = df[df.columns[df.isnull().mean() < threshold]]
new_df.isnull().mean()

ขณะที่การลบ Row ที่มีร้อยละของ Missing Value มากกว่า 50% จะใช้คำสั่งด้านล่าง ซึ่งพบว่าจำนวน Row จะลดลงไป 5 Row

print(df.shape)

new_df = df.loc[df.isnull().mean(axis=1) < threshold]

print(new_df.shape)
print(new_df.isnull().sum())

แต่ถ้าค่าเฉลี่ยของแต่ละ Column มีความ Sensitive ต่อค่าที่ผิดปกติ (Outlier Value) หรือค่าน้อยๆ หรือค่ามากๆ (มีผลกระทบต่อการคิดค่าเฉลี่ย) วิธีหนึ่งในการแก้ปัญหาคือการแทน Missing Value ด้วยค่ามัธยฐานของแต่ละ Column แทน เนื่องจากค่ามัธยฐานจะมีความทนทานต่อ Outlier Value ได้มากกว่า ดังตัวอย่างต่อไปนี้

print(df.median())
new_df = df.fillna(df.median())
print(new_df.isnull().sum())

นอกจากนี้การแทน Missing Value ทุก Cell ด้วย 0 จะใช้คำสั่งดังนี้

new_df = df.fillna(0)
print(new_df.isnull().sum())

แต่การแทน Missing Value ด้วย 0 จะทำให้ Cell ที่มีชนิดข้อมูลแบบ String เป็น 0 ด้วยเช่นกัน ดังเช่นใน Cell ของ Column region_2 ด้านล่าง ซึ่งเป็นสิ่งที่เราไม่ต้องการ

new_df.head()

อีกวิธีหนึ่งคือการลบทั้งแถวทิ้ง ในกรณีที่พบ Cell หนึ่ง Cell ใดมี Missing Value ซึ่งจะเป็นวิธีการที่ดีถ้าไม่ทำให้มีข้อมูลหายไปเป็นจำนวนมาก

print(df.shape)

new_df = df.dropna(how='any')

print(new_df.shape)

ซึ่งเมื่อใช้กับ Wine Magazine Dataset พบว่ามีข้อมูลถูกลบไปมากกว่า 1 หมื่น Row

Handling Outliers

Outlier หรือค่าที่ผิดปกติ คือ ข้อมูลที่มีค่าสูง หรือต่ำกว่าข้อมูลส่วนใหญ่ในชุดข้อมูลหนึ่งๆ อย่างมาก การจัดการกับ Outlier อาจจะพิจารณาจาก Standard Deviation และ Percentile ดังแสดงในภาพด้านล่าง

เราจะทำ Data Visualization เพื่อดูลักษณะการกระจายของข้อมูล และ Outlier จาก Function boxplot() ของ Seaborn Library

import seaborn as sns
from matplotlib import pyplot as plt 
fig = plt.figure(figsize=(12,8))
sns.boxplot(x=df['price'], color='lime')
plt.xlabel('Price Featured', fontsize=14)
plt.savefig('boxplot.png', dpi=300)

จากภาพด้านบนจะเป็นการแสดงลักษณะการกระจายของข้อมูล และ Outlier ของ Column price ซึ่งพบราคาไวน์ที่สูงกว่าข้อมูลส่วนใหญ่ (Outlier) ทางด้านขวามือของภาพ โดยราคาไวน์มีค่าเฉลี่ยที่ 33 Dollar แต่มีราคาสูงสุด 2300 Dollar ซึ่งอาจเป็นราคาไวน์จริงที่ผู้นิยมสะสมไวน์ราคาแพงต้องการก็ได้นะครับ

df['price'].describe()

Drop Outlier with Standard Deviation

อย่างไรก็ตาม เราจะทดลองลบแถวที่พบ Outlier ใน Column ได้จากคำสั่งด้านล่าง

print(df.shape)

factor = 3
upper_lim = df['price'].mean () + df['price'].std () * factor
lower_lim = df['price'].mean () - df['price'].std () * factor

drop_outlier1 = df[(df['price'] < upper_lim) & (df['price'] > lower_lim)]

print(drop_outlier1.shape)

ซึ่งเมื่อดูลักษณะการกระจายของข้อมูล และ Outlier จะพบว่าค่าเฉลี่ยของราคาไวน์จะลดลงมาที่ 30.5 Dollor และราคาสูงสูงอยู่ที่ 142 Dollor

fig = plt.figure(figsize=(12,8))
sns.boxplot(x=drop_outlier1['price'], color='lime')
plt.xlabel('Price Featured', fontsize=14)
plt.savefig('boxplot.png', dpi=300)
drop_outlier1['price'].describe()

หมายเหตุ เราใช้ & สำหรับการทำ logical AND  เช่น

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
test_df = pd.DataFrame(a, columns=['A'])
test_df
In:
test_df['A'] < 8

Out:
0     True
1     True
2     True
3     True
4     True
5     True
6     True
7    False
8    False
9    False

In:
test_df['A'] > 5

Out:
0    False
1    False
2    False
3    False
4    False
5     True
6     True
7     True
8     True
9     True

In:
(test_df['A'] < 8) & (test_df['A'] > 5)

Out:
0    False
1    False
2    False
3    False
4    False
5     True
6     True
7    False
8    False
9    False
test_df[(test_df['A'] < 8) & (test_df['A'] > 5)]

Drop with Percentiles

นอกจากนี้เราสามารถลบแถวที่พบ Outlier ใน Column price ที่น้อยกว่าหรือเท่ากับ Quantile 0.5  และมากกว่าหรือเท่ากับ Quantile 0.95 ตามตัวอย่างด้านล่าง

print(df.shape)

upper_lim = df['price'].quantile(.95)
lower_lim = df['price'].quantile(.05)

drop_outlier2 = df[(df['price'] < upper_lim) & (df['price'] > lower_lim)]

print(drop_outlier2.shape)

ซึ่งเมื่อดูลักษณะการกระจายของข้อมูล และ Outlier จะพบว่าค่าเฉลี่ยของราคาไวน์ และราคาสูงสูงที่ได้จะแตกต่างจากการใช้ Standard Deviation โดยค่าเฉลี่ยราคาไวน์จะลดลงมาที่ 29.1 Dollor และราคาสูงสูงอยู่ที่ 79 Dollor

fig = plt.figure(figsize=(12,8))
sns.boxplot(x=drop_outlier2['price'], color='lime')
plt.xlabel('Price Featured', fontsize=14)
plt.savefig('boxplot.png', dpi=300)
drop_outlier2['price'].describe()

Binning

การทำ Binning หรือการแบ่งข้อมูลออกตามช่วงที่กำหนด อาจจะทำให้สามารถป้องกันการเกิด Overfitting เมื่อมีการ Train Model ได้ในระดับหนึ่ง

เช่น การแบ่งราคาไวน์เป็น Low, Mid, High ตามช่วง bin = [0, 20, 40, 100]

labels = ['low', 'mid', 'high']
bins = [0., 20., 40., 100.]

drop_outlier2['price_cat'] = pd.cut(drop_outlier2['price'], labels=labels, bins=bins, include_lowest=False)
drop_outlier2.sample(n=5).head()

Log Transform

Log Transform เป็นการใช้ Log ทางคณิตศาสตร์แปลงข้อมูล ซึ่งจะช่วยลดการเบ้ของข้อมูล โดยหลังการแปลงข้อมูลแล้ว จะทำให้การกระจายตัวเข้าสู่ Normal Distribution มากขึ้น

ax = drop_outlier2['price'].plot.hist(bins=12, alpha=0.5)
ax.figure.savefig('his.png', dpi=300)
drop_outlier2['log'] = drop_outlier2['price'].transform(np.log)
ax = drop_outlier2['log'].plot.hist(bins=12, alpha=0.5)
ax.figure.savefig('his.png', dpi=300)
drop_outlier2.sample(n=5).head()

*ตัวอย่างข้อมูลที่ถูก Transform โดยใช้ Log

One-hot Encoding

One-hot Encoding เป็นการเข้ารหัสข้อมูลแบบหนึ่งที่มักจะใช้กันบ่อยในงานทางด้าน Machine Learning โดยการขยายข้อมูลจากเดิมที่มี Column เดียว เป็นค่า 0 และ 1 หลายๆ Column ตามจำนวนหมวดหมู่ของข้อมูลใน Column เดิม โดยจะมีการกำหนดค่าเป็น 1 ใน Column ใหม่ ที่ตำแหน่งของ Column ซึ่งตรงกับเลขหมวดหมู่ของข้อมูลเดิม แล้วกำหนดค่า 0 ใน Column อื่นๆ ที่เหลือ เช่น การเข้ารหัส price_cat ซึ่งจะใช้ Function get_dummies() ของ Pandas Library ดังตัวอย่างด้านล่าง

encoded_columns = pd.get_dummies(drop_outlier2['price_cat'])
drop_outlier2 = drop_outlier2.join(encoded_columns)
drop_outlier2.sample(n=5).head()

ลองทำดู

ให้ผู้อ่านนำไฟล์ titanic.csv จาก Gitlab Project ที่ Clone ไว้ ทำ Feature Engineering กับข้อมูลด้วยเทคนิคต่างๆ ในบนความนี้