Go together

If you want to go fast, go alone. If you want to go far, go together.

파이썬 머신 러닝 완벽 가이드

분류 실습 - 캐글 신용카드 사기 검출

NowChan 2021. 11. 14. 11:40

배운 내용

  1. XGBoost
  2. imbalanced-learn
  3. DataFrame.copy()
  4. Series.value_counts()
  5. LogisticRegression
  6. LGBMClassifier, boost_from_average=False
  7. np.log1p()
  8. DataFrame.corr() & sns.heatmap()
  9. DataFrame.index
  10. SMOTE.fit_resample()
  11. precision_recall_curve()

 

분류 실습 - 캐글 신용카드 사기 검출

데이터는 https://www.kaggle.com/mlg-ulb/creditcardfraud 에서 내려 받을 수 있습니다. 해당 데이터 세트의 레이블인 Class 속성은 매우 불균형한 분포를 가지고 있습니다. 0은 정상적인 신용카드 트랜잭션 데이터, 1은 신용카드 사기 트랜잭션을 의미합니다. 사기 트랜잭션은 전체 데이터의 약 0.172% 밖에 안됩니다. 

 

일반적으로 사기 검출(Fraud Detection)이나 이상 검출(Anomaly Detection)은 레이블 값이 극도로 불균형한 분포를 가지기 쉽습니다. 사기와 같은 이상 현상은 비율이 극히 적을 수 밖에 없기 때문입니다.

 

 

언더 샘플링과 오버 샘플링의 이해

레이블이 불균형한 분포를 가진 데이터를 학습시킬 때 예측 성능의 문제가 발생할 수 있는데, 이는 이상 레이블 데이터 건수가 매우 적어서 제대로 다양한 유형을 학습하지 못하고, 데이터가 넘처 나는 정상 레이블로 치우친 학습을 하는 경향이 있기 때문입니다.

 

지도 학습에서 극도로 불균형한 레이블 값 분포로 인한 문제점을 해결하기 위해 적절한 학습 데이터를 확보하는 방안으로 오버 샘플링(Oversampling)언더 샘플링(Undersampling) 방법이 있습니다.

 

언더 샘플링많은 레이블을 가진 데이터 세트를 적은 레이블을 가진 데이터 세트 수준으로 감소하는 방식입니다. 정상 레이블을 가진 데이터가 10,000건, 이상 레이블을 가진 데이터가 100건 있으면 정상 레이블 데이터는 100건으로 줄여 버리는 방식입니다. 이는 너무 많은 정상 레이블 데이터를 감소시키기 때문에 정상 레이블의 경우 오히려 제대로 된 학습을 수행할 수 없다는 단점이 있습니다.

 

오버 샘플링적은 레이블을 가진 데이터 세트를 많은 레이블을 가진 데이터 세트 수준으로 증식시키는 방식입니다. 오버 샘플링 방식이 예측 성능상 더 유리한 경우가 많아 주로 사용됩니다. 동일한 데이터를 단순히 증식하는 방법은 과적합이 되기 때문에 의미가 없으므로 원본 데이터의 피처 값들을 아주 약간만 변경하여 증식합니다. 대표적으로 SMOTE(Synthetic Minority Over-sampling Technique) 방법이 있습니다.

 

SMOTE는 적은 데이터 세트에 있는 개별 데이터들의 K 최근접 이웃(K Nearest Neighbor)을 찾아서 이 데이터와 K개 이웃들의 차이를 일정 값으로 만들어서 기존 데이터와 약간 차이가 나는 새로운 데이터들을 생성하는 방식입니다.

 

SMOTE 원리

 

SMOTE를 구현한 대표적인 파이썬 패키지는 imbalanced-learn입니다. 아나콘다 프롬프트를 관리자 권한으로 실행하고 아래 명령어를 입력합니다.

conda install -C conda-forge imbalanced-learn

 

 

데이터 일차 가공 및 모델 학습/예측/평가

이제 신용카드 사기 검출 모델을 생성하겠습니다.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
%matplotlib inline

card_df = pd.read_csv('/content/drive/MyDrive/military/fraud detection/creditcard.csv')
card_df.head(3)
'''
결과1
'''

결과1

