1. Data Preprocessing (데이터 전처리)
https://velog.io/@dauuuum/preprocessing#6-feature-engineering
순서 (EMOS - BF)
Encoding -> 결측치 제거 or 대체 -> 이상치 처리 -> Scaling(정규화) -> Data Balance -> Feature Engineering
Data Preprocessing
- 데이터 인코딩 (Label, one-hot encoding)
- ML 모델은 문자열을 허용하지 않음 -> 숫자값으로 바꿔야 함
OneHotEncoder | 순위·순서가 없는 범주형 변수일 때 사용 (X가 명목 - 범주형 변수) |
OrdinalEncoder | 순위·순서가 있는 범주형 변수일 때 사용 (X가 순서 - 범주형 변수) |
TargetEncoder | 높은 카디널리티의 범주형 변수일 때 사용 |
pd.get_dummies | 순위·순서가 없는 범주형 변수일 때 사용 (X가 명목 - 범주형 변수), OneHotEncoder 대체 가능 |
TargetEncoder().fit_transform(X_features, y_target)
- 범주형 타겟의 경우
특정 범주 형 값이 주어진 대상의 사후 확률과 모든 훈련 데이터에 대한 대상의 사전 확률의 혼합 - 연속형 타겟의 경우
특정 범주 값이 주어진 목표의 예상 값과 모든 학습 데이터에 대한 목표의 예상 값의 혼합
#install as:
pip install category_encoders
from category_encoders import OneHotEncoder
enc = OneHotEncoder()
enc.fit_transform(X_train)
- 결손값 처리 (Missing value handling)
- ML 모델을 데이터로서 Null/NaN값을 허용하지 않음 -> 무엇으로 대체할 것인가
df.isnull() # 결측치 확인
df.isnull().sum() # 컬럼별 결측값 개수 확인
df.fillna(0) # 결측값을 0으로 대체
df.fillna(df.mean()) # 결측값을 변수별 평균으로 대체
df.dropna() # 결측값이 포함된 행 제거
from sklearn.impute import SimpleImputer
imp = SimpleImputer(strategy='mean') # strategy(대체값 파라미터)= mean, median, most_frequent, constant
imp.fit_transform(X_train)
- 이상치 제거 (Outlier Detection)
- 평균에서 굉장히 동떨어진 데이터 제거
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.datasets import load_iris
# 데이터셋 불러오기
iris = load_iris()
df = pd.DataFrame(data=iris.data, columns=iris.feature_names)
df['target'] = iris.target
# boxplot으로 이상치가 있는지 판단
sns.boxplot(y = "sepal width (cm)", data = df);
# outlier 제거
def get_outlier(df, column, weight):
quantile_25 = np.percentile(df[column].values, 25)
quantile_75 = np.percentile(df[column].values, 75)
IQR = quantile_75 - quantile_25
IQR_weight = IQR * weight
lowest = quantile_25 - IQR_weight
highest = quantile_75 + IQR_weight
outlier = df[column][ (df[column] < lowest) | (df[column] > highest) ].index
return outlier
outlier = get_outlier(df, 'sepal length (cm)', 1.5)
df.drop(outlier, axis=0, inplace=True)
- 피처 스케일링 (Feature Scaling)
- 척도 맞추기
StandardScaler | 각 feature의 평균을 0, 분산을 1로 변경 |
MinMaxScaler | 최소/최대값이 각각 0, 1이 되도록 변경 |
MaxAbsScaler | 최대 절대값이 1이 되도록 변경 |
RobustScaler | 중앙값(median)과 IQR(interquartile range)을 사용하여 변경 |
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit_transform(X_train)
- 데이터 밸런싱 (Data Balancing)
- 하이퍼 파라미터 조정
- Up - Sampling: 데이터 자체를 증가시키는 기법
- Down - Sampling: 데이터 자체를 감소시키는 기법
- 하이퍼 파라미터 조정
# Random Forest 모델
RandomForestClassifier(class_weight='balanced') # class_weight [default=None]
# XGBoost 모델
XGBClassifier(scale_pos_weight='balanced') # scale_pos_weight [default=1]
- Up - Sampling
df_majority = df[df.balance==0]
df_minority = df[df.balance==1]
df_minority_upsampled = resample(df_minority, replace=True, n_samples=df_majority.shape[0], random_state=123)
df_upsampled = pd.concat([df_majority, df_minority_upsampled])
- Down - Sampling
df_majority = df[df.balance==0]
df_minority = df[df.balance==1]
df_majority.shape, df_minority.shape
df_majority_downsampled = resample(df_majority, replace=False, n_samples=df_minority.shape[0], random_state=123)
df_downsampled = pd.concat([df_majority_downsampled, df_minority])
data에 imbalance가 존재하는 경우, 단순 accuracy_score을 확인하는 것이 아닌 AUC, sensitivity, specificity를 확인해야 한다.
def evaluate(model, X, y):
predictions = model.predict(X)
accuracy = accuracy_score(predictions, y)
tn, fp, fn, tp = confusion_matrix(y, predictions).ravel()
sensitivity = tp / (tp+fn)
specificity = tn / (tn+fp)
auc = roc_auc_score(y, predictions)
print("AUC: ", auc)
print("Sensitivity: ", round(sensitivity,2))
print("Specificity: ", round(specificity,2))
return round(accuracy,2)
evaluate(clf_0, X, y)
- Feature 선택, 추출 및 가공 (Feature Engineering)
- Feature Selection
- 불필요한 Feature들을 제거
- Feature Extraction
- 머신러닝 알고리즘을 작동하기 위해 데이터에 대한 도메인 지식을 활용하여 특징(Feature)를 만들어내는 과정
- Feature Selection
2. Outlier(이상치) 제거
Tukey Fences 방법
Tukey Fences는 사분위 범위(IQR, interquartile range) = 데이터의 50%가 존재하는 공간을 기반으로 한다.
IQR은 세번째 사분위에서 첫번째 사분위를 뺀 값이며 이를 식으로 나타내면
IQR = Q3 - Q1이 된다.
아래 그림은 짝수와 홀수 값을 가진 데이터셋의 IQR를 구하는 과정이다.
Tukey Fences에선 아래 두가지로 아웃아이어를 판단한다.
- Q1 - (1.5 * IQR) 미만
- Q3 + (1.5 * IQR) 초과
이를 이용해 아웃라이어를 찾는 함수를 정의해보자.
Quantile
주어진 데이터를 동등한 크기로 분할하는 지점
np.quantile()의 parameter로는 데이터셋,
0과 1 사이의 숫자로 구성된 배열(데이터를 분할하고자 하는 분위 수)을 갖는다.
q1, q2, q3 = np.quantile(dataset, [0.25, 0.5, 0.75])
#Tukey Fences 사용
#이상치 제거 함수 생성
def handle_outliers(df, column):
q3 = df[column].quantile(0.75)
q1 = df[column].quantile(0.25)
iqr = q3 - q1
boundary = 1.5 * iqr
lower_bound = q1 - boundary
upper_bound = q3 + boundary
df = df[(df[column]<upper_bound)&(df[column]>lower_bound)]
return df
Quantile
주어진 데이터를 100등분하는 지점
q1, q2, q3 = np.percentile(dataset, [25, 50, 75])
def tukey_fences(data):
q1, q3 = np.percentile(data, [25, 75])
iqr = q3 - q1
lower_bound = q1 - (iqr * 1.5)
upper_bound = q3 + (iqr * 1.5)
mask = np.where((data > upper_bound) | (data < lower_bound))
return mask
np.percentile을 이용해 4분위 중 1번째와 3번째를 찾아내고
이를 이용해 iqr을 정의한다.
np.where은 해당 값을 만족하는 데이터의 위치(인덱스)를 반환한다.
Z - Score (표준 점수)
표준점수 = 데이터의 평균과 표준편차의 차이
표준점수를 구하면 변환한 데이터의 평균값이 0이 되고 표준편차는 1이된다.
표준점수 = (데이터 포인트 - 데이터의 평균) / 데이터의 표준편차
- 음의 Z-Score는 데이터 포인트가 평균보다 작다는 것
- 양의 Z-Score는 데이터 포인트가 평균보다 크다는 것
- Z-Score = 0 → 데이터 포인트가 중간(평균)이다.
- Z-Score = 1 → 데이터 포인트가 평균보다 1 표준편차가 높다.
Z-score의 값이 3보다 크거나 -3보다 작은 것은 이상치라고 판단할 수 있다.
3. 결측치 제거
ML Model은 데이터로서 Null / NaN 값을 지원하지 않음
replace()
- 원본 값을 특정 값으로 대체 : Dict를 이용해서 Key를 바꾸기 전, Value를 바꾼 후로 세팅
- 특정 Column에 적용
- Column.replace( 이전 value, column.aggregation_method)
- DataFrame 전체에 적용: 각 column별로 replace 적용
- DataFrame[colulmn_filtering].replace(이전 value, DataFrame[colulmn_filtering].aggregation_method)
- DataFrame에서 바로 aggregation을 호출할 경우 모든 column에 해당 aggregation을 적용
# Sex의 male값을 Man
replace_test_df['Sex'].replace('male', 'Man')
replace_test_df["Sex"] = replace_test_df["Sex"].replace(
{"male": "Man", "female": "Woman"}
)
replace_test_df['Cabin'] = replace_test_df['Cabin'].replace(np.nan, 'COO1')
replace_test_df["Cabin"].value_counts(dropna=False)
# COO1 687
# C23 C25 C27 4
# G6 4
# B96 B98 4
# C22 C26 3
# ...
# E34 1
# C7 1
# C54 1
# E36 1
# C148 1
# Name: Cabin, Length: 148, dtype: int64
replace_test_df["Sex"].value_counts()
# Man 577
# Woman 314
# Name: Sex, dtype: int64
fillna()
- Missing Data를 인자로 주어진 값으로 대체함
- fillna() 안의 인자로 단순 'string'이 아니라 aggregation등 여러가지 가능
4. Data Encoding
Encoding: fit_transform(Series or DataFrame)
Scaling: fit_transform(DataFrame)
Scaling에서 transform()시 scale 변환된 데이터 셋이 numpy ndarray로 반환되어 이를 DataFrame으로 변환
Data Encoding: 문자 -> 숫자
- Label Encoding
- One-Hot Encoding
머신러닝 알고리즘은 문자열 데이터 속성 입력을 받지 않으며 모든 데이터는 숫자형으로 표현되어야 함
문자형, 카테고리형 속성은 모두 숫자값으로 변환/인코딩 되어야 함
- Label Encoding
- One-Hot Encoding
- Feature 값에 유형에 따라 새로운 Feature를 추가해 고유 값에 해당하는 column에만 1을 표시하고 나머지 column에는 0을 표시하는 방법
Encoding 적용 방법
- Label Encoding
- LabelEncoder Class사용: fit()과 transform()을 이용하여 변환
- fit(): model 학습 뿐만 아니라 변환할 때에도 사용
- fit()과 transform()의 인자는 동일
- encoder.classes_: Encoding Class들을 반환
- encoder.inverse_transform(): Encoding된 값들을 원래 값으로 변환 (숫자 -> 문자)
Series 기준 Encoding
df[column] = encoder.fit_transform(df[column]) 으로 한번에 Encoding 가능
DataFrame기준 Encoding
oe = OneHotEncoder(sparse_output=False)
new_df = oe.fit_transform(df[column_list]) 으로 한번에 Encoding 가능
from sklearn.preprocessing import LabelEncoder
items = ["TV", "냉장고", "전자렌지", "컴퓨터", "선풍기", "선풍기", "믹서", "믹서"]
# LabelEncoder를 객체로 생성한 후 , fit( ) 과 transform( ) 으로 label 인코딩 수행.
encoder = LabelEncoder()
encoder.fit(items)
labels = encoder.transform(items)
print(f"인코딩 변환값: {labels}")
# 인코딩 변환값: [0 1 4 5 3 3 2 2]
print("인코딩 클래스:", encoder.classes_)
# 인코딩 클래스: ['TV' '냉장고' '믹서' '선풍기' '전자렌지' '컴퓨터']
print("디코딩 원본 값:", encoder.inverse_transform([4, 5, 2, 0, 1, 1, 3, 3]))
# 디코딩 원본 값: ['전자렌지' '컴퓨터' '믹서' 'TV' '냉장고' '냉장고' '선풍기' '선풍기']
- fit_transform(): 한번에 fit() & transform()을 수행할 수 있음
labels = encoder.fit_transform(labels)
print(labels)
# [0 1 4 5 3 3 2 2]
- One-Hot Encoding
- OneHotEncoder Class사용: fit()과 transform()을 이용하여 변환
- 인자로 2차원 ndarray 입력 필요 & reshape(-1,1)
- Sparse 배열 형태로 변환되므로 toarray()를 적용하여 다시 Dense 형탤로 변환되어야 함
- pd.get_dummies(DataFrame or Series)을 이용
- OneHotEncoder Class사용: fit()과 transform()을 이용하여 변환
- OneHotEncoder Class 사용
from sklearn.preprocessing import OneHotEncoder
import numpy as np
items = [["TV", "냉장고", "전자렌지", "컴퓨터", "선풍기", "선풍기", "믹서", "믹서"]]
# 2차원 ndarray로 변환
items = np.array(items).reshape(-1,1)
# One-Hot Encoding을 적용
oh_encoder = OneHotEncoder()
oh_encoder.fit(items)
oh_labels = oh_encoder.transform(items)
# OneHotEncoder로 변환한 결과는 Sparse 행렬(희소 행렬)이므로 이를 Dense 행렬로 변환
print("One-Hot Encoding Data")
print(oh_labels.toarray())
print("One-Hot Encoding Data Dimension")
print(oh_labels.shape)
# One-Hot Encoding Data
# [[1. 0. 0. 0. 0. 0.]
# [0. 1. 0. 0. 0. 0.]
# [0. 0. 0. 0. 1. 0.]
# [0. 0. 0. 0. 0. 1.]
# [0. 0. 0. 1. 0. 0.]
# [0. 0. 0. 1. 0. 0.]
# [0. 0. 1. 0. 0. 0.]
# [0. 0. 1. 0. 0. 0.]]
# One-Hot Encoding Data Dimension
# (8, 6)
- pd.get_dummies() 사용
- 원본 DataFrame의 각 Column 별로 One-Hot Encoding 적용
- 특정 Column에만 적용하고 싶으면 해당 Column에만 적용 후 concatenation 사용
#Converting furnishingstatus column to binary column using get_dummies
status = pd.get_dummies(housing['furnishingstatus'],drop_first=True)
housing = pd.concat([housing,status],axis=1)
housing.drop(['furnishingstatus'],axis=1,inplace=True)
pd.get_dummies(DataFrame, columns = column_list)로 한번에 처리 가능
import pandas as pd
df = pd.DataFrame({"item": ["TV", "냉장고", "전자렌지", "컴퓨터", "선풍기", "선풍기", "믹서", "믹서"]})
df
ohe_df = pd.get_dummies(df)
display(ohe_df)
X = mushroom.drop(['class'],axis=1)
y = mushroom['class']
X
X = pd.get_dummies(X)
X
5. Data Scaling
Encoding: fit_transform(Series or DataFrame)
Scaling: fit_transform(Series or DataFrame)
Scaling에서 transform()시 scale 변환된 데이터 셋이 numpy ndarray로 반환되어 이를 DataFrame으로 변환
DataFrame기준 Scaling
Scalar = StandardScalar()
new_df = scalar.fit_transform(df[column_list])
Feature Scaling
서로 다른 변수의 값 범위를 일정한 수준으로 맞추는 작업
- Standardization 표준화는 데이터의 Feature 각각이 평균이 0이고 분산이 1인 Gaussian 정규 분포를 가진 값으로 변환
- 데이터 분포의 중심을 0으로 = Zero - Centered
- $ x_{i}new = \frac {x_{i}-mean(x)} {stdev(x)}$
- Normalization 정규화는 서로 다른 Feature의 크기를 통일하기 위해 크기를 변환해주는 개념
- $ x_{i}new = \frac {x_{i}-min(x)} {max(x) - min(x)}$ : [0,1] 사이의 값으로 변환
Scikit-Learn Feature Scaling 지원
- StandardScalar(표준화): 평균이 0이고, 분산이 1인 정규 분포 형태로 변환
- MinMaxScalar(정규화): 데이터 값을 0과 1사이의 범위 값으로 변환함 (음수 값이 있으면 -1에서 1값으로 변환)
Feature Scaling
- 대표적인 선형 계열 알고리즘인 Logistic Regression이나 SVM 같은 경우 값의 scaling이 민감함
- fit()과 transform()의 인자는 동일하다
- 기본적으로 Scaling을 해줬을 때 좋아지는 부분이 생길 수 있기 때문에 가급적이면 Feature들을 전부 Scaling 작업을 해줌
- Regression, Classification: Feature Scaling을 가급적이면 해준다
- Tree는 굳이 안해도 괜찮음
- StandardScalar
- fit(): 에서 mean과 standard deviation을 미리 구함
- transform(): 에서 실제 계산을 수행
- fit_transform(): 으로 한번에 계산할 수도 있음
import pandas as pd
from sklearn.datasets import load_iris
# 붓꽃 데이터 셋을 로딩하고 DataFrame으로 변환
iris = load_iris()
iris_data = iris.data
iris_df = pd.DataFrame(iris_data, columns=iris.feature_names)
print("feature들의 평균 값")
print(iris_df.mean())
print("feature들의 분산 값")
print(iris_df.var())
# feature들의 평균 값
# sepal length (cm) 5.843333
# sepal width (cm) 3.057333
# petal length (cm) 3.758000
# petal width (cm) 1.199333
# dtype: float64
# feature들의 분산 값
# sepal length (cm) 0.685694
# sepal width (cm) 0.189979
# petal length (cm) 3.116278
# petal width (cm) 0.581006
# dtype: float64
from sklearn.preprocessing import StandardScaler
# StandardScalar 객체 생성
scalar = StandardScaler()
# StandarScalar 로 데이터셋 변환, fit()과 transform() 호출
scalar.fit(iris_df)
iris_scaled = scalar.transform(iris_df)
# transform()시 scale 변환된 데이터 셋이 numpy ndarray로 반환되어 이를 DataFrame으로 변환
iris_df_scaled = pd.DataFrame(iris_scaled, columns=iris.feature_names)
print("feature들의 평균 값:")
print(iris_df_scaled.mean())
print("\nfeature들의 분산 값:")
print(iris_df_scaled.var())
# feature들의 평균 값:
# sepal length (cm) -1.690315e-15
# sepal width (cm) -1.842970e-15
# petal length (cm) -1.698641e-15
# petal width (cm) -1.409243e-15
# dtype: float64
# feature들의 분산 값:
# sepal length (cm) 1.006711
# sepal width (cm) 1.006711
# petal length (cm) 1.006711
# petal width (cm) 1.006711
# dtype: float64
- fit_transform() 사용
from sklearn.preprocessing import StandardScaler
# StandardScalar 객체 생성
scalar = StandardScaler()
# StandarScalar 로 데이터셋 변환, fit()과 transform() 호출
iris_scaled = scalar.fit_transform(iris_df)
# transform()시 scale 변환된 데이터 셋이 numpy ndarray로 반환되어 이를 DataFrame으로 변환
iris_df_scaled = pd.DataFrame(iris_scaled, columns=iris.feature_names)
print("feature들의 평균 값:")
print(iris_df_scaled.mean())
print("\nfeature들의 분산 값:")
print(iris_df_scaled.var())
Scalar를 이용하여 학습 데이터와 테스트 데이터에 fit(), transform(), fit_transform() 적용시 유의사항
- Scalar Class의 fit(), transform()은 2차원이상 데이터만 입력이 가능하므로 1차원 ndarray는 reshape(-1,1)으로 차원 변경
Test Data Set은 Train Data set의 Scale(척도)를 따라간다
- Train Data Set과 Test Data Set을 한 번에 Scale 적용하면 좋지만 그게 불가능할 경우:
- Test Data Set에 fit()을 하거나 fit_transform()을 적용하지 말 것
- test data에 Scale 변환을 할 때는 반드시 fit()을 호출하지 않고 transform()만으로 변환해야 함
from sklearn.preprocessing import MinMaxScaler
import numpy as np
# 학습 데이터는 0 부터 10까지, 테스트 데이터는 0 부터 5까지 값을 가지는 데이터 세트로 생성
# Scaler클래스의 fit(), transform()은 2차원 이상 데이터만 가능하므로 reshape(-1, 1)로 차원 변경
train_array = np.arange(11).reshape(-1, 1)
test_array = np.arange(6).reshape(-1, 1)
# 최솟값 0, 최댓값 1로 반환하는 MinMaxScalar 객체 생성
scalar = MinMaxScaler()
# fit()하게 되면 train_array 데이터의 최솟값이 0, 최댓값이 10으로 설정
scalar.fit(train_array)
# 1/10 scale로 train_array 데이터 변환함. 원본 10 -> 1로 변환됨
train_scaled = scalar.transform(train_array)
print("원본 train_array 데이터:", np.round(train_array.reshape(-1), 2))
print("scale된 train_array 데이터:", np.round(train_scaled.reshape(-1), 2))
# 원본 train_array 데이터: [ 0 1 2 3 4 5 6 7 8 9 10]
# scale된 train_array 데이터: [0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]
- 잘못된 test_array 변환 학습
- Scale 비율이 train_data와 test_data가 동일해야함 (학습을 통해 scale을 파악한 의미가 없기 때문)
# 앞에서 생성한 MinMaxScalar에 test_array를 fit()하게 되면 원본 데이터의 최솟값이 0, 최댓값이 5로 설정됨
scalar.fit(test_array)
# 1/5 scale로 test_array 데이터 변환함. 원본 5 -> 1로 변환
test_scaled = scalar.transform(test_array)
# test _array 변환 출력
print("원본 test_array 데이터:", np.round(test_array.reshape(-1), 2))
print("Scale된 test_array 데이터:", np.round(test_scaled.reshape(-1), 2))
# 원본 test_array 데이터: [0 1 2 3 4 5]
# Scale된 test_array 데이터: [0. 0.2 0.4 0.6 0.8 1. ]
- test data에 Scale 변환을 할 때는 반드시 fit()을 호출하지 않고 transform()만으로 변환해야 함
scaler = MinMaxScaler()
scaler.fit(train_array)
train_scaled = scaler.transform(train_array)
print("원본 train_array 데이터:", np.round(train_array.reshape(-1), 2))
print("Scale된 train_array 데이터:", np.round(train_scaled.reshape(-1), 2))
# test_array에 Scale 변환을 할 때는 반드시 fit()을 호출하지 않고 transform() 만으로 변환해야 함.
test_scaled = scaler.transform(test_array)
print("\n원본 test_array 데이터:", np.round(test_array.reshape(-1), 2))
print("Scale된 test_array 데이터:", np.round(test_scaled.reshape(-1), 2))
# 원본 train_array 데이터: [ 0 1 2 3 4 5 6 7 8 9 10]
# Scale된 train_array 데이터: [0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]
# 원본 test_array 데이터: [0 1 2 3 4 5]
# Scale된 test_array 데이터: [0. 0.1 0.2 0.3 0.4 0.5]
- Test Data Set에 fit()을 하거나 fit_transform()을 적용하지 말 것
test_scaled = scaler.fit_transform(test_array)
print("\n원본 test_array 데이터:", np.round(test_array.reshape(-1), 2))
print("Scale된 test_array 데이터:", np.round(test_scaled.reshape(-1), 2))
# 원본 test_array 데이터: [0 1 2 3 4 5]
# Scale된 test_array 데이터: [0. 0.2 0.4 0.6 0.8 1. ]