오늘 포스팅에는 유명한 Kaggle 신용카드 사기 감지 데이터셋(Credit Card Fraud Detection)을 가지고 데이터 전처리/분석/머신러닝을 하는 과정을 기록할 것입니다. 데이터 EDA를 진행하고 적절한 전처리를 해준 후 머신러닝 모델링을 수행하고 성능 지표를 비교하는 일련의 과정을 전부 담을 예정인데요, 의식의 흐름대로 작성할 예정이라 중간 중간 Tmi도 많고 삽질하는 내용까지도 필터링 없이 기록할 것임을 미리 알려드립니다.

 

1. 데이터 불러오기, 컬럼/결측치/데이터 타입 확인

https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud/data

 

Credit Card Fraud Detection

Anonymized credit card transactions labeled as fraudulent or genuine

www.kaggle.com

!kaggle datasets download -d mlg-ulb/creditcardfraud
!unzip "/content/creditcardfraud.zip"

먼저 캐글 신용카드 사기 감지 데이터셋을 다운로드받아서 가지고 옵니다. 저는 API Command를 복사하여 실행하고 코랩에 다운받아진 파일을 unzip해주는 형식으로 간단히 데이터를 불러왔습니다. 이렇게 하면 파일을 직접 다운로드해서 가져오는것보다 훨씬 빠릅니다.

 준비된 데이터프레임에 총 31개의 column, 284807개의 row를 확인했습니다.

card.info()

info 메소드를 통해 컬럼의 데이터타입과 결측치 여부를 확인했는데, 다행히 모든 컬럼에 결측치 없이 데이터가 잘 들어가 있었고, 정수타입의 Class 컬럼을 제외한 모든 열은 float64 타입임을 확인했습니다. 컬럼명에 들어있는 V1~V28의 경우 어떤 속성인지 알 수는 없고, Amount는 해당 row의 결제 금액, Class는 정상/사기 이진 분류(binary classification) 결과(label) 컬럼에 해당합니다.

만약 object 타입의 컬럼이 있었다면 라벨 인코딩(Label Encoding)이나 원핫 인코딩(One-Hot Encoding) 등의 작업을 통해 값을 숫자로 변환하는 작업이 필요합니다. 다행히 모두 숫자로 이루어져 있기 때문에 따로 인코딩 작업은 들어가지 않아도 될 것 같습니다 :-)

또, NaN 등의 결측치가 있는 경우 적절한 근거를 가지고 대표값으로 결측치를 채워 넣거나 해당 row를 삭제하는 등의 작업을 통해 결측치를 제거해 주어야 합니다. 다행히도 우리의 친절한 캐글 신용카드 데이터셋은 우리에게 그런 노가다를 요구하지 않고 있습니다... (흡족)

 

2. 결제 금액 분포도 그래프로 확인

결제 금액을 나타내는 Amount 컬럼의 분포도를 그래프로 확인해 보겠습니다.

import seaborn as sns
import matplotlib.pyplot as plt

plt.style.use('fivethirtyeight')
plt.figure(figsize = (10, 4))
plt.xticks(range(0, 40000, 1000), rotation = 45)
sns.histplot(card['Amount'], bins = 100, kde = True)
plt.show()

Tmi지만 저는 matplotlib의 테마 중에서 fivethirtyeight를 가장 좋아합니다. 왜인지는 모르겠습니다. 그래서 웬만하면 그래프를 그릴 때 fivethirtyeight으로 테마를 변경한 다음에 그래프를 뽑는 편입니다. 이런 각박한 작업 속에서도 내 취향이란 걸 반영할 수 있다는 게 전 재밌더라구요^_^...;;;

plt.figure(figsize = (8, 4))
plt.xlim(0, 1000)
plt.xticks(range(0, 1000, 50), rotation = 45)
sns.histplot(card['Amount'], bins = 500, kde = True)
plt.show()

다시 본론으로 돌아와서... 0부터 약 500달러 미만의 결제 금액이 차지하는 비율이 압도적으로 많은 것을 확인할 수 있었습니다. 이 그래프를 확인한 후 저는

과하게 치우친 Amount 컬럼의 값을 로그변환하여 보정해 주면 모델의 성능 지표가 상승할 것이다

라는 첫 번째 가설을 세우게 됩니다.

 

3. Amount 컬럼 박스 플롯 그리기

결제 금액 분포도를 살펴본 이후 저는 Amount 컬럼의 박스 플롯을 그려서 이상치를 확인해 봐야겠다는 생각이 들었는데요.

sns.boxplot(data = card, y = 'Amount')