Time 피처의 경우 데이터 생성 관련한 작업용 속성으로서 큰 의미가 없어서 삭제하고, Amount 피처는 신용카드 트랙잭션 금액을 의미하며, Class는 레이블로 0이 정상, 1이 사기 트랜잭션입니다.

card_df.info()
'''
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 284807 entries, 0 to 284806
Data columns (total 31 columns):
 #   Column  Non-Null Count   Dtype  
---  ------  --------------   -----  
 0   Time    284807 non-null  float64
 1   V1      284807 non-null  float64
 2   V2      284807 non-null  float64
 3   V3      284807 non-null  float64
 4   V4      284807 non-null  float64
 5   V5      284807 non-null  float64
 6   V6      284807 non-null  float64
 7   V7      284807 non-null  float64
 8   V8      284807 non-null  float64
 9   V9      284807 non-null  float64
 10  V10     284807 non-null  float64
 11  V11     284807 non-null  float64
 12  V12     284807 non-null  float64
 13  V13     284807 non-null  float64
 14  V14     284807 non-null  float64
 15  V15     284807 non-null  float64
 16  V16     284807 non-null  float64
 17  V17     284807 non-null  float64
 18  V18     284807 non-null  float64
 19  V19     284807 non-null  float64
 20  V20     284807 non-null  float64
 21  V21     284807 non-null  float64
 22  V22     284807 non-null  float64
 23  V23     284807 non-null  float64
 24  V24     284807 non-null  float64
 25  V25     284807 non-null  float64
 26  V26     284807 non-null  float64
 27  V27     284807 non-null  float64
 28  V28     284807 non-null  float64
 29  Amount  284807 non-null  float64
 30  Class   284807 non-null  int64  
dtypes: float64(30), int64(1)
memory usage: 67.4 MB
'''

데이터에 NaN과 같은 결측치(Missing Value) 값은 없으며, Class는 int형, 나머지는 float형 피처입니다.

 

다양한 데이터 사전 가공을 수행하는 함수 get_preprocessed_df( ) 함수와 데이터 가공 후 학습/테스트 데이터 세트를 반환하는 get_train_test_df( ) 함수를 생성하겠습니다. 

 

DataFrame.copy()

먼저, get_preprocessed_df( ) 함수는 불필요한 Time 피처를 삭제하는 것으로부터 시작하겠습니다.

from sklearn.model_selection import train_test_split

# 인자로 입력받은 DataFrame을 복사한 뒤 Time 칼럼만 삭제하고 복사된 DataFrame 반환
def get_preprocessed_df(df=None):
  df_copy = df.copy()
  df_copy.drop('Time', axis=1, inplace=True)
  return df_copy

get_train_test_dataset( )으로 테스트 데이터 세트를 전체의 30%인 Stratified 방식으로 추출해 학습/테스트 데이터 세트의 레이블 값 분포도를 서로 동일하게 만듭니다.

# 사전 데이터 가공 후 학습과 테스트 데이터 세트를 반환하는 함수.
def get_train_test_dataset(df=None):
  # 인자로 입력된 DataFrame의 사전 데이터 가공이 완료된 복사 DataFrame 반환
  df_copy = get_preprocessed_df(df)
  # DataFrame의 맨 마지막 칼럼이 레이블, 나머지는 피처들
  X_features = df_copy.iloc[:, :-1]
  y_target = df_copy.iloc[:, -1]
  # train_test_split()으로 학습과 테스트 데이터 분할. stratify=y_target으로 Stratified 기반 분할
  X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.3, \
                                                      random_state=0, stratify=y_target)
  # 학습과 테스트 데이터 세트 반환
  return X_train, X_test, y_train, y_test

X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)

 

 

Series.value_counts()

생성된 학습 데이터 세트와 테스트 데이터 세트의 레이블 값 비율이 잘 분할됐는지 확인해보겠습니다.

print('학습 데이터 레이블 값 비율')
print(y_train.value_counts()/y_train.shape[0]*100)
print('테스트 데이터 레이블 값 비율')
print(y_test.value_counts()/y_test.shape[0]*100)
'''
학습 데이터 레이블 값 비율
0    99.827451
1     0.172549
Name: Class, dtype: float64
테스트 데이터 레이블 값 비율
0    99.826785
1     0.173215
Name: Class, dtype: float64
'''

