6.2 ML data engineering(preprocessing)
6.2 데이터 전처리
앞서 6.1에서 살펴본 사이킷런에서 제공하는 데이터와 달리 보통 머신러닝 데이터들은 데이터 전처리가 필요함
‘피처 엔지니어링’ 이라 불림
결측치 처리 / 클래스 라벨 설정 / 원-핫 인코딩 / 데이터 스케일링 등이 있다.
6.2.1 결측치 처리
주어진 데이터 셋에 데이터가 존재하지 않는다? 빵꾸가 나있다?(missing value) 이 ‘결측치’를 처리해줘야 한다.
import numpy as np
import pandas as pd
# 먼저 이 결측치를 포함하는 데이터 프레임을 생성한다
# 데이터 프레임 직접 생성
df = pd.DataFrame([
[42, 'male',12,'reading','class2'],
[35, 'unknown',3,'cooking','class1'],
[1000, 'female',7,'cycling','class3'],
[1000, 'unknown',21,'unknown','unknown']
])
# columns이름 정해주기
df.columns = ['age','gender', 'month_birth', 'hobby', 'target']
# 생성한 데이터 프레임 출력해보기
df
| age | gender | month_birth | hobby | target | |
|---|---|---|---|---|---|
| 0 | 42 | male | 12 | reading | class2 |
| 1 | 35 | unknown | 3 | cooking | class1 |
| 2 | 1000 | female | 7 | cycling | class3 |
| 3 | 1000 | unknown | 21 | unknown | unknown |
모두 값이 존재하긴하나, 부적절한 값들이 있을 때 그것을 결측치로 여겨야 한다. 고로 이 ‘부적절’한 값들을 ‘결측치’로 변경해야 한다.
# 먼저 unique() 메소드로 중복 제거한 해당 각 column의 값들을 array로 출력.
df['age'].unique()
# df['gender'].unique()
# df['month_birth'].unique()
# df['hobby'].unique()
# df['target'].unique()
array([ 42, 35, 1000])
부적절한 해당 값들을 np.nan을 이용하여 결측치 처리할 것이다.
nan 이 결측치를 의미한다
# 결측치로 세팅
df.loc[df['age'] > 150, ['age']] = np.nan
df.loc[df['gender'] == 'unknown', ['gender']] = np.nan
df.loc[df['month_birth'] > 12, ['month_birth']] = np.nan
df.loc[df['hobby'] == 'unknown', ['hobby']] = np.nan
df.loc[df['target'] == 'unknown', ['target']] = np.nan
# 결과 확인
df
| age | gender | month_birth | hobby | target | |
|---|---|---|---|---|---|
| 0 | 42.0 | male | 12.0 | reading | class2 |
| 1 | 35.0 | NaN | 3.0 | cooking | class1 |
| 2 | NaN | female | 7.0 | cycling | class3 |
| 3 | NaN | NaN | NaN | NaN | NaN |
결측치를 포함하는 데이터는 머신러닝 모델에 적합하지 않은 경우가 많다
고로, 처음 데이터셋을 접했을 때, 결측치가 존재하는지 확인해야 한다. (부적절한 값이 존재하면 결측치로 바꿔줘야함)
# 각 열마다 결측치가 몇개 있는지 확인
df.isnull().sum()
age 2
gender 2
month_birth 1
hobby 1
target 1
dtype: int64
- 삭제하는 방식은 df.dropna() 메소드로 처리
# 결측치 포함했다면 해당 row(data) 삭제하는 방식으로 처리
df2 = df.dropna(axis=0)
df2
| age | gender | month_birth | hobby | target | |
|---|---|---|---|---|---|
| 0 | 42.0 | male | 12.0 | reading | class2 |
# 결측치 포함했다면 해당 column 삭제하는 방식으로 처리
# 모든 column에 결측치 있으므로 출력되는 것이 없을 것임
df3 = df.dropna(axis=1)
df3
| 0 |
|---|
| 1 |
| 2 |
| 3 |
# 모든 값이 결측치인 row 삭제하기
df4 = df.dropna(how = 'all')
df4
| age | gender | month_birth | hobby | target | |
|---|---|---|---|---|---|
| 0 | 42.0 | male | 12.0 | reading | class2 |
| 1 | 35.0 | NaN | 3.0 | cooking | class1 |
| 2 | NaN | female | 7.0 | cycling | class3 |
# 결측치 제외한 값이 thresh = n 개보다 작을 경우, 해당 row를 삭제
df5 = df.dropna(thresh=2)
df5
| age | gender | month_birth | hobby | target | |
|---|---|---|---|---|---|
| 0 | 42.0 | male | 12.0 | reading | class2 |
| 1 | 35.0 | NaN | 3.0 | cooking | class1 |
| 2 | NaN | female | 7.0 | cycling | class3 |
# 특정 column에 결측치가 있는 경우 해당 row삭제 (gender column에 결측치 있을 때 해당 row 삭제)
df6 = df.dropna(subset = ['gender'])
df6
| age | gender | month_birth | hobby | target | |
|---|---|---|---|---|---|
| 0 | 42.0 | male | 12.0 | reading | class2 |
| 2 | NaN | female | 7.0 | cycling | class3 |
# 결측치 대체하기
# 왜 에러나는지 잘 모르겠음(after_values는 왜 안될까)
# fillna(0) 등으로 특정값으로 그냥 대체하면 될 것 같음
# after_values = ('age': 0,
# 'gender' : 'U',
# 'month_birth' : 0,
# 'hobby' : 'U',
# 'target' : 'class4')
df7 = df
# 다른 방식으로 대체
df7['age'] = df7['age'].fillna(0)
df7['gender'] = df7['gender'].fillna('U')
df7['month_birth'] = df7['month_birth'].fillna(0)
df7['hobby'] = df7['hobby'].fillna('U')
df7['target'] = df7['target'].fillna('class4')
df7
| age | gender | month_birth | hobby | target | |
|---|---|---|---|---|---|
| 0 | 42.0 | male | 12.0 | reading | class2 |
| 1 | 35.0 | U | 3.0 | cooking | class1 |
| 2 | 0.0 | female | 7.0 | cycling | class3 |
| 3 | 0.0 | U | 0.0 | U | class4 |
6.2.2 클래스 라벨 설정
# 현재 df는 타겟(라벨)이 string이다. int로 바꿔줘보자
# 사이킷런의 preprocessing 패키지의 LabeEncoder 함수 import
from sklearn.preprocessing import LabelEncoder as LE
df8 = df7
# 라벨링 모델을 LE로 설정
class_label = LE()
# 'target' 의 값들을 data_value에 저장
data_value = df8['target'].values
# 라벨링 모델 'class_label'에 데이터 값을 넣고 변환한 값을 y_new에 저장
y_new = class_label.fit_transform(data_value)
# 결과 확인
y_new
array([1, 0, 2, 3])
# 실제 df8 데이터 프레임에 int로 바꿔준 target column(라벨링)을 대체
df8['target'] = y_new
df8
| age | gender | month_birth | hobby | target | |
|---|---|---|---|---|---|
| 0 | 42.0 | male | 12.0 | reading | 1 |
| 1 | 35.0 | U | 3.0 | cooking | 0 |
| 2 | 0.0 | female | 7.0 | cycling | 2 |
| 3 | 0.0 | U | 0.0 | U | 3 |
# 원래대로 되돌리고 싶다면?
y_ori = class_label.inverse_transform(y_new)
y_ori
df8['target'] = y_ori
df8
| age | gender | month_birth | hobby | target | |
|---|---|---|---|---|---|
| 0 | 42.0 | male | 12.0 | reading | class2 |
| 1 | 35.0 | U | 3.0 | cooking | class1 |
| 2 | 0.0 | female | 7.0 | cycling | class3 |
| 3 | 0.0 | U | 0.0 | U | class4 |
사이킷런 사용하지 않고 직접 라벨링하는 법을 알아보자!
# label들을 담은 배열을 만든다
y_arr = df8['target'].values
y_arr.sort()
y_arr
array(['class1', 'class2', 'class3', 'class4'], dtype=object)
# dictionary를 구성한다 (string label : int label) pair로
num_y = 0
dic_y = {}
for ith_y in y_arr:
dic_y[ith_y] = num_y
num_y += 1
dic_y
{'class1': 0, 'class2': 1, 'class3': 2, 'class4': 3}
# 대체한다(에러발생)
# df8['target'] = df8['target'].replace(dic_y)
6.2.3 원-핫 인코딩
0과 1만을 써서 데이터값을 나타내는 것
dummy variable이라고도 불림
df9 = df8
# 타깃 라벨링을 현재 int인 것을 string으로 바꿔줌
df9['target'] = df9['target'].astype(str)
df10 = pd.get_dummies(df9['target'])
# 타깃 0 은 1000 / 타깃 1 은 0100 ... 이런식으로 표현됨
print(df10)
df10
0 1 2 3
0 1 0 0 0
1 0 1 0 0
2 0 0 1 0
3 0 0 0 1
| 0 | 1 | 2 | 3 | |
|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 0 |
| 1 | 0 | 1 | 0 | 0 |
| 2 | 0 | 0 | 1 | 0 |
| 3 | 0 | 0 | 0 | 1 |
# 벡터길이 3인 원-핫 인코딩 하기
df11 = pd.get_dummies(df9['target'], drop_first = True)
print(df11)
1 2 3
0 0 0 0
1 1 0 0
2 0 1 0
3 0 0 1
# 특정 열이 아니라 데이터 프레임 전체의 column을 원-핫 인코딩하기 (column이 추가 변경된다 0,1로만 표현해야하므로)
df12 = df8
df13 = pd.get_dummies(df12)
df13
| age | month_birth | gender_U | gender_female | gender_male | hobby_U | hobby_cooking | hobby_cycling | hobby_reading | target_0 | target_1 | target_2 | target_3 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 42.0 | 12.0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
| 1 | 35.0 | 3.0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
| 2 | 0.0 | 7.0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 |
| 3 | 0.0 | 0.0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
# 데이터 프레임 형식이 아닌 array형식으로 출력하기
from sklearn.preprocessing import OneHotEncoder as OHE
# 텐서플로우 라이브러리에서는 to_categorical() 함수 이용한다
# from tensorflow.keras.utils import to_categorical
# y_hotec = to_categorical(y)
# 이런 식으로 진행하면 된다.
# 원-핫 인코더 함수 불러오기
hot_encoder = OHE()
# 원-핫 인코딩 변수 설정
# 해당 target column을 인코딩할꺼니까 target column 값들을 array로 저장
y = df7[['target']]
# fit_transform으로 주어진 데이터 원-핫 인코딩 한다.
y_hot = hot_encoder.fit_transform(y)
# y_hot을 array형태로 출력
print(y_hot.toarray())
[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
6.2.4 데이터 스케일링
feature(=각 data의 column들)은 제각기 다른 단위를 가진다
데이터 스케일링은 ‘단위’에 영향받지 않도록 데이터를 변형하는 것을 말한다
(1) 표준화 스케일링(Standard Scaling)
평균이 0 , 표준편차가 1 이 되는 표준정규분포가 되도록 하는 scaling 방식
# df8의 month_birth feature를 표준화해보자
from sklearn.preprocessing import StandardScaler as ss
# 표준화 스케일러의 이름을 std로 정함
std = ss()
# 표준화 스케일러에 해당 feature를 적합시킨다
std.fit(df8[['month_birth']])
# 해당 feature 값 실제로 변경하는 단계
# 반드시 fit과정을 거친 후 transform 해줘야함 안그러면 에러남
# 귀찮아서 하나로 합친 함수 등장
x_std = std.transform(df8[['month_birth']])
x_std
array([[ 1.44444444],
[-0.55555556],
[ 0.33333333],
[-1.22222222]])
# 위 fit 이후 , transform 해주는 과정을 하나로 압축!!!
x_std2 = std.fit_transform(df8[['month_birth']])
x_std2
array([[ 1.44444444],
[-0.55555556],
[ 0.33333333],
[-1.22222222]])
# 정말로 표준 정규분포를 따르는지 확인해볼까?
np.mean(x_std)
print(np.std(x_std))
1.0
(2) 로버스트 스케일링
표준화 스케일 변형한 방식으로 중강값(median)과 사분위수(quantile)를 사용한다.
극단값의 영향을 거의 받지 않는다는 장점
from sklearn.preprocessing import RobustScaler as rs
# 함수를 따로 호출을 반드시해줘야한다. 안해주면 에러남
rb = rs()
# 적합시키고, 변형
x_robust = rb.fit_transform(df8[['month_birth']])
x_robust
array([[ 1.16666667],
[-0.33333333],
[ 0.33333333],
[-0.83333333]])
(3) 최소-최대 스케일링
데이터가 최댓값이 1, 최솟값이 0이 되도록 범위를 정하는 변형방식.
from sklearn.preprocessing import MinMaxScaler
mms = MinMaxScaler()
x_mms = mms.fit_transform(df8[['month_birth']])
x_mms
array([[1. ],
[0.25 ],
[0.58333333],
[0. ]])
(4) 노멀 스케일링
Euclidean Length 가 1이 되도록 데이터값을 변경
벡터 길이는 상관 없고, 방향(각도)만 고려할 때 해당 전처리 방식을 사용한다
!! 앞선 세가지 스케일링과 달리 행 기준이다.
고로, age, month_birth 값을 이용하여 스케일링해보자
from sklearn.preprocessing import Normalizer
nm = Normalizer()
x_nm = nm.fit_transform(df8[['age','month_birth']])
x_nm
array([[0.96152395, 0.27472113],
[0.99634665, 0.08540114],
[0. , 1. ],
[0. , 0. ]])
! 데이터 스케일링 주의점!
트레이닝 데이터에 대해서만 fit을 해준다!
테스트 데이터에서는 transform만 해줘야 트레이닝 데이터에서와 동일한 평균, 표준편차 등을 사용하게 된다
이렇게 하지 않으면 train 이 아니라 test 데이터에 대한 평균, 표준편차를 이용하게 되므로 스케일링 범위와 파라미터가 달라지게 된다!!
# 표준화 스케일링
from sklearn.preprocessing import StandardScaleer
ss = StandardScaler()
# x_train과 x_test가 있다고 가정!
x_train_std = ss.fit_transform(x_train)
x_test_std = ss.transform(x_test)