쩝... 네... 첫 번째 삽질 보여드립니다. 방금 위에서 데이터가 쏠려 있음을 확인해놓고 박스플롯이 예쁘게 그려지리라고 생각한 제가 좀 바보같네요. 결제 금액의 박스 플롯은 큰 의미가 없는 것 같으니 다음으로 데이터 프레임의 상관 계수를 확인해 보도록 하겠습니다.

 

4. 상관 계수 시각화하기

card.corr()

corr() 메소드를 이용해서 모든 컬럼 사이의 상관계수를 나타내어 보았습니다. 한 눈에 들어오질 않으니 일단 쉐입을 확인해 볼게요...

card_corr = card.corr()
card_corr.shape
# (31, 31)

31개의 컬럼 사이의 상관계수가 (31, 31) 정사각 쉐입의 데이터프레임으로 이쁘게 반환되었습니다. 저는 저렇게 e어쩌고로 나타내진 숫자값들은 봐도 봐도 적응이 안되더라구요. 어쨌든 이 상태로는 전체 분포를 한 눈에 알아보기 힘들기 때문에 seaborn의 heatmap을 이용해서 그래프로 시각화를 해 보도록 하겠습니다.

두 번째 Tmi... 저는 또 이렇게 색상이 중요한 그래프를 그릴 때 괜히 컬러 팔레트 고르느라 1-2분을 더 낭비하는 것을 좋아합니다.. 역시 각박한 일상 속에서 찾아내는 저만의 소소한 작은 행복입니다 (^_^*) 이번에는 따뜻한 색감의 노랑-브릭 계열의 팔레트를 골라 보았습니다.

plt.figure(figsize = (8, 8))
sns.heatmap(card_corr, cmap = "YlOrBr")
plt.tight_layout()
plt.savefig("corr.png")

그래프를 큼직하게 뽑아서 괜히 savefig를 이용해 png파일로 다운로드까지 해 보았습니다. 이것도 자주 안하면 계속 까먹어요. ㅋ

상관관계 히트맵에서 양의 상관관계가 높을수록 색깔이 진한 갈색에 가깝고, 음의 상관관계가 높을수록 연한 노란색에 가깝습니다. 그래프의 변두리에 희끗희끗하게 보이는 밝은 부분들이 음의 상관관계가 높은 지점입니다. 

이 때, 레이블에 해당하는 Class컬럼과 가장 낮은 상관관계를 보이는 컬럼은 V14와 V17인것으로 확인되는데요.

이렇게 레이블과 관계 없는 컬럼의 이상치를 제거하면 모델의 성능 지표가 상승할 것이다

라는 두 번째 가설을 세우게 됩니다. 

 

5. [before] baseline 모델링

(5-1) train, test 구분하고 레이블 비율 확인해 보기

card.drop('Time', axis = 1, inplace = True)

먼저 큰 의미가 없는 Time 컬럼은 drop을 통해 삭제를 해 주고 시작하겠습니다.

def train_test(df):
    card_copy = card.copy()
    X_features = card_copy.iloc[:, :-1] # label에 해당하는 마지막 'Class'컬럼 제외
    y_label = card_copy.iloc[:, -1]     # label에 해당하는 마지막 'Class'컬럼만

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X_features,
                                                        y_label,
                                                        test_size = 0.2,
                                                        random_state = 1004,
                                                        stratify = y_label)
    
    return X_train, X_test, y_train, y_test

X_train, X_test, y_train, y_test = train_test(card)

card 데이터프레임의 마지막 'Class' 컬럼을 기준으로 feature과 label을 구분했구요. 싸이킷런의 train_test_split을 통해 8:2의 비율로 train과 test를 분할했습니다. 3번째 Tmi...인데.. 저는 random_state로 항상 1004(천사)를 사용합니다. ^^;;;;;

len(X_train), len(X_test), len(y_train), len(y_test)
# (227845, 56962, 227845, 56962)

이렇게 나눠졌구요. 그럼 데이터에서 레이블 비율(0과 1의 비율)이 어떻게 되는지 살펴볼까요?

y_train.value_counts(normalize = True).apply(lambda x: str(round(x * 100,4)) + '%')
y_test.value_counts(normalize = True).apply(lambda x: str(round(x * 100,4)) + '%')

와우! 사기에 해당하는 1번 레이블이 약 0.17%에 해당하는 극소수로 확인되었습니다. 하긴, 사기 거래가 2~30%씩 차지하고 있으면 그것도 말이 안되겠네요. 저는 이렇게 뭐든 시각화하면서 가지고 있는 데이터와 친숙해지는 작업이 꼭 필요하다고 생각합니다. 그렇지 않으면 의미 없는 숫자놀음에 그치게 된다고 생각해요,,, 어쨌거나 저쨌거나 이 비율 확인 후 저는 