로지스틱 회귀와 LightGBM 기반의 모델이 데이터 가공을 수행하면서 예측 성능이 어떻게 변하는지 살펴보기 위해 가공 전 결과를 도출해보도록 하겠습니다. 예측 성능 평가는 3장에서 생성한 get_clf_eval( ) 함수를 다시 사용하겠습니다.

 

LogisticRegression

from sklearn.linear_model import LogisticRegression

lr_clf = LogisticRegression()
lr_clf.fit(X_train, y_train)
lr_pred = lr_clf.predict(X_test)
lr_pred_proba = lr_clf.predict_proba(X_test)[:, 1]

# 3장에서 사용한 get_clf_eval() 함수를 이용해 평가 수행.
get_clf_eval(y_test, lr_pred, lr_pred_proba)
'''
오차 행렬
[[85280    15]
 [   55    93]]
정확도: 0.9992, 정밀도: 0.8611, 재현율: 0.6284, F1:0.7266,   AUC:0.9568
'''

테스트 데이터로 측정 시 재현율(Recall)이 0.6081, ROC-AUC가 0.9709입니다.

 

앞으로 반복적으로 모델을 변경해 학습/예측/평가할 것이므로 이를 위한 별도의 함수를 생성하겠습니다. get_model_train_eval( )는 인자로 사이킷런의 Estimator 객체와 학습/테스트 데이터 세트를 받아 예측/평가를 수행하는 역할을 하는 함수입니다.

# 인자로 사이킷런의 Estimator객체와 학습/테스트 데이터 세트를 입력 받아서 학습/예측/평가 수행.
def get_model_train_eval(model, ftr_train=None, ftr_test=None, tgt_train=None, tgt_test=None):
  model.fit(ftr_train, tgt_train)
  pred = model.predict(ftr_test)
  pred_proba = model.predict_proba(ftr_test)[:,1]
  get_clf_eval(tgt_test, pred, pred_proba)

 

 

LGBMClassifier

먼저, 본 데이터 세트는 극도로 불균형한 레이블 값 분포도를 가지고 있어서 LGBMClassifier 객체 생성시 boost_from_average=False로 파라미터를 설정해야 합니다.

from lightgbm import LGBMClassifier

lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1,\
                          boost_from_average=False)
get_model_train_eval(lgbm_clf, ftr_train = X_train, ftr_test = X_test, \
                        tgt_train = y_train, tgt_test = y_test)
'''
오차 행렬
[[85289     6]
 [   36   112]]
정확도: 0.9995, 정밀도: 0.9492, 재현율: 0.7568, F1:0.8421,   AUC:0.9797
'''

LogisticRegression보다 재현율, ROC-AUC가 높습니다.

 

데이터 분포도 변환 후 모델 학습/예측/평가

이번에는 왜곡된 분포도를 가지는 데이터를 재가공한 뒤 모델을 다시 테스트해보겠습니다. 로지스틱 회귀는 선형 모델이므로 중요 피처 값들이 정규 분포 형태를 유지하는 것을 선호합니다. Amount 피처는 신용카드 사용 금액으로 정상/사기 트랜잭션을 결정하는 매우 중요한 속성일 가능성이 높습니다. Amount 피처의 분포도를 확인해보겠습니다.

import seaborn as sns
plt.figure(figsize=(8, 4))
plt.xticks(range(0, 30000, 1000), rotation=60)
sns.distplot(card_df['Amount'])
'''
결과2
'''

결과2

카드 사용 금액(Amount) 사용 금액이 1,000불 이하인 데이터가 대부분이며, 27,000불까지 드물지만 사용한 경우가 있어 꼬리가 긴 분포 곡선을 가지고 있습니다. Amount를 정규 분포로 변환해 로지스틱 회귀의 예측 성능을 측정해보겠습니다.

from sklearn.preprocessing import StandardScaler
# 사이킷런의 StandardScaler를 이용해 정규 분포 형태로 Amount 피처값 변환하는 로직으로 수정.
def get_preprocessed_df(df=None):
  df_copy = df.copy()
  scaler = StandardScaler()
  amount_n = scaler.fit_transform(df_copy['Amount'].values.reshape(-1, 1))
  # 변환된 Amount를 Amount_Scaled로 피처명 변경 후 DataFrame 맨 앞 칼럼으로 입력
  df_copy.insert(0, 'Amount_Scaled', amount_n)
  # 기존 Time, Amount 피처 삭제
  df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
  return df_copy

