본문 바로가기
머신러닝

Credit Card Fraud Detection

by 미생22 2024. 5. 27.
728x90

이번 시간에는 금융권 데이터를 가져오는 프로젝트를 진행하겠습니다.

 

credit card Fraud Detection이라고 신용카드 부정 사용자를 검출하는 프로젝트입니다.

- 신용카드와 같은 금융 데이터들은 구하기가 어렵습니다.
- 그러나 지능화 되어가는 현대 범죄에 맞춰 사전 이상 징후 검출 등 금융 기관이 많은 노력을 기울이고 있습니다.
- 이 데이터 역시 센서를 이용한 사람의 행동 과정 유추처럼 머신러닝의 이용 분야 중에 하나입니다.
https://www.kaggle.com/MLG-ULB/CREDITCARDFRAUD
- 데이터 받은 후 압축 풀고 소스코드 폴더에 옮깁니다.

 

데이터 개요
- 신용카드 사기 검출 분류 실습용 데이터
- 데이터에 class라는 이름의 컬럼이 사기 유무를 뜻함
- class 컬럼의 불균형이 극심해서 전체 데이터의 약 0.172%가 1 (사기 Fraud)을 가짐
- 금융 데이터이고 기업의 기밀 보호를 위해 대다수 특성의 이름은 삭제되어 있음 (V13 ~ V21 처럼)
- Amount : 거래 금액
- Class : Fraud 여유 (1이면 Fraud)

 

1. 데이터 읽고 관찰하기

데이터를 저장하고 가져와보겠습니다.

 

import pandas as pd

data_path = './creditcard.csv'
raw_data = pd.read_csv(data_path)
raw_data

 

class가 0이 일반, 1이 신용불량자입니다. Amount는 사용금액이네요.

 

column들을 살펴보고 Class를 value_counts() 시켜보겠습니다.

raw_data.columns

 

raw_data['Class'].value_counts()

492명이 전체의 몇퍼센트인지 확인해보겠습니다.

fraud_rate = round(raw_data['Class'].value_counts()[1]/len(raw_data)*100, 3)
print('Frauds', fraud_rate, '% of the dataset')

Frauds 0.173 % of the dataset으로 17.3퍼가 아니라 0.173%가 나오네요. 그래프로 보겠습니다.

 

import seaborn as sns
import matplotlib.pyplot as plt

sns.countplot(raw_data['Class'])
plt.title('Class Distribution \n (0: No Fraud || 1: Fraud)', fontsize=14)
plt.show()

 

거의 밑바닥에 붙어있네요... 이 0.173%가 위험한 이유가 있습니다. X, y로 확인해보겠습니다.

 

0번째 컬럼이 Time이므로 이를 제외하고 X,y를 나눌겁니다.

X = raw_data.iloc[:, 1:-1]
y = raw_data.iloc[:, -1]

X.shape, y.shape

((284807, 29), (284807,)) 라고 총 284807개의 데이터가 있고 feature는 29개와 1개네요.

 

데이터의 불균형이 심하기 때문에 train test split시 stratify를 주겠습니다. 안그러면 test 데이터에 fraud가 아예 없을수도..있으니까요...

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=13, stratify=y)

 

나눈 데이터의 불균형 정도가 어떤지 확인해보겠습니다.

import numpy as np

np.unique(y_train, return_counts=True)

위 코드를 기억해야 하는 점이 return_counts를 주면 value_counts()하듯이 각각의 갯수도 같이 출력해주기 때문입니다.

1의 갯수만 가져오기 위해서 [1][1]이라고 하고 전체 y_train 길이에서 나눠줍니다.

 

print('%: ', np.unique(y_train, return_counts=True)[1][1] / len(y_train) * 100)

%:  0.17254870488152324

 

이번에도 0.173%가 나오네요. stratify가 잘 된것을 확인할 수 있습니다.

 

2. 분석 첫시도 - 단순 분석법

먼저 분류기의 성능을 return 하는 함수를 하나 작성합니다.

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

def get_clf_eval(y_test, pred):
    acc = accuracy_score(y_test, pred)
    pre = precision_score(y_test, pred)
    re = recall_score(y_test, pred)
    f1 = f1_score(y_test, pred)
    auc = roc_auc_score(y_test, pred)

    return acc, pre, re, f1, auc

 