평가 지표(metrics)로 Accuracy가 아닌 다른 지표들을 전부 다 확인할 필요가 있음

이라는 결론을 내렸고, 다양한 평가 지표를 모두 확인해 보기로 결정했습니다.

(5-2) 모델 학습 후 예측 성능 평가

def train_eval(model, f_tr = None, f_test= None, t_tr= None, t_test= None):
    # 모델 학습
    model.fit(f_tr, t_tr)

    # 예측 
    pred = model.predict(f_test)
    pred_proba = model.predict_proba(f_test)[:, 1]

    # 평가 (1) confusion matrix 출력
    from sklearn.metrics import confusion_matrix
    confusion = confusion_matrix(t_test, pred)
    print("----- confusion matrix -----")
    print(confusion)
    
    # 평가 (2) accuracy / precision / recall / f1 / roc_score 출력
    from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
    accuracy = accuracy_score(t_test, pred)
    precision = precision_score(t_test, pred)
    recall = recall_score(t_test, pred)
    f1 = f1_score(t_test, pred)
    roc_auc = roc_auc_score(t_test, pred_proba)
    print("----- evaluation score -----")
    print(f"accuracy : {accuracy:.4f}")
    print(f"precision : {precision:.4f}")
    print(f"recall : {recall:.4f}")
    print(f"f1 : {f1:.4f}")
    print(f"ROC_SCORE : {roc_auc:.4f}")

 

먼저 baseline 모델을 학습시키고 예측을 수행한 다음 confusion matrix와 여러 평가 지표를 출력하는 함수를 작성했습니다.

from lightgbm import LGBMClassifier
from xgboost import XGBClassifier

lgbm = LGBMClassifier(n_estimators = 1000,
                      num_leaves = 64,
                      n_jobs = -1,
                      boost_from_average = False)

xgb = XGBClassifier(n_estimators = 1000,
                    learning_rate = 0.05,
                    max_depth = 3, 
                    eval_metric = 'logloss')

이후 XGBM, LightGBM 모델 2가지를 학습하고 예측 평가를 수행했습니다.

train_eval(lgbm, X_train, X_test, y_train, y_test)
train_eval(xgb, X_train, X_test, y_train, y_test)

(좌) LGBM / (우) XGB

결과값은 위와 같이 나왔는데요. Accuracy는 대체로 동일하고, precision과 recall, f1은 LightBGM 모델이 더 높았고, roc_auc 점수는 XGB 모델이 더 높게 측정되었습니다.

 

6. [after] 가설 2가지 적용하기

이 번엔 데이터 분석을 통해 세웠던 2가지의 가설을 검증해보는 작업을 수행해 보겠습니다.

레이블과 관계 없는 컬럼의 이상치를 제거하고 Amount 컬럼의 값을 로그변환하여 보정한다면 모델의 성능 지표를 상승시킬 수 있을 것이다

(6-1) 이상치 찾아내기

def find_outliers(df, column):
    # 해당 컬럼의 사기 데이터 series 선언
    frauds = df[df['Class'] == 1][column]
    
    # IQR 구하기
    import numpy as np
    Q1 = np.percentile(frauds.values, 25)
    Q3 = np.percentile(frauds.values, 75)
    IQR = Q3 - Q1

    # 1QR에 1.5를 곱해서 최댓값, 최솟값 구하기
    lowest_value = Q1 - IQR * 1.5
    highest_value = Q3 + IQR * 1.5

    # 이상치 데이터 찾아서 인덱스만 리스트로 뽑아내기
    outliers = frauds[(frauds < lowest_value) | (frauds > highest_value)].index

    # 이상치 인덱스 리스트 리턴
    return outliers

데이터 프레임과 원하는 컬럼명을 인자로 받아서 해당 컬럼의 이상치 인덱스를 리스트로 반환하는 함수를 작성했습니다 :) 처음부터 바로 함수를 작성한 건 아니고, 컬럼 하나를 지정해서 하나하나 실행해본 다음 깔끔하게 함수로 정리하는 작업을 통해 함수를 완성했습니다.

find_outliers(card, "V14")
# Index([8296, 8615, 9035, 9252], dtype='int64')

find_outliers(card, "V17")
# Index([], dtype='int64')