수정된 get_preprocessed_df( ) > get_train_test_dataset() > get_model_train_eval( )을 통해 로지스틱 회귀, LightGBM 모델을 학습/예측/평가해보겠습니다. 

# Amount를 정규 분포 형태로 변환 후 로지스틱 회귀 및 LightGBM 수행.
X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)

print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression()
get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train,\
                     tgt_test=y_test)
print('### LightGBM 예측 성능 ###')
lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False)
get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train,\
                     tgt_test=y_test)
                     
'''
### 로지스틱 회귀 예측 성능 ###
오차 행렬
[[85281    14]
 [   58    90]]
정확도: 0.9992, 정밀도: 0.8654, 재현율: 0.6081, F1:0.7143,   AUC:0.9702
### LightGBM 예측 성능 ###
오차 행렬
[[85289     6]
 [   36   112]]
정확도: 0.9995, 정밀도: 0.9492, 재현율: 0.7568, F1:0.8421,   AUC:0.9773
'''

Amount로 피처값 변환 후 테스트 데이터 세트에 적용한 로지스틱 회귀는 향상되지 않았고, LightGBM의 경우 재현율이 조금 상승했습니다.

 

np. log1p(): 로그 변환

이번엔 StandardScaler가 아닌 로그 변환을 수행해보겠습니다. 로그 변환은 데이터 분포도가 심하게 왜곡됐을 경우 작용하는 중요한 기법입니다. 원래 값을 log값으로 변환해 원래 큰 값을 상대적으로 작은 값으로 변환해서 왜곡을 상당히 낮춰줍니다. 로그 변환은 넘파이의 log1p( ) 함수를 이용해 변환 가능합니다. get_preprocessed_df( )를 다음과 같이 로그 변환 로직으로 변경합니다.

def get_preprocessed_df(df=None):
  df_copy = df.copy()
  # 넘파이의 log1p()를 이용해 Amount를 로그 변환
  amount_n = np.log1p(df_copy['Amount'])
  df_copy.insert(0, 'Amount_Scaled', amount_n)
  df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
  return df_copy

다시 로지스틱 회귀와 LightGBM 모델에 적용해 예측 성능을 확인해보겠습니다.

# Amount를 정규 분포 형태로 변환 후 로지스틱 회귀 및 LightGBM 수행.
X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)

print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression()
get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train,\
                     tgt_test=y_test)
print('### LightGBM 예측 성능 ###')
lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False)
get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train,\
                     tgt_test=y_test)
'''
### 로지스틱 회귀 예측 성능 ###
오차 행렬
[[85283    12]
 [   59    89]]
정확도: 0.9992, 정밀도: 0.8812, 재현율: 0.6014, F1:0.7149,   AUC:0.9727
### LightGBM 예측 성능 ###
오차 행렬
[[85290     5]
 [   35   113]]
정확도: 0.9995, 정밀도: 0.9576, 재현율: 0.7635, F1:0.8496,   AUC:0.9786
'''

로지스틱 모델의 경우 정밀도, AUC에서 성능이 약간 향상됐습니다. LightGBM의 재현율이 조금 상승했습니다.

 

 

이상치 데이터 제거 후 모델 학습/예측/평가

이상치 데이터(Outlier)는 전체 데이터의 패턴에서 벗어난 이상 값을 가진 데이터입니다. 이상치는 머신러닝 모델의 성능에 영향을 주는 경우가 많기에 제거해야합니다. 흔히 IQR(Inter Quantile Range) 방식을 적용합니다.

 

IQR은 사분위(Quantile) 값의 편차를 이용하는 기법으로 흔히, 박스 플롯(Box Plot) 방식으로 시각화할 수 있습니다.

IQR 정의

