LITTLE BY LITTLE

[5] 파이썬 머신러닝 완벽가이드 - 사이킷런 데이터 전처리, 타이타닉 예제, 정리 본문

데이터 분석/파이썬 머신러닝 완벽가이드

[5] 파이썬 머신러닝 완벽가이드 - 사이킷런 데이터 전처리, 타이타닉 예제, 정리

위나 2022. 8. 10. 11:04

*목차

  1. 파이썬 기반의 머신러닝과 생태계 이해
    1. 머신러닝의 개념
    2. 주요 패키지
    3. 넘파이
    4. 판다스 (데이터 핸들링) (39p)
    5. 정리
  2. 사이킷런으로 시작하는 머신러닝(87p)
    1. 사이킷런 소개
    2. 첫번째 머신러닝 만들어보기 - 붓꽃 품종 예측
    3. 사이킷런 기반 프레임워크 익히기 ( fit(), predict() ..)
    4. Model selection 모듈 소개 (교차검증, GridSerachCV..)
    5. 데이터 전처리 
    6. 사이킷런으로 수행하는 타이타닉 생존자 예측
    7. 정리

2-5. 데이터 전처리

Null 값 처리하기

Null 값이얼마 되지 않는다면 피처의 평균값으로 간단히 대체 가능하나, 많을 경우 피처를 드롭, 단순히 피처의 평균값을 대체시 예측 왜곡이 심하다면 더 정밀한 대체값을 선정


문자열 값 변환하기

2-1. 레이블 인코딩 Label Encoder 객체로 생성 -> fit()과 transform()을 호출해 레이블 인코딩 수행

from sklearn.preprocessing import LabelEncoder

items = ['TV', '냉장고', '전자레인지', '컴퓨터', '선풍기', '선풍기', '선풍기', '믹서', '믹서']

# Label Encoder을 객체로 생성한 후, fit() 과 transform()으로 레이블 인코딩 수행
encoder = LabelEncoder()
encoder.fit(items)
labels = encoder.transform(items)
print('인코딩 변환값:',labels)
# 인코딩 변환값 : [0 1 4 5 3 3 2 2]

- 특정 알고리즘 (ex. 선형 회귀)에서는 레이블 인코딩된 값에 가중치를 부여하여 예측성능이 떨어질 수 있기 때문에 주의해야한다. 트리 계열 알고리즘에서는 사용해도 무관하다.
- LabelEncoder 객체의 classes_ 속성값으로 어떤 숫자값으로 인코딩되었는지 확인 : encoder.classes_
- LabelEncoder 객체의 inverse_tranform()을 통해 인코딩된 값을 다시 디코딩할 수 있음 : encoder.inverses_transoform([4,5,2,0,1,1,3,3\))


2-2. 원-핫 인코딩 OneHotEncoder 클래스로 변환가능하나, 1)문자열->숫자형 변환 먼저 2) 입력값으로 2차원 데이터가 필요 하다는 점에 주의하자.
- 피처 값의 유형에 따라 새로운 피처를 추가해 고유값에 해당하는 칼럼에만 1을 표시, 나머지 칼럼에는 0을표시하는 방식

# 먼저 숫자값으로 변환하기 위해 LabelEncoder로 변환
encoder = LabelEncoder()
encoder.fit(items)
labels = encoder.transform(items)

# 2차원 데이터로 변환
labels = labels.reshape(-1,1)

#원핫 인코딩
oh_encoder = OneHotEncoder()
oh_encoder.fit(labels)
oh_labels = oh_encoder.tarnsform(labels)
print(oh_labels.toarray())
print(oh_labels.shape)

원-핫 인코딩 완료

- 8개의 레코드와 1개의 칼럼을 가진 원본데이터가 8개의 레코드와 6개의 칼럼을 가진 데이터로 변환되었다.
* 판다스에는 원-핫 인코딩을 더 쉽게 하는 api가 있다. pd.get_dummies() 이용하기 : 이 경우에는 문자열 카테고리 값을 숫자형으로 반환할 필요 없이 바로 변환할 수 있다.

