본문 바로가기

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

4.7 캐글 신용카드 사기 검출

728x90

자! 이번 시간에는 이전 포스팅에서 예고했듯이

'캐글 신용카드 사기 검출'에 대한 실습을 진행해보도록 하겠습니다.

다양한 전처리 기법을 익혀보는 시간이 될 것입니다.

바로 시작하겠습니다.

 

해당 내용은 '파이썬 머신러닝 완벽 가이드'를 정리한 내용입니다.


항상 해왔듯 이 먼저 데이터셋을 확인해보도록 하겠습니다. 해당 데이터 셋은 0.172%만 사기 트랜젝션인 데이터셋입니다. 따라서 재현율이 무척 중요한 평가지표로 사용될 예정입니다.

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('./creditcard.csv')
card_df.head(3)

 

 

card_df.shape

 

(284807, 31)

 

해당 데이터셋은 31개의 속성을 가진 300000개 가까운 데이터를 가지고 있는 매우 큰 데이터 셋입니다. 우선 다양한 전처리 기법을 적용하기 전에 불필요한 'Time' 속성만 제거한 후 학습을 진행해보겠습니다.

 

get_preprocessed_df는 데이터 프레임을 가공하는 함수입니다.

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함수는 학습 데이터와 테스트 데이터를 반환하는 함수입니다. 신용카드 사기 검출 데이터셋은 이진 분류 데이터셋으로 매우 불균형한 분포를 가진 데이터셋입니다. 따라서 stratify을 통해 원래 데이터의 분포도를 따라 학습 데이터와 테스트 데이터를 분류하였습니다.

 

 

# 사전 데이터 가공 후 학습과 테스트 데이터 세트를 반환하는 함수.
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)

 

print('학습 데이터 레이블 값 비율')
print(y_train.value_counts()/y_train.shape[0] * 100)
print('테스트 데이터 레이블 값 비율')
print(y_test.value_counts()/y_test.shape[0] * 100)

 

 

분리 결과를 보면 원래 데이터의 label 분포도에 따라 잘 분리된 것을 볼 수 있습니다.

 

from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score
from sklearn.metrics import roc_auc_score

# 수정된 get_clf_eval() 함수 
def get_clf_eval(y_test, pred=None, pred_proba=None):
    confusion = confusion_matrix( y_test, pred)
    accuracy = accuracy_score(y_test , pred)
    precision = precision_score(y_test , pred)
    recall = recall_score(y_test , pred)
    f1 = f1_score(y_test,pred)
    # ROC-AUC 추가 
    roc_auc = roc_auc_score(y_test, pred_proba)
    print('오차 행렬')
    print(confusion)
    # ROC-AUC print 추가
    print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f},\
    F1: {3:.4f}, AUC:{4:.4f}'.format(accuracy, precision, recall, f1, roc_auc))

 

우선 로지스틱 회귀를 사용하여 예측해보도록 하겠습니다. 로지스틱 회귀에 대한 자세한 내용은 다음장 5장에서 자세히 다루도록 하겠습니다.

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)

 

 

결과를 보면 재현율이 60%로 매우 낮은 거을 볼 수 있습니다.

 

모델 학습 재사용을 용이하게 하기 위해 모듈로 나타내도록 하겠습니다.

# 인자로 사이킷런의 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)

 

이번에는 LightGBM을 통해 데이터를 예측해보겠습니다.

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)

 

재현율이 75%로 로지스틱 회귀에 비해서는 높은 결과를 보여줍니다.

 

1. 데이터 분포도 변환

   1.1 정규화

데이터 전처리를 하지 않고 모델을 학습한 결과 그리 좋은 성능을 보여주지 못했습니다. 지금부터는 데이터 전처리 기법을 하나씩 적용해보며 모델의 예측 성능을 향상해겠습니다. 첫 번째로 '데이터 분포도 변환'입니다. 

 

우선 사기 트랜젝션을 검출하는 데 있어서 'Amount'는 매우 중요한 속성으로 작용할 가능성이 큽니다. 따라서 Amount의 데이터의 분포를 살펴보겠습니다.