사분위는 전체 데이터를 값이 높은 순으로 정렬하고, 이를 1/4(25%) 씩으로 구간을 분할하는 것을 말합니다. 0~25%까지를 Q1, 25~50%까지를 Q2, 50~75%까지를 Q3, 75~100%까지를 Q4라고 하고, Q3 - Q1을 IQR이라 합니다. 보통 이상치 데이터를 검출하는 방식은 IQR*1.5를 곱해서 생성된 범위를 최댓값과 최솟값으로 결정해서, 이 범위를 벗어나는 데이터를 이상치로 간주하는 것입니다. 1/4 분위수(Q1)에 IQR*1.5를 빼서 최솟값으로 가정하고, 3/4 분위수(Q3)에 IQR*1.5를 더해서 최댓값으로 가정합니다.

이상치를 나누는 경계

IQR을 이용해 이상치 데이터를 제거해보겠습니다. 많은 피처가 있을 경우 이들 중 결정값(레이블)과 가장 상관성이 높은 피처들을 위주로 이상치를 검출하는 것이 좋습니다. 모든 피처의 이상치를 검출하는 것은 시간이 오래 걸리기 때문입니다.

 

DataFrame.corr() & sns.heatmap()

DataFrame.corr()을 이용해 각 피처별로 상관도를 구할 수 있습니다. 각 피처별로 상관도를 구하고 시본의 heatmap을 통해 시각화해보겠습니다. cmap'RdBu'로 설정해서, 양의 상관관계가 높을 수록 진한 파란색에 가깝고 음의 상관관계가 높을 수록 진한 빨간색에 가깝습니다.

import seaborn as sns

plt.figure(figsize=(9, 9))
corr = card_df.corr()
sns.heatmap(corr, cmap='RdBu')
'''
결과1
'''

결과1, 상관관계 히트맵

 

DataFrame.index

Class와 음의 상관관계가 가장 높은 피처는 V17과 V14입니다. V14에 IQR을 이용해서 이상치를 제거해보겠습니다.

import numpy as np

def get_outlier(df=None, column=None, weight=1.5):
  # fraud에 해당하는 column 데이터만 추출, 1/4 분위와 3/4 분위 지점을 np.percentile로 구함.
  fraud = df[df['Class']==1][column]
  quantile_25 = np.percentile(fraud.values, 25)
  quantile_75 = np.percentile(fraud.values, 75)
  # IQR을 구하고, IQR에 1.5를 곱해 최댓값과 최솟값 지점 구함.
  iqr = quantile_75 - quantile_25
  iqr_weight = iqr * weight
  lowest_val = quantile_25 - iqr_weight
  highest_val = quantile_75 + iqr_weight
  # 최댓값보다 크거나, 최솟값보다 작은 값을 이상치 데이터로 설정하고 DataFrame index 반환.
  outlier_index = fraud[(lowest_val > fraud ) | (highest_val < fraud )].index
  return outlier_index
  
# V14 칼럼에서 이상치 데이터 찾기
outlier_index = get_outlier(df=card_df, column='V14', weight=1.5)
print('이상치 데이터 인덱스:', outlier_index)
'''
이상치 데이터 인덱스: Int64Index([8296, 8615, 9035, 9252], dtype='int64')
'''

Class가 1인 데이터만 찾아서 이상치를 구하는 것은 1인 데이터를 분류하는 것이 중요하기 때문인 것 같다. get_outlier( )를 이용해 이상치를 추출하고 삭제하는 로직을 get_processed_df( ) 함수에 추가해서 가공한 데이터 세트를 LogisticRegression, LightGBM 모델에 다시 적용해보겠습니다.

def get_preprocessed_df(df=None):
  df_copy = df.copy()
  # 넘파이의 log1p()를 이용해 Amount를 로그 변환
  amount_n = np.log1p(df_copy['Amount'])
  df_copy.insert(0, 'Amount_Scaled', amount_n)
  df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
  # 이상치 데이터 삭제하는 로직 추가
  outlier_index = get_outlier(df = df_copy, column='V14', weight=1.5)
  df_copy.drop(outlier_index, axis=0, inplace=True)
  return df_copy
# Amount를 정규 분포 형태로 변환 후 로지스틱 회귀 및 LightGBM 수행.
from sklearn.linear_model import LogisticRegression
from lightgbm import LGBMClassifier
X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)

print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression()
get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train,\
                     tgt_test=y_test)