이번에는 성능을 출력하는 함수를 작성합니다.

 

from sklearn.metrics import confusion_matrix

def print_clf_eval(y_test, pred):
    confusion = confusion_matrix(y_test, pred)
    acc, pre, re, f1, auc = get_clf_eval(y_test, pred)

    print('=> confusion matrix')
    print(confusion)
    print('=============')

    print('Accuracy: {0:.4f}, Precision: {1:.4f}' .format(acc, pre))
    print('Recall: {0:.4f}, F1: {1:.4f}, AUC: {2:.4f}' .format(re, f1, auc))

 

이제 Logistic regression을 사용해서 돌려보겠습니다.

 

from sklearn.linear_model import LogisticRegression

lr_clf = LogisticRegression(random_state=13, solver='liblinear')#기억나시나요? logistic regression은 solver='liblinear'로 줘야합니다.
lr_clf.fit(X_train, y_train)
lr_pred = lr_clf.predict(X_test)

print_clf_eval(y_test, lr_pred)

accuracy가 상당히 높습니다.

fraud의 원래 개수를 보면 148개네요.

np.unique(y_test, return_counts=True)

(array([0, 1], dtype=int64), array([85295,   148], dtype=int64))

 

위 코드를 보면 Fraud는 148개가 있습니다. 148개인데 88개만 잡아낸겁니다.
85295중에서 11개를 Fraud라고 오해했습니다. 148개의 Fraud 중에서 60개를 틀렸습니다.
이 accuracy가 99.92%라고 하더라도 Recall이 59.5%밖에 안됩니다. 즉 60%가 안됩니다. Fraud 검출을 못한겁니다. 정상이 아닌것 같습니다.
이번에는 Decision Tree를 사용해보겠습니다.

 

여기서 주의할점은 금융권에서는 recall이 높을수록 좋다입니다. 일반인이어도 fraud라고 판정하더라도 fraud를 다 잡는것이 이득이라는 거죠.

 

from sklearn.tree import DecisionTreeClassifier

dt_clf = DecisionTreeClassifier(max_depth=4, random_state=13)
dt_clf.fit(X_train, y_train)
dt_pred = dt_clf.predict(X_test)

print_clf_eval(y_test, dt_pred)

148개 중에서 42개만 틀렸습니다. 더 해볼까요?
random forest를 돌려보겠습니다.

 

from sklearn.ensemble import RandomForestClassifier

rf_clf = RandomForestClassifier(n_estimators=100, n_jobs=-1, random_state=13)
rf_clf.fit(X_train, y_train)
rf_pred = rf_clf.predict(X_test)

print_clf_eval(y_test, rf_pred)

148개중에 38개를 틀렸네요, 점점 성능이 좋아지네요. 이번에는 Light GBM을 써보겠습니다.

from lightgbm import LGBMClassifier

lgbm_clf = LGBMClassifier(random_state=13, n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False);
#num_leaves는 decision tree의 leaf옵션을 얼만큼 줄것인가입니다. boost_from_average는...
lgbm_clf.fit(X_train, y_train);
lgbm_pred = lgbm_clf.predict(X_test);

print_clf_eval(y_test, lgbm_pred);

148개중에서 34개를 틀렸습니다. recall이 더 높아졌네요
precision은 예측한 것 중에서 참값, recall은 참값중에서 예측맞은값 이었습니다.
은행 입장에서는 recall이 좋을것입니다.
그러나 사용자 입장에서는 precision이 좋을 것 같습니다.
recall이 높아지면 정상 사용자인데 fraud로 의심받을 수 있기 때문입니다.

 

2-1. 분석 첫시도 모델링

모델과 데이터를 주면 성능을 출력하는 함수를 만들겠습니다.

def get_result(model, X_train, y_train, X_test, y_test):
    model.fit(X_train, y_train)
    pred = model.predict(X_test)

    return get_clf_eval(y_test, pred)

 

보기 쉽게 다수의 모델 성능을 정리해서 DataFrame으로 반환하는 함수를 작성해보겠습니다.

 