아까 히트맵을 통해 확인했을 때 Class컬럼과 가장 낮은 상관관계를 보이는 컬럼은 V14와 V17인것으로 확인되었었는데요, find_outliers 함수를 통해 확인해 본 결과 V14 컬럼의 경우 이상치가 4개 발견되었고, V17 컬럼의 경우 이상치가 발견되지 않았습니다 :)

 따라서 저는 V14 컬럼의 이상치 데이터 4개를 삭제해 보겠습니다.

 

(6-2) 이상치 제거, 로그 변환 후 train, test 분리

def train_test_no_outliers(df):
    card_copy = df.copy()
    # outlier 제거
    outliers = find_outliers(card_copy, "V14")
    card_copy.drop(outliers, axis = 0, inplace = True)

    # Amount 컬럼 로그변환
    import numpy as np
    card_copy['Amount'] = np.log1p(card_copy['Amount'])
    
    # 데이터셋 나누기
    X_features = card_copy.iloc[:, :-1] # label에 해당하는 마지막 'Class'컬럼 제외
    y_label = card_copy.iloc[:, -1]     # label에 해당하는 마지막 'Class'컬럼만

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X_features,
                                                        y_label,
                                                        test_size = 0.2,
                                                        random_state = 1004,
                                                        stratify = y_label)
    
    return X_train, X_test, y_train, y_test

X_train_af, X_test_af, y_train_af, y_test_af = train_test_no_outliers(card)

train, test 분리하는 함수에 Amount 컬럼 로그변환과 outlier rows 제거하는 과정을 추가한 다음 분리 작업을 실시했습니다.

(6-3) 모델 학습 후 예측 성능 평가

train_eval(lgbm, X_train_af, X_test_af, y_train_af, y_test_af)
train_eval(xgb, X_train_af, X_test_af, y_train_af, y_test_af)

(좌) LGBM / (우) XGB

다음과 같이 결과가 나왔습니다.

 

7.  모델 4개 비교

지금까지 학습시킨 모델들의 성능 지표를 표로 작성하여 한눈에 비교해 보겠습니다.

  Baseline(XGB) Baseline(LGBM) After(XGB) After(LGBM)
Accuracy 0.9996 0.9996 0.9996 0.9996
Precision 0.9419 0.9405 0.9405 0.9302
Recall 0.8265 0.8061 0.8061 0.8163
F1 0.8804 0.8681 0.8681 0.8696
ROC Score 0.9853 0.9863 0.9882 0.9793

저는 이상치 제거와 로그 변환의 전처리 과정을 거친 다음 학습한 After 모델들의 모든 성능 지표가 다 개선될 것이라고 생각했습니다. 하지만 실습 결과 Precision / Recall / F1의 경우 오히려 Baseline의 수치가 더 높게 나온것을 확인할 수 있었습니다. 역시 인생이란 건 그렇게 호락호락하지 않습니다. 원하는 대로 결과가 안 나와서 좀 찝찝하긴 한데요...

다만, After XGB 모델 ROC-AUC 스코어의 경우 Baseline 모델들보다 점수가 더 상승한 것을 확인할 수 있었습니다. 이번에 다룬 Kaggle 신용카드 사기감지 데이터셋의 경우, 기존에 미리 확인한 대로 전체 데이터의 약 0.17%만이 사기에 해당하는 극도로 치우친 값을 가지고 있었습니다. 이렇게 과하게 편향된 데이터셋의 경우 ROC-AUC 스코어가 유의미하므로, 만약 4가지 모델 중 한 가지를 골라야 한다면 저는 ROC-AUC 스코어가 가장 높게 나온 After(XGB) 모델을 고르는 것도 나쁘지 않겠다는 생각을 했습니다.

 

8. 마무리

이번 포스팅은 여기에서 실습을 마치도록 할텐데요. 위의 4가지 모델에서 성능 지표를 더 올리기 위해서는 0.17%에 해당하는 사기 데이터를 펌핑해서 수를 늘려 주는 방법을 사용하면 좋을 것 같습니다. 마음같아서는 지금 당장 하고싶지만 할일이 많아서 일단 여기에서 끊겠습니다만, 좀 찝찝하기 때문에 시간여유가 생기면 모델 개선 작업을 추가로 수행해서 추가 포스팅을 하도록 하겠습니다.

이번 실습을 통해서 앞으로 다가올 머신러닝 팀프로젝트에 어떤 식으로 데이터 EDA를 진행하고 모델링을 개선해나갈 수 있을 지 좋은 가이드라인이 되었습니다. 

읽어주셔서 감사합니다 :-)

 

+ Recent posts