import seaborn as sns

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

 

 

위의 그래프를 보면 알 수 있듯이 대부분의 신용 카드 사용 금액은 1000불 이하입니다. 하지만 27000불까지 매우 긴 꼬리를 가진 데이터 분포 형태를 보입니다. 뒷 장에서 배우겠지만 로지스틱 회귀를 포함한 대부분의 선형 회귀에서는 중요한 피처들이 정규분포의 형태를 따르는 것은 매우 중요합니다. 따라서 StandardScaler클래스를 사용하여 정규 분포 형태로 변환한 후 예측 성능을 확인해보겠습니다.

 

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

 

# 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)

 

 

위의 결과를 통해 보면 알 수 있듯이 성능이 그리 향상되지 않은 것을 볼 수 있습니다.

   1.2 로그 변환

이번에는 로그 변환을 수행해보겠습니다. 로그 변환은 데이터 분포도 왜곡이 심한 경우 적용할 수 있는 매우 중요한 기법입니다. 로그 변환은 log1p()를 통해 손쉽게 가능합니다.

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

 

여기서 로그 변환이란 매우 작은 값에 1을 더 한 후 로그를 씌워주는 것을 말합니다. 왜 1을 더해주는 걸까요? 그 이유는 아래 식의 결과를 통해 확인 가능합니다.

# log1p 와 expm1 설명 
import numpy as np

print(1e-1000 == 0.0)

print(np.log(1e-1000))

print(np.log(1e-1000 + 1))
print(np.log1p(1e-1000))

 

True
-inf
0.0
0.0

 

1e-1000은 매우 작은 값으로 0에 근사하는 값입니다. 하지만 이에 로그를 씌우면 -inf로 값이 반환됩니다. 이를 방지하기 위해 로그 변환에서는 1을 더해줍니다. 그럼 그 0으로 결괏값이 반환되는 것을 확인할 수 있습니다. 

 

이제 로그 변환을 한 후 모델의 성능 변화를 확인해보겠습니다. 

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

print('### 로지스틱 회귀 예측 성능 ###')
get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

print('### LightGBM 예측 성능 ###')
get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

 

 

위의 결과를 보면 알 수 있듯이 두 모델 모두 성능(재현율)이 아주 조금씩 좋아진 것을 볼 수 있습니다. 하지만 아직 성능이 많이 아쉽죠...

2. 이상치 데이터 제거

이번에는 이상치 데이터를 제거하도록 하겠습니다. 이상치 데이터(Outlier)는 전체 데이터의 패턴에서 벗어난 이상 값을 가진 데이터로, 아웃라이어라고도 불립니다. 이상치는 머신러닝 모델의 성능에 영향을 주는 일은 매우 빈번하게 발생합니다. 이상치는 박스 플롯(Box Plot)이라고 불리는 방식을 통해 쉽게 구할 수 있습니다. 

박스 플롯은 위의 그림과 같이 데이터를 값이 높은 순으로 나열한 후 전체 데이터의 0%, 25%, 50%, 75%, 100%에 해당하는 값을 표시해줍니다.

이후 위의 그림에 나와있는 방법으로 최댓값과 최솟값을 정한 후 이를 벗어난 값을 이상치로 나타냅니다. 

 

이제 본격적으로 이상치를 제거해보도록 하겠습니다. 이상치를 제거할 때 어떤 속성에 대하여 이상치를 제거할 것인지 정하는 것은 매우 중요합니다. 결정 값과 상관성이 없는 속성의 이상치를 제거하면 큰 성능 향상이 나타나지 않기 때문입니다. 따라서 결정 값과 속성들의 상관관계를 corr()를 통해 구해보고 이를 heatmap으로 시각화해보겠습니다.

 

import seaborn as sns

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

 

위 히트맵을 보면 알 수 있듯이 v14, v17이 결정값에 많은 영향을 주는 것을 볼 수 있습니다. 따라서 이번 실습에서는 'v14'에 대해서 이상치 제거를 해보겠습니다. 

 