print('### LightGBM 예측 성능 ###')
lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False)
get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train,\
                     tgt_test=y_test)
'''
### 로지스틱 회귀 예측 성능 ###
오차 행렬
[[85281    14]
 [   48    98]]
정확도: 0.9993, 정밀도: 0.8750, 재현율: 0.6712, F1:0.7597,   AUC:0.9743
### LightGBM 예측 성능 ###
오차 행렬
[[85291     4]
 [   25   121]]
정확도: 0.9997, 정밀도: 0.9680, 재현율: 0.8288, F1:0.8930,   AUC:0.9831
'''

이상치 데이터를 제거한 뒤 로지스틱 회귀는 재현율이 크게 상승했고, LightGBM는 재현율과 AUC 수치가 매우 크게 상승했습니다.

 

 

SMOTE 오버 샘플링 적용 후 모델 학습/예측/평가 

SMOTE 기법으로 오버 샘플링을 적용한 뒤 로지스틱 회귀와 LightGBM 모델의 예측 성능을 평가해보겠습니다. SMOTE는 앞에서 설치한 imbalanced-learn 패키지의 SMOTE 클래스를 이용해 간단하게 구현이 가능합니다. SMOTE를 적용할 때 학습 데이터 세트만 오버 샘플링을 해야합니다. 테스트 데이터나 검증 데이터를 오버샘플링하면 원본 데이터에 대해 검증할 수 없기 때문입니다.

 

SMOTE.fit_resample()

from imblearn.over_sampling import SMOTE

smote = SMOTE(random_state=0)
X_train_over, y_train_over = smote.fit_resample(X_train, y_train)
print('SMOTE 적용 전 학습용 피처/레이블 데이터 세트:', X_train.shape, y_train.shape)
print('SMOTE 적용 후 학습용 피처/레이블 데이터 세트:', X_train_over.shape, y_train_over.shape)
print('SMOTE 적용 후 레이블 값 분포:\n', pd.Series(y_train_over).value_counts())
'''
SMOTE 적용 전 학습용 피처/레이블 데이터 세트: (199362, 29) (199362,)
SMOTE 적용 후 학습용 피처/레이블 데이터 세트: (398040, 29) (398040,)
SMOTE 적용 후 레이블 값 분포:
1    199020
0    199020
Name: Class, dtype: int64
'''

SMOTE 적용 전 학습 데이트 세트가 199,362 건 있었는데, 적용 후 2배 가까이 많아지고, 레이블 1과 0의 개수가 동일하게 199,020 건으로 생성됐습니다. LogisticRegression 모델을 학습한 뒤 성능을 평가해보겠습니다.

lr_clf = LogisticRegression()
# ftr_train과 tgt_train 인자값이 SMOTE 증식된 x_train_over과 y_train_over로 변경됨에 유의
get_model_train_eval(lr_clf, ftr_train=X_train_over, ftr_test = X_test, tgt_train=y_train_over,\
                     tgt_test = y_test)
'''
오차 행렬
[[82937  2358]
 [   11   135]]
정확도: 0.9723, 정밀도: 0.0542, 재현율: 0.9247, F1:0.1023,   AUC:0.9737
'''

로지스틱 회귀 모델의 경우 SMOTE로 오버샘플링된 데이터를 학습할 경우 재현율이 92.47%로 크게 증가하지만, 정밀도가 5.4%로 급격히 저하됩니다. 로지스틱 회귀 모델이 오버 샘플링으로 인해 실제 원본 데이터의 유형 보다 너무 많은 Class=1 데이터를 학습하면서 실제 테스트 데이터 세트에서 지나치게 Class=1로 적용해 정밀도가 급격히 떨어지게 된 것입니다. 

 

분류 결정 임곗값에 따른 정밀도와 재현율 곡선을 통해 어떤 문제가 발생했었는지 확인해보겠습니다. 3장에서 사용한 precision_recall_curve_plot( ) 함수를 이용합니다.

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from sklearn.metrics import precision_recall_curve
%matplotlib inline