def get_result_pd(models, model_names, X_train, y_train, X_test, y_test):
    col_names = ['accuracy', 'precision', 'recall', 'f1', 'roc_auc']
    tmp = []

    for model in models:
        tmp.append(get_result(model, X_train, y_train, X_test, y_test))

    return pd.DataFrame(tmp, columns=col_names, index=model_names)

 

이제 4개의 분류모델을 한 번에 표로 정리해보겠습니다.

 

import time

models = [lr_clf, dt_clf, rf_clf, lgbm_clf]
model_names = ['LogisticRegression', 'DecisionTree', 'RandomForest', 'LightGBM']

start_time = time.time()
results = get_result_pd(models, model_names, X_train, y_train, X_test, y_test)

print('Fit time:', time.time()-start_time)
results

 

accuracy가 다 높습니다. recall은 lgbm에서 가장 높게 나오네요.
test 데이터 내 fraud인 148개 중에서 77%정도로 나오는 겁니다.

 

3. 두번째 시도 - scaler를 적용하자

amount라고 하는 거래량 컬럼을 봐보겠습니다.

plt.figure(figsize=(15,8))
sns.distplot(raw_data['Amount'], color='r')

plt.show()

특정 대역에 컬럼이 몰려있습니다. 즉 소량의 거래량에 몰려있습니다. 그렇다면 이 amount가 큰 키를 쥐고있다면 이렇게 몰려있는건 문제가 있을 수 있습니다.
scaler를 적용해야 하지 않을까요? Standard scaler를 적용시켜보겠습니다.

*Standard scaler는 평균을 0, 표준편차를 1로 두는 scaler로 이상치(outliers) 민감합니다. 이상치가 있으면 평균과 표준편차가 왜곡되어 변환 결과에 영향을 미칠 수 있습니다. 이상치가 많은 경우, 다른 스케일링 방법(예: RobustScaler)을 고려할 수 있습니다. SVM, 로지스틱 회귀, k-평균 군집화와 같은 모델에서 자주 사용됩니다. 이들 모델은 특성 스케일의 영향을 많이 받기 때문에 정규화가 필요합니다.
from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
amount_n = ss.fit_transform(raw_data['Amount'].values.reshape(-1,1))

 

우선 왜 이런 코드가 나왔는지 보겠습니다.

 

raw_data['Amount']

 

raw_data['Amount'].values

values를 취했다는 것은 array로 보겠다는 뜻입니다.

 

type(raw_data['Amount'].values)

numpy.ndarray

 

raw_data['Amount'].values.shape

(284807,) 라는 결과가 나옵니다. 즉, 284807개입니다.
그런데 우리는 (284807,1)형태를 갖고싶습니다. 그래서 reshape(-1,1) 크기는 동일하게 하되, 1로 바꿔라라는 명령을 주는겁니다.

그래서 reshape을 하면,

raw_data['Amount'].values.reshape(-1, 1)

이렇게 나오고 이 형태를 갖춰야 scaler에 fit_transform을 할 수 있습니다.

 

from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
amount_n = ss.fit_transform(raw_data['Amount'].values.reshape(-1,1))

raw_data_copy = raw_data.iloc[:, 1:-2]
#amount를 뺐기 때문에 1:-2입니다. -1은 class였고 -2가 amount 였습니다.
raw_data_copy['Amount_Scaled'] = amount_n
raw_data_copy.head()

 

데이터를 다시 나누겠습니다.

X_train, X_test, y_train, y_test = train_test_split(raw_data_copy, y, test_size=0.3, stratify=y, random_state=13)

 

이제 모델평가를 다시 돌립니다.

 

models = [lr_clf, dt_clf, rf_clf, lgbm_clf]
model_names = ['LinearReg.', 'Decision Tree', 'Random Forest', 'Light GBM']

start_time = time.time()
results = get_result_pd(models, model_names, X_train, y_train, X_test, y_test)
print('Fit time:', time.time()-start_time)
results

모델별로 roc커브도 그려보겠습니다.

 

from sklearn.metrics import roc_curve