이상치 제거하는 방법은 간단합니다. 위의 박스 플롯에 나타난 이상치 식을 적용하여 이상치에 해당하는 데이터의 인덱스를 구하고 이를 데이터 프레임에서 제거하면 됩니다.

 

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[(fraud < lowest_val) | (fraud > highest_val)].index
    
    return outlier_index

 

이상치에 해당하는 인덱스는 다음과 같습니다.

outlier_index = get_outlier(df=card_df, column='V14', weight=1.5)
print('이상치 데이터 인덱스:', outlier_index)
이상치 데이터 인덱스: Int64Index([8296, 8615, 9035, 9252], dtype='int64')

 

이제 이상치를 제거한 후 모델 학습을 진행해보겠습니다.

# get_processed_df( )를 로그 변환 후 V14 피처의 이상치 데이터를 삭제하는 로직으로 변경. 
def get_preprocessed_df(df=None):
    df_copy = df.copy()
    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

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

print('### 로지스틱 회귀 예측 성능 ###')
get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

print('### LightGBM 예측 성능 ###')
get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

두 모델 모두 6~7%가량 성능이 매우 향상되었음을 알 수 있습니다. 역시 이상치 제거의 효과 매우 대단하군요!!

3. SMOTE 오버 샘플링

오버 샘플링이란 이상 데이터와 같이 적은 데이터를 증식하여 학습을 위한 충분한 데이터를 확보하는 방법입니다. 

오버 샘플링의 대표적인 방법으로 SMOTE가 있습니다. SMOTE란 아래와 같이 적은 데이터 세트에 있는 개별 데이터들의 K 최근접 이웃을 찾아서 해당 데이터와 k개의 이웃들 사이의 차이를 이용하여 새로운 데이터를 생성하는 방법입니다.

SMOTE 적용 전/후 데이터 양을 비교해보겠습니다.

from imblearn.over_sampling import SMOTE

smote = SMOTE(random_state=0)
X_train_over, y_train_over = smote.fit_sample(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
dtype: int64

위의 결과를 보면 알 수 있듯이 학습 데이터의 양이 약 2배 증가하였으며, 1과 0의 개수 또한 동일해진 것을 알 수 있습니다.

 

하지만 SMOTE 오버샘플링은 반드시 학습 데이터에만 적용해야 합니다. 따라서 아래 y_train의 데이터 분포를 보면 여전히 불균형한 데이터 분포를 보임을 알 수 있습니다. 

y_train.value_counts()

 

0    199020
1       342
Name: Class, dtype: int64

 

이제 SMOTE 데이터에 대해 로지스틱 회귀를 통해 예측해보도록 하겠습니다. 

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)

 

 

예측 결과를 보면 재현율은 약 92%로 굉장히 상승한 것에 비해 정밀도가 5% 정도로 낮아진 것을 볼 수 있습니다. 이렇게 정밀도가 낮은 경우 실제 상황에 모델을 적용할 수 없습니다.

이렇게 정밀도가 떨어진 이유는 모델이 class=1인 데이터를 지나치게 많이 학습하면서 class=1로 지나치게 많이 예측했기 때문입니다. 아래 percision_recall_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] )

 

이제 마지막으로 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)

위의 결과를 통해 알 수 있듯이 재현율이 84%로 향상된 것을 볼 수 있습니다. 하지만 정밀도가 3% p 정도 감소한 것을 볼 수 있습니다. 이처럼 SMOTE를 적용하면 일반적으로 재현율이 높아지고 정밀도가 낮아집니다.


이상으로 신용카드 사기 검출에 대한 실습을 마무리하도록 하겠습니다.

이번 실습에서 '로그 변환', '이상치 제거', 'SMOTE 오버 샘플링'에 대해서 배웠습니다.

세 가지 기법 모두 불균형한 데이터 분포를 가지는 데이터셋에서 자주 사용되기 때문에 

꼭 명확하게 익히셨으면 좋겠습니다. 

이제 분류에 대한 것도 어느덧 마무리를 향하고 있으니 조금만 힘냅시다!!

반응형