# pd.get_dummies로 원-핫인코딩 하기
import pandas as pd
df = pd.DataFrame({'items':['TV','냉장고','전자레인지','컴퓨터','선풍기','선풍기','믹서','믹서']})
pd.get.dummies(df)

pd.get_dummies로 원-핫인코딩 완료


피처 스케일링과 정규화

  1. 피처 스케일링 : 서로 다른 변수의 값 범위를 일정수준으로 맞춰주는 작업 - 표준화와 정규화가 있다.
    1. 표준화는 데이터 피처각각이 평균이 0이고, 분산이 1인 가우시안 정규분포를 가진 값으로 변환하는 것, 평균을 뺀 값을 표준편차로 나눈 값으로 계산
    2. 정규화는 서로 다른 피처의 크기를 통일하기 위해 "크기를 변환"해주는 개념이다.
      1. 예를들어 피처A는 거리를 나타내는 변수로서 값이 0~100km로 주어지고, 피처B는 금액을 나타내는 속성으로 값이 0 ~ 100,000,000,000원으로 주어진다면 이 변수를 모두 동일한 크기 단위로 비교하기 위해 값을 모두 최소0~최대1의 값으로 변환하는 것이다. 즉, 개별데이터 크기를 모두 같은 단위로 변경
      2. 원래값에서 최솟값을 뺀 값을 최댓값과 최솟값의 차이로 나눈 값으로 변환
      3. 사이킷런 전처리에서 제공하는 Normalzer 모듈과의 차이점은 이 모듈은 선형대수의 정규화 개념이 적용되고, 개별 벡터의 크기를 맞추기위해 변환하는 것을 의미한다는 점이다. 즉, 3개의 피처가 있다고 한다면 원래 값에서 세 개의 피처의 i번째 피처값에 해당하는 크기를 합한 값으로 나누어준다. 이러한 정규화를 '벡터 정규화'라 한다. (일반적인 의미의 표준화와 정규화는 피처 스케일링으로 통칭)
  2. StandScalers - 표준화를 지원하는 클래스
    1. 정규분포를 가질 수 있도록 데이터를 변환하는 것은 몇몇 알고리즘에서는 매우 중요하다.
      1. 사이킷런에서 구현한 RBF 커널을 이용하는 SVM(서포트 벡터 머신)
      2. 선형 회귀
      3. 로지스틱 회귀 에서는 데이터가 가우시안 정규분포를 가지고있다고 가정하고 구현되었기 때문에 사전에 표준화를 적용하는 것은 예측 성능 향상에 중요한 요소
# StandScalder로 정규화 실시
from sklearn.datasets import load_iris
import pandas as pd
iris = load_iris()
iris_data = iris.data
iris_df = pd.DataFrame(data=iris_data, columns=iris.feature_names)

print('feature들의 평균 값')
print(iris_df.mean())
print('\nfeature 들의 분산 값')
print(iris_df.var())

1. 붓꽃 데이터를 불러와서 데이터프레임으로 변환하고, 평균과 분산을 구했다. (정규화 이전 이후를 비교하기 위해 구한 것, 이후 평균은 0에 가까운 값으로, 분산은 1에 가까운 값으로 변환된다.)
2. 이제 StandardScaler을 이용해 각 피처를 한번에 표준화해 변환하자.
3. 여기서도 StandSclaer 객체를 생성한 후에, fit()과 transform() 메소드에 변환 대상 피처 데이터 세트를 입력하고 호출한다.
* transform()을 호출할 때 변환된 데이터 셋이 넘파이의 ndarrary이기 때문에, dataframe으로 변환해서 확인하자.

# StandScaler 객체 생성
scaler = StandardScaler()
# StandSacler으로 데이터셋 변환, fit()과 transform()호출
scaler.fit(iris_df)
iris_scaled = scaler.transofmr(iris_df)