def precision_recall_curve_plot(y_test, pred_proba_c1):
  # threshold ndarray와 이 threshold에 따른 정밀도, 재현율 ndarray 추출.
  precisions, recalls, thresholds = precision_recall_curve(y_test, pred_proba_c1)

  # X축을 threshold값으로, Y축은 정밀도, 재현율 값으로 각각 Plot 수행. 정밀도는 점선으로 표시
  plt.figure(figsize=(8, 6))
  threshold_boundary = thresholds.shape[0]
  plt.plot(thresholds, precisions[0:threshold_boundary], linestyle='--', label='precision')
  plt.plot(thresholds, recalls[0:threshold_boundary], label='recall')

  # threshold 값 X축의 Scale을 0.1단위로 변경
  start, end = plt.xlim()
  plt.xticks(np.round(np.arange(start, end, 0.1), 2))

  # x축, y축 label과 legend, 그리고 grid 설정
  plt.xlabel('Threshold value'); plt.ylabel('Precision and Recall value')
  plt.legend(); plt.grid()
  plt.show()
precision_recall_curve_plot(y_test, lr_clf.predict_proba(X_test)[:, 1])
'''
결과2
'''

결과2

임계값이 0.99 이하에서는 재현율이 좋다가 0.99에서는 대폭 떨어지고, 임계값이 0.95 이하에서는 정밀도가 나쁘다가 0.95 이후에서 급격히 상승합니다. 분류 결정 임계값을 조정하더라고 임계값의 민감도가 너무 심해서 올바른 재현율/정밀도 성능을 얻을 수 없으므로 로지스틱 회귀 모델의 경우 SMOTE를 적용하면 안됩니다.

 

이번엔 LightGBM 모델을 오버 샘플링된 데이터 세트로 학습/예측/평가를 수행하겠습니다.

lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False)
get_model_train_eval(lgbm_clf, ftr_train=X_train_over, ftr_test=X_test,\
                     tgt_train=y_train_over, tgt_test=y_test)
'''
오차 행렬
[[85286     9]
 [   22   124]]
정확도: 0.9996, 정밀도: 0.9323, 재현율: 0.8493, F1:0.8889,   AUC:0.9789
'''

재현율이 약간 상승하고 정밀도가 약간 낮아졌습니다. SMOTE를 적용하면 재현율은 높아지나, 정밀도는 낮아지는 것이 일반적입니다. 좋은 SMOTE 패키지일 수록 재현율 증가율은 높이고 정밀도 감소율은 낮출 수 있도록 효과적으로 데이터를 증식합니다.

 

지금까지 다양한 방법으로 데이터를 가공하면서 LogisticRegression과 LightGBM을 적용한 결과를 정리했습니다.

 

데이터 가공 유형 머신러닝 알고리즘 평가 지표
정밀도 재현율 ROC-AUC
원본 데이터 가공 없음 로지스틱 회귀 0.8661 0.6284 0.9568
LightGBM 0.9492 0.7568 0.9797
데이터 로그 변환 로지스틱 회귀 0.8812 0.6014 0.9727
LightGBM 0.9576 0.7635 0.9786
이상치 데이터 제거 로지스틱 회귀 0.8750 0.6712 0.9743
LightGBM 0.9680 0.8288 0.9831
SMOTE 오버 샘플링 로지스틱 회귀 0.0542 0.9247 0.9737
LightGBM 0.9323 0.8493 0.9789

 

출처: 파이썬 머신러닝 완벽 가이드(권철민)

사진 출처:

https://www.scribbr.com/statistics/interquartile-range/

https://joonable.tistory.com/27

https://hong-yp-ml-records.tistory.com/15

 

resampling으로 imbalanced data(불균형 데이터 문제) 해결하기

Resampling으로 imbalanced data(불균형 데이터 문제) 해결하기 imbalanced data : 데이터 내 각각의 class들이 차지하는 데이터의 비율이 균일하지 않고 한쪽으로 치우친 데이터 major class : dataset 내에서 상..

joonable.tistory.com

 

'파이썬 머신 러닝 완벽 가이드' 카테고리의 다른 글

회귀  (0) 2021.11.19
분류 - 스태킹 앙상블  (0) 2021.11.16
분류 실습 - 캐글 산탄데르 고객 만족 예측  (0) 2021.11.14
분류  (0) 2021.11.06
피마 인디언 당뇨병 예측  (0) 2021.11.05