def draw_roc_curve(models, model_names, X_test, y_test):
    plt.figure(figsize=(10,10))

    for model in range(len(models)):
        pred = models[model].predict_proba(X_test)[:, 1]
        fpr, tpr, thresholds = roc_curve(y_test, pred)
        plt.plot(fpr, tpr, label=model_names[model])

    plt.plot([0,1],[0,1], 'k--', label='random guess')#대각선
    plt.title('ROC')
    plt.legend()
    plt.grid()
    plt.show()

 

2-2. 스케일링에 집중해서 log scale을 적용해보겠습니다.

amount_log = np.log1p(raw_data['Amount'])

raw_data_copy['Amount_Scaled'] = amount_log

 

로그를 취할때 np.log1p()를 사용합니다.

여기서 다시 log()와 log1p()의 차이를 살펴보겠습니다.

  1. np.log():
    • np.log() 함수는 주어진 숫자의 자연로그를 계산합니다.
    • 하지만, 0이나 음수를 인자로 받으면 정의되지 않으므로 에러를 발생시킵니다.
  2. np.log1p():
    • np.log1p() 함수는 log(1 + x)를 계산합니다.
    • 이 함수는 x가 아주 작은 양수인 경우에도 안전하게 사용할 수 있습니다. 왜냐하면 x가 아주 작은 경우에도 1을 더해줌으로써 언더플로우(underflow)를 방지하기 때문입니다.
    • 즉, np.log1p(x)는 np.log(1 + x)와 동일하게 동작하지만, 작은 x에 대해 더 안전합니다.
따라서, 일반적으로는 작은 양수 값을 다룰 때는 np.log1p()를 사용하는 것이 안전하고 좋습니다. 이 함수는 특히 데이터 변환(Data Transformation)에서 사용되는 경우가 많습니다. 예를 들어, 어떤 변수의 분포가 왜곡되어 있을 때 이를 정규분포에 가깝게 만들기 위해 로그 변환을 적용할 때 사용될 수 있습니다.

 

로그를 취하는 이유가 상대적으로 큰 값을 낮은 값으로 보고, 낮은값은 그대로 보는 경향이 있기 때문입니다.

plt.figure(figsize=(10,5))
sns.distplot(raw_data_copy['Amount_Scaled'], color='r')

plt.show()

아까 그래프에 비해 훨씬 정규분포 모양을 띄는 것을 확인할 수 있습니다.

다시 성능을 확인해보겠습니다.

 

X_train, X_test, y_train, y_test = train_test_split(raw_data_copy, y, test_size=0.3, random_state=13, stratify=y)
start_time = time.time()
results = get_result_pd(models, model_names, X_train, y_train, X_test, y_test)

print('Fit time:', time.time() - start_time)
results

roc curve를 그려볼까요?

draw_roc_curve(models, model_names, X_test, y_test)

 

3. 세번째 시도 - outlier를 정리해보자

이번에는 outlier를 없애는 방식으로 진행해보겠습니다.

우선 outlier가 있는지 살펴보겠습니다.

 

import seaborn as sns

plt.figure(figsize=(10, 7))
sns.boxplot(data=raw_data[['V13', 'V14', 'V15']]);

V14를 보면 outlier가 엄청나게 많다는걸 볼 수 있습니다 ^^

너무 심한 outlier를 제거해보려고 합니다.
outlier를 정리하기 위해 outlier의 인덱스를 파악하는 코드를 짜보겠습니다.

 

def get_outlier(df=None, column=None, weight=1.5): #iqr의 1.5배를 하기위해 weight를 조정할 수 있게 해주겠습니다.
    fraud = df[df['Class']==1][column] #outlier 중에서 정상 데이터는 outlier로 생각을 안하고 fraud에 대해서만 outlier를 확인하기 위해서

    quantile_25 = np.percentile(fraud.values, 25) #np므로 numpy array로 입력을 해줘야 하기 때문에 .value를 씁니다.
    quantile_75 = np.percentile(fraud.values, 75)

    iqr = quantile_75 - quantile_25
    iqr_weight = iqr * weight

    lowest_val = quantile_25 - iqr_weight #25%지점에서 보면 iqr_weight를 빼는것이 lowest 구간
    highest_val = quantile_75 + iqr_weight

    outlier_index = fraud[(fraud < lowest_val) | (fraud > highest_val)].index

    return outlier_index

기억나시나요 np.percentile(data, 25) -> 1Q라는것... 