# tramsform()시 스케일 변환된 데이터셋이 Numpy ndarray로 반환되어 이를 dataframe으로 변환해서 보기
iris_df_scaled = pd.DataFrame(data=iris_scaled, columns=iris.feature_names)
print('feature 들의 평균 값')
print(iris_df_scaled.mean()) 
print('\nfeature 들의 분산 값’) 
print(iris_df_scaled.var())

3. MinMaxScaler - StandScaler과 비슷하게 스케일링 작업 수행
- 차이점은 음수 값이 있으면 -1과 1값으로 변환한다는 점
- 데이터분포가 가우시안 분포가 아닐 경우에 MinMaxScaler 적용

# MinMaxScaler 객체 생성
scaler = MinMaxScaler()
# fit(), transform() 호출
scaler.fit(iris_df)
iris_scaled = scaler.transform(iris_df)

#dataframe으로 변환해서 보기
iris_df_scaled_pd.DataFrame(data=iris_scaled, columns=iris.feature_names)

- 결과를 보면 최댓값은 1로, 최솟값은 0으로 잘 변환되었다.


학습 데이터와 테스트 데이터의 스케일링 변환시 유의점

  1. StandScaler나 MinMaxScaler과 같은 스케일러 객체를 통해 변환시 fit(), transform(), fit_transform() 메소드를 이용하였다.
    1. fit()은 데이터 변환을 위한 기준 정보 설정 (ex. 데이터셋의 최대/최솟값 설정 등)
    2. transform()은 이렇게 설정된 정보를 통해 데이터를 반환
  2. 주의해야할 점은 Scaler객체를 이용해 학습 데이터셋으로 fit()과 transform()을 적용하면 테스트데이터셋으로는 다시 fit()을 수행하지 않고, 학습 데이터로 fit()을 수행한 결과를 이용해 transform()변환을 적용해야한다는 점이다. (테스트 데이터는 훈련과정에서는 그 어떠한 처리에서도 쓰이지 않음)
    1. 그래서 test_array에 scale 변환을 할 때에는, 반드시 fit()을 호출하지 않고, 학습 데이터로 이미 fit()된 Scaler 객체를 이용해서 transform()만으로 변환해야한다. 따라서 fit_transform()메소드는 사용할 수 없다.
    2. 따라서 학습과 테스트 데이터셋으로 분리하기 이전에 먼저 전체 데이터 셋에 스케일링을 적용한 뒤 학습과 테스트 데이터셋으로 분리하는 것이 더 바람직하다.

2-6. 사이킷런으로 수행하는 타이타닉 생존자 예측

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

titanic_df = pd.read_csv('./titanic_train.csv')
titanic_df.head(3)

print(\n ### 학습 데이터 정보 ### \n')
print(titanic_df.info())
# 상단에 Range Index로 전체 로우 수 확인, 칼럼명과 개수 확인, 데이터타입 확인, Null값 확인

#결측치 처리
titanic_df['Age'].fillna(titanic_df['Age'].mean(), inplace=True)
titanic_df['Cabin'].fillna('N',inplace=True)
titanic_df['Embarked'].fillna('N',inplace=True)
print('데이터 셋 Null 개수' , titanic_df.isnull().sum().sum())

#문자열 피처 처리를 위해 "분포" 확인
print(' Sex값 분포 :\n', titanic_df['Sex'].value_counts())
print('\n Cabin값 분포 :\n', titanic_df['Cabin'].value_counts())
print('\n Embared 값 분포 :'\n', titanic_df['Embarked'].value_counts())
# 고르게 분포되어있는 Sex, Embarked 칼럼은 문제 없음, Cabin의 경우 최댓값 687, 최소4로 문제 있음
# Cabin의 경우, 선실 번호 중 선실 등급을 나타내는 첫번째 알파벳이 중요해보인다. 일등석에 투숙한
# 사람이 3등석에 투숙한 사람보다 더 생존률(타깃변수에 영향)이 높을 것이기 때문

# Cabin 속성의 앞 문자를 추출하자
titanic_df['Cabin'] = titanic_df['Cabin'].str[:1]
print(titanic_df['Cabin'].head(3))


(EDA) 예측 수행 전 데이터를 탐색해보자.

1. 여성과 아이들, 노약자가 일반적으로 먼저 구조 대상이고, 부자나 유명인이 다음 구조 대상이었을 것이다.

# 성별이 생존률에 영향을 미쳤는지 확인해보자.(성별에 따른 생존률 카운트)
titanic_df.groupby(['Sex','Survived'])['Survived'].count()

#시각화로 더 자세히 확인해보자.
sns.barplot(x='Sex',y='Survived',data=titanic_df) # 여성이 훨씬 생존률이 높다.


2. 부자와 가난한 사람의 생존률이 다른지 확인해보자. 객실등급 -> 부 측정 수단으로 적합

sns.barplot(x='Pclass',y='Survived',hue='Sex',data=titanic_df) # 성별도 함께 보고자 hue로 추가함
# 여성의 경우 부에 따른 생존률 차이가 크지 않으나, 남성의 경우 1등석 생존확률이 월등히 높다.

3. 나이에 따른 생존확률을 확인해보자. Age의 경우 값 종류가 많기 때문에 범위별로 분류해 카테고리 값을 할당하자.

def get_category(age):
	cat = ''
    if age<= -1: cat = 'Unknown'
    elif age <=5: cat = 'Baby'
    elift age <=12: cat = 'Child'
    elif age <=18: cat = 'Teenager'
    elif age <=25: cat = 'Student'
    elift age <=35: cat = 'Young Adult'
    ellift age <=60: cat = 'Adult'
    else : cat = 'Elderly'
    
    return cat
    
# 막대그래프 figure 크기 크게
plt.figure(figsize=(10,6))
# x축 값을 순차적으로 정렬
group_names = ['Unknown', 'Baby', 'Child', 'Teenager', 'Student', 'Young Adult', 'Adult', 'Elderly']

# lambda 식에 위에서 생성한 get_category() 함수를 반환값으로 지정,
# get_category(x)는 입력값으로 Age칼럼을 받아서 해당하는 cat 반환
titanic_df['Age_cat'] = titanic_df['Age'].apply(lambda x : get_category(x))
sns.barplot(s='Age_cat', y='Survived', hue='Sex', data=titanic_df, order=group_names)
titanic_df.drop('Age_cat', axis=1, inplace=True)

- 여자 Baby 생존률 높음
- 여자 Child 생존률 낮음
- 여자 Elderly 생존률 매우 높음

→ 분석 결과 Sex, Age, PClass 등이 중요하게 생존을 좌우하는 피처임을 확인함.

남아있는 문자열 카테고리 피처를 숫자형 카테고리 피처로 변환 (인코딩)

# 레이블 인코딩 - LabelEncoder
# encode_feautres() 함수를 새로 생성해 한번에 변환하자
from sklearn import preprocessing
def encode_features(dataDF):
	features = ['Cabin', 'Sex', 'Embarked']
    for feature in features:
    	le = preprocessing.LabelEncoder()
        le = le.fit(dataDF[feature])
        dataDF[feature] = le.transform(dataDF[feature])
        
        return datDF

titanic_df = encode_features(titanic_df)
titanic_df.head()
# sex, cabin, embarked 속성이 숫자형으로 바뀌었다.

전처리 내용을 정리해서 함수로 만들어 쉽게 재사용할 수 있도록 만들자.

# Null처리 함수 fillna
def fillna(df):
    df['Age'].fillna(df['Age'].mean(), inplace=True)
    df['Cabin'].fillna('N', inplace=True)
    df['Fare'].fillna(0,inplace=True)
    return df
    
# 머신러닝 알고리짐에 불필요한 속성 제거 함수 drop_features
def drop_features(df):
    df.drop(['PassengerId','Name','Ticket'],axis=1, inplace=True)
    return df

# 레이블 인코딩 함수 format_features 
def format_features(df):
  df['Cabin'] = df['Cabin'].str[:1]
  features = ['Cabin', 'Sex', 'Embarked']
  for feature in features:
      le = LabelEncoder()
      le = le.fit(df[feature])
      df[feature] = le.transform(df[feature])
  return df
    
# 모든 함수 호출 함수 transform_features(df):
def transform_features(df):
    df = fillna(df)
    df = drop_features(df)
    df = format_features(df)
    return df

원본 데이터 재로딩, 레이블인 Survived 속성만 별도 분리해서 결정값 데이터셋으로 만들고, Survived를 드롭해 피처 데이터셋을 만들고, 위의 함수 적용해서 데이터를 가공하자.

# 원본 데이터 재로딩, 피처 데이터 셋과 레이블 데이터셋 추출
titanic_df = pd.read-csv('./titanic_train.csv')
y_titanic_df = titanic_df['Survived']
X_titanic_df = titanic-df.drop('Survived',axis=1)

X_titanic_df = transform_features(X_titanic_df)

train_test_split() API를 이용해 테스트 데이터를 추출하자. (20% 추출)

# 테스트 데이터셋 추출
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_titanic_df, y_titanic_df, 
	test_size=0.2, random_state=11)

결정 트리(Decision Tree Classifier), 랜덤 포레스트(RandomForestClassifier), 로지스틱 회귀(LogisticRegression)로 생존자를 예측해보자.

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

# Classifier 클래스 생성
dt_clf = DecisionTreeClassifier(random_state=11)
rf_clf = RandomForestClassifier(random_state=11)
lr_clf = LogisticRegression()

# 결정트리 학습-예측-평가
dt_clf.fit(X_train,y_train)
dt_pred = dt_clf.predict(X_test)
print('DecisionTreeClassifier 정확도: {0:4f}'.format(accuracy_score(y_test,dt_pred)))

# 랜덤포레스트 학습-예측-평가
rf_clf.fit(X_train,y_train)
rf_pred = rf_clf.predict(X_train)
print('RandomForestClassifier 정확도:{0:.4f}'.format(accuracy_score(y_test, rf_pred)))

# 로지스틱회귀 학습-예측-평가
lr_clf.fit(X_train,y_train)
lr_pred = lr_clf.predict(X_train)
print('LogisticRegression 정확도:{0:.4f}'.format(accuracy_score(y_test,lr_pred)))

# Output
# DeccisionTreeClassifier 정확도:0.7877
# RandomForestClassifier 정확도: 0.8324
# LogisticRegression 정확도:0.8659

- 아직 최적화 작업도 수행하지 않았고, 데이터 양도 충분하지 않기 때문에 어떤 알고리즘이 가장 성능이 좋다고 평가할 수 없다.

교차 검증으로 결정 트리모델을 좀 더 평가해보자. - 1. KFOLD 클래스 사용

# KFold 클래스를 이용해서 교차검증을 수행하자. k=5
def exce_kfold(clf, folds=5):
	kfold = KFold(n_splits=folds) # 폴드 수5개 만큼 예측 결과 저장을 위한 리스트 객체 생성
    scores = []

# KFold 교차 검증 수행
for iter_count, (train_index, test_index) in enumerate(kfold.split(X_titanic_df)):
	X_train, X_test = X_titanic_df.values[train_index], X_titanic_df.values[test_index]
    y_train, y_test = y_titanic_df.values[train_index], y_titanic_df.values[test_index]
    # 교차 검증별로 학습,검증 데이터를 가리키는 index를 생성하였다.
    # Classifier 학습-예측-정확도 계산
    clf.fit(X_train, y_train)
    predictions = clf.predict(X_test)
    accuracy = accuracy_score(y_test, predictions)
    scores.append(accuracy)
    print("교차 검증 {0} 정확도 : {1:.4f}".format(iter_count, accuracy))
    
# 5개 fold에서 평균 정확도 계산
mean_score = np.mean(scores)
print("평균 정확도: {0:4f}".format(mean_score))

#exec_kfold 호출
exec_kfold(df_clf, folds=5)

# output
# 교차 검증 0 정확도 : 0.7542
# 교차 검증 1 정확도 : 0.7809
# 교차 검증 2 정확도 : 0.7865
# 교차 검증 3 정확도 : 0.7697
# 교차 검증 4 정확도 : 0.8202
# 평균 정확도 : 0.7823

교차 검증으로 결정 트리모델을 더 평가 - 2. cross_val_score() 사용

from sklearn.model_selection import cross_val_score

scores = cross_val_score(dt_clf, X_titanic_df, y_titanic_df, cv=5)
for iter_count, accuracy in enumerate(scores):
	print("교차 검증 {0} 정확도: {1:.4f}.format(iter_count, accuracy))
    
print("평균 정확도: {0:.4f}".format(np.mean(scores)))
# output
# 교차 검증 0 정확도 : 0.7430
# 교차 검증 1 정확도 : 0.7765
# 교차 검증 2 정확도 : 0.7809
# 교차 검증 3 정확도 : 0.7753
# 교차 검증 4 정확도 : 0.8418
# 평균 정확도 : 0.7835

- KFold와 평균 정확도가 다른 이유는 cross_val_score()가 Stratified KFold를 이용해 폴드세트를 분할하기 때문이다.

교차 검증으로 결정 트리모델을 더 평가 - 3. GridSearchCV 사용

from sklearn.model_selection import GridSearchCV

parameters = {'max_depth':[2,3,5,10],
	'min_samples_split':[2,3,5], 'min_samples_leaf':[1,5,7]}
grid_dclf = GridSearchCV(dt_clf, param_grid = parameters, scoring = 'accuracy', cv=5)
grid_dclf.fit(X_train,y_train)

print('GridSerachCV 최적 하이퍼 파라미터 :', grid_dclf.best_params_)
print('GridSearchCV 최고 정확도 : {0:.4f}'.format(grid_dclf.best_score_))
best_dclf = grid_dclf.best_estimator_

# GridSearchCV의 최적 하이퍼 파라미터로 학습된 Estimator로 예측 및 평가 수행
dpredictions = best_dclf.predict(X_test)
accuracy = accuracy_score(y_test, dpredictions)
print('테스트 세트에서의 DecisionTreeClassifier 정확도: {0:.4f}'.format(accuracy))

#output
# GridSearchCV 최적 하이퍼 파라미너 : {'max_depth':3, 'min_samples_leaf':1, 'min_samples_split':2}
# GridSearchCV 최고 정확도 : 0.7992
# 테스트 세트에서의 DecisionTreeClassifier 정확도 : 0.8715

- 최적화된 하이퍼 파라미터 max_depth =3, min_samples_leaf =1, min_sampls_split=2로 DecisionTreeClassifier를 학습시킨 뒤, 예측 정확도가 87.15%로 향상되었다.
- 하이퍼 파라미터 변경 전보다 약 8%이상이 증가하였는데, 일반적으로는 하이퍼 파라미터를 튜닝하여도 이정도까지 증가하기 어렵다. 테스트용 데이터셋이 작아서 예측 성능이 많이 좋아진 것


2-7. 정리

 

1. 머신러닝 애플리케이션은 1) 데이터의 가공 및 변환 과정의 전처리 작업, 2) 데이터를 학습 데이터와 테스트 데이터로 분리하는 데이터 세트 분리 작업을 거친 후에 3) 학습 데이터를 기반으로 머신러닝 알고리즘을 적용해 모델을 학습시킨다. 그리고 4) 학습된 모델을 기반으로 테스트 데이터에 대한 예측을 수행하고, 5) 예측된 결괏값을 실제 결괏값과 비교해 머신러닝 모델에 대한 평가를 수행하는 방식으로 구성된다.

2. 데이터 전처리 작업은 1) 오류 데이터의 보정 2) 결손값 처리 등 다양한 데이터 클렌징 작업 3) 레이블 인코딩이나 원-핫 인코딩과 같은 인코딩 작업, 4) 그리고 데이터의 스케일링/정규화 작업 등 머신러닝 알고리즘이 최적으로 수행될 수 있게 데이터를 사전처리 하는 것

3. 머신러닝 모델은 학습데이터로 학습한 뒤, 반드시 별도의 테스트 데이터셋으로 평가되어야한다.

4. 또한 테스트 데이터의 건수 부족이나 고정된 테스트 데이터셋을 이용한 반복적 모델의 학습과 평가는 해당 테스트 데이터셋에만 치우친 빈약한 머신러닝 모델을 만들 가능성이 높다.(과적합)

5. 이를 해결하기 위해 학습 데이터셋을 학습 데이터셋과 검증 데이터셋으로 구성된 여러개의 폴드 셋으로 분리해
교차 검증을 수행한다. KFold, StratifiedKFold, cross_val_score()을 이용할 수 있으며, 최적의 하라미터를 교차검증을 통해 추출하기위해 GridSearchCV를 이용한다.

Comments