iqr도 가물가물할텐데 3Q-1Q였습니다. 하하하

이제 iqr에 weight를 곱해서 lowest_val, highet_val을 나누어 이 밖의 값을 outlier로 칭합니다.

 

V14에서 outlier를 가져오겠습니다.

get_outlier(df=raw_data, column='V14')

Int64Index([8296, 8615, 9035, 9252], dtype='int64')

 

4개의 index가 나왔네요.

이제 drop 시켜보겠습니다.

 

raw_data_copy.shape

(284807, 31)

outlier_index = get_outlier(df=raw_data, column='V14', weight=1.5)
raw_data_copy.drop(outlier_index, axis=0, inplace=True)
raw_data_copy.shape

(284803, 29)

 

네 4개가 사라진걸 확인할 수 있습니다.

이제 outlier를 제거했으니 또 데이터를 나누겠습니다...

 

X = raw_data_copy #이 데이터에는 y값이 없었습니다.
raw_data.drop(outlier_index, axis=0, inplace=True)
#raw_data도 raw_data_copy처럼 outlier를 제거해주고 y를 만들겠습니다.
y = raw_data.iloc[:, -1] #scaler를 적용하기 전입니다.

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=13, stratify=y)
models = [lr_clf, dt_clf, rf_clf, lgbm_clf]
model_names = ['LogisticRegression', 'DecisionTree', 'RandomForest', 'LightGBM']

result = get_result_pd(models, model_names, X_train, y_train, X_test, y_test)

result

recall이 lgbm에서 0.79로 좋아졌다고 얘기할 수 있지 않을까요?
roc curve를 살펴보겠습니다.

 

 

4. 네번째 시도 - SMOTE Oversampling

지금까지 우리는 3번의 시도를 했습니다.

1번째 : 그냥 시도
2번째 : scaler를 다 던져서 시도
3번째 : outlier를 제거해서 시도
그리고 4번째는 oversampling 입니다.

 

fraud의 수가 적으니 데이터의 불균형이 극심할 때 불균형한 두 클래스의 분포를 강제로 맞춰보는 작업을 할겁니다.
Undersampling : 많은 수의 데이터를 적은 수의 데이터로 강제로 조정
Oversampling :
 - 원본 데이터의 피처값들을 아주 약간 변경하여 증식
 - 대표적으로 SMOTE(Synthetic Minority Over-sampling Technique) 방법이 있음
 - 적은 데이터 세트에 있는 개별 데이터를 k-최근접 이웃 방법으로 찾아서 데이터의 분포 사이에 새로운 데이터를 만드는 방식
 - imbalanced-learn 이라는 Python pkg가 있음

!pip install imbalanced-learn

 

imbalanced-learn을 설치해줍니다.

 

from imblearn.over_sampling import SMOTE

smote = SMOTE(random_state=13)
X_train_over, y_train_over = smote.fit_resample(X_train, y_train) #명령어가 fit_resample입니다.

random state만 지정해주고 fit_resample 시켜줍니다. 

여기서 조심해야할 게 있습니다. 데이터를 다루는건 train 데이터에 한정해야합니다. test데이터는 건드리면 안됩니다.
test 데이터는 오염되면 안됩니다.

얼마나 불려나갔는지 shape을 통해 보겠습니다.

 

X_train_over.shape, y_train_over.shape

 

((398040, 29), (398040,))

 

X_train.shape, y_train.shape

((199362, 29), (199362,))

 

print(np.unique(y_train, return_counts=True))

(array([0, 1], dtype=int64), array([199020,    342], dtype=int64))

 

 

 

 

 

 

print(np.unique(y_train_over, return_counts=True))

(array([0, 1], dtype=int64), array([199020, 199020], dtype=int64))

 

train데이터에서 강제로 fraud수를 맞춰놨습니다.
이제 모델을 돌리겠습니다.

 

models = [lr_clf, dt_clf, rf_clf, lgbm_clf]
model_names = ['LogisticRegression', 'DecisionTree', 'RandomForest', 'LightGBM']

result = get_result_pd(models, model_names, X_train_over, y_train_over, X_test, y_test)

result

draw_roc_curve(models, model_names, X_test, y_test)

728x90