728x90
1. Credit Card Fraud Detection
DataSet
https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud
데이터 셋은 2013년 European Card 사용 트랜잭션을 가공하여 생성
불균형(imbalanced) 되어 있는 데이터 셋
284,807건의 데이터 중 492건의 Fraud이다 (전체의 0.172%)
Credit Card Fault Detection ML 모델 구축
다양한 Feature Engineering 방식을 차례로 Logistic Regression과 LightGBM을 이용하여 비교
Feature Engineering
- 중요 Feature의 데이터 분포도 변경
- 정규 분포
- Log 변환
- 이상치(Outlier) 제거
- 유의사항: 덜 지우고 기준상에서 많이 벗어나는 경우에만 제거 (엄격한 기준)
- 막 제거하다 보면 기준치들이 점점 낮아지는 경우가 생김
- SMOTE 오버 샘플링: 적은 수의 샘플 수를 큰 수의 샘플 수만큼 증가
Log 변환
- Log 변환은 왜곡된 분포도를 가진 데이터 세트를 비교적 정규 분포에 가깝게 변환해주는 훌륭한 Feature Engineering 방식
- np.log1p( )를 이용
- log(x)가 아니라 log(x+1)함수 사용 = np.log( )가 아니라 np.log1p( ) 사용
- 컴퓨터는 수치해석시 매우 작은 값은 0.0으로 처리: log( )에 매우 작은 값이 들어가면 -inf가 나오므로 이를 방지하기 위해 log(x+1)을 사용한다
- np.expm1( )을 이용하여 복원
# 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
var_1 = np.log1p(100)
var_2 = np.expm1(var_1)
print(var_1, var_2)
# 4.61512051684126 100.00000000000003
Tukey-Fences Method (Outlier 제거)
Outlier: 전체 데이터의 패턴에서 벗어난 이상 값을 가진 데이터
-> 이상치 데이터로 인해 모델의 성능에 영향을 받을 수 있다
Tukey Fences에선 아래 두가지로 아웃아이어를 판단한다.
- Q1 - (1.5 * IQR) 미만
- Q3 + (1.5 * IQR) 초과
이를 이용해 아웃라이어를 찾는 함수를 정의해보자.
#Tukey Fences 사용
#이상치 제거 함수 생성
def handle_outliers(df, column):
q3 = df[column].quantile(0.75)
q1 = df[column].quantile(0.25)
iqr = q3 - q1
boundary = 1.5 * iqr
lower_bound = q1 - boundary
upper_bound = q3 + boundary
df = df[(df[column]<upper_bound)&(df[column]>lower_bound)]
return df
Under Sampling과 Oversampling
- 레이블이 불균형한 분포를 가진 데이터 세트를 학습 시
- 이상 레이블을 가지는 데이터 건수가 매우 적어 제대로 된 유형의 학습이 어려움
- 반면에 정상 레이블을 가지는 데이터 건수는 매우 많아 일방적으로 정상 레이블로 치우친 학습을 수행하여, 제대로 된 이상 데이터 검출이 어려움
- 대표적으로 오버 샘플링(Oversampling)과 언더 샘플링(Undersampling) 방법을 통해 적절한 학습 데이터를 확보
UnderSampling
- 많은 레이블을 가진 데이터 세트를 적은 레이블을 가진 데이트 세트 수준으로 감소 샘플링
- (+): 과도하게 정상 레이블로 학습 / 예측하는 문제 개선 가능
- (-): 너무 많이 감소시켜 정상레이블 학습 수행의 문제 발생 가능
OverSampling
- 적은 레이블을 가진 데이터 세트를 많은 레이블을 가진 데이트 세트 수준으로 증식
- Oversampling이 Undersampling보다 상대적으로 더 많이 사용됨
- 동일 데이터를 단순하게 증식시킬 경우 과적합 문제 발생가능 -> 원본 데이터의 피처를 약간만 변경하여 증식
- 대표적으로 SMOTE 방법이 있음
Oversampling 이나 Undersampling은 절대 Test_data 에서는 사용 불가능하다 (왠만하면 Train_data에서만 사용)
- 실제 Test는 가공된 Data가 아니라 실제 Test_Data로만 해야하기 때문
SMOTE (Synthetic Minority Over-Sampling Technique 개요)
- 소수 클래스 데이터 사이에 새로운 데이터를 생성하는 방법
- 원본 데이터에 있는 개별 데이터들의 K-Nearest Neighbor을 찾아서 이 데이터와 K개의 이웃들의 차이를 일정 값으로 만들어 기존 데이터와 약간 차이가 나는 새로운 데이터를 생성하는 방식
- fit_resample(x,y)를 이용
- 학습데이터 세트만 오버 샘플링 수행
- 일반적으로 재현율은 높아지고, 정밀도는 낮아짐
2. 데이터 일차 가공 및 모델 학습/예측/평가
Data Load
- Class=0: 정상거래
- Class=1: 사기거래
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)
- 원본 DataFrame은 유지하고 데이터 가공을 위한 DataFrame을 복사하여 반환
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
- 학습과 테스트 데이터 세트를 반환하는 함수 생성, 사전 데이터 처리가 끝난 뒤 해당 함수 호출
- stratify=y_target 으로 Stratified 기반 분할
- 불균형한 데이터 세트를 최대한 레이블 분포에 따라 분할함
- 1(사기거래)의 데이터 수가 워낙 적기 때문에 각 학습, 테스트 데이터 셋에 원래 레이블 분포와 동일하게 분포되어야 함
# 사전 데이터 가공 후 학습과 테스트 데이터 세트를 반환하는 함수.
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)
# 학습 데이터 레이블 값 비율
# Class
# 0 99.827451
# 1 0.172549
# Name: count, dtype: float64
# 테스트 데이터 레이블 값 비율
# Class
# 0 99.826785
# 1 0.173215
# Name: count, dtype: float64
- 분류 모델 성능 평가 지표 정의
from sklearn.metrics import (
confusion_matrix,
accuracy_score,
precision_score,
recall_score,
f1_score,
)
from sklearn.metrics import roc_auc_score
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
)
)
- Logistic Regression 으로 학습/예측/평가
- 분류 성능 평가지표 중 Recall 값이 중요: FN이 중요 = 실제 사기(F)인데 정상으로 판단(N)하는 것이 문제
from sklearn.linear_model import LogisticRegression
lr_clf = LogisticRegression(max_iter=1000)
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)
# 오차 행렬
# [[85281 14]
# [ 58 90]]
# 정확도: 0.9992, 정밀도: 0.8654, 재현율: 0.6081, F1: 0.7143, AUC:0.9703
- 앞으로 피처 엔지니어링을 수행할 때마다 모델을 학습/예측/평가하므로 이를 위한 함수 생성
# 인자로 사이킷런의 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 학습 / 예측 / 평가
- boost_from_average
- True일 경우 레이블 값이 극도로 불균형 분포를 이루는 경우 재현율 및 ROC-AUC 성능이 매우 저하됨. 레이블 값이 극도로 불균형할 경우 boost_from_average를 False로 설정하는 것이 유리
- LightGBM 2.1.0 이상 버젼에서 boost_from_average가 True가 Default가 됨
- boost_from_average
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)
# 오차 행렬
# [[85290 5]
# [ 36 112]]
# 정확도: 0.9995, 정밀도: 0.9573, 재현율: 0.7568, F1: 0.8453, AUC:0.9790
3. 데이터 분포도 변환 후 모델 학습/예측/평가
- 중요 feature의 분포도 확인
import seaborn as sns
plt.figure(figsize=(8, 4))
plt.xticks(range(0, 30000, 1000), rotation=60)
sns.histplot(card_df["Amount"], bins=100, kde=True)
plt.show()
- 데이터 사전 가공을 위한 별도의 함수에 StandardScalar를 이용하여 Amount 피처 변환
- df.insert(loc, column, value, allow_duplicates=False): DataFrame의 특정 위치에 열을 삽입하는 method
- loc : 삽입될 열의 위치 (0번부터 정수형으로 시작)
- column : 삽입될 열의 이름
- val : 삽입될 열의 값
- allow_duplicates : {True or False} 기본값은 False로 True일경우 중복 열의 삽입을 허용합니다.
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
- StandardScalar 변환 후 로지스틱 회귀 및 LightGBM 학습 / 예측 / 평가
# Amount를 정규분포 형태로 변환 후 로지스틱 회귀 및 LightGBM 수행.
X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)
print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression(max_iter=1000)
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)
- Amount를 로그 변환
- np.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
- 로그 변환 후 Logistic Regression / LightGBM 모델 학습 / 예측 / 평가
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)
- 로그 변환 후 Amount 데이터 분포 확인
import seaborn as sns
plt.figure(figsize=(8, 4))
sns.histplot(X_train["Amount_Scaled"], bins=50, kde=True)
plt.show()
4. 이상치 데이터 제거 후 모델 학습/예측/평가
- 각 피처들의 상관 관계를 시각화, 결정 Label인 class 값과 가장 상관도가 높은 피처 추출
- .corr( ): correlation coefficient (상관계수) -> DataFrame을 반환
- -1 ~ 1 사이의 실수값을 가짐
- 1로 가까워 지거나, -1로 가까워질 경우 상관도가 높다
- heatmap: x축의 값들과 y축의 값들간의 상관관계
import seaborn as sns
plt.figure(figsize=(12, 12))
corr = card_df.corr()
sns.heatmap(corr, annot=True, fmt=".1f", cmap="RdBu")
- DataFrame에서 outlier에 해당하는 데이터를 필터링하기 위한 함수 생성
- outlier 레코드의 index를 반환함
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
np.percentile(card_df["V14"].values, 100)
np.max(card_df["V14"].values)
quantile_25 = np.percentile(card_df["V14"].values, 25)
quantile_75 = np.percentile(card_df["V14"].values, 75)
print(quantile_25, quantile_75)
# -0.4255740124549935 0.493149849218149
outlier_index = get_outlier(df=card_df, column='V14', weight=1.5)
print('이상치 데이터 인덱스:', outlier_index)
# 이상치 데이터 인덱스: Int64Index([8296, 8615, 9035, 9252], dtype='int64')
- 로그 변환 후 V14 피처의 이상치 데이터를 삭제한 뒤 모델들을 재학습 / 예측 / 평가
# 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)
# 이상치 데이터 삭제하는 로직 추가: index이므로 drop시 axis=0이다
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
)
5. SMOTE 오버 샘플링 적용 후 모델 학습/예측/평가
import imblearn
print(imblearn.__version__)
# 0.11.0
- oversampling된 피처 / 레이블 데이터 세트 반환
- 적은 레이블을 가진 데이터 세트를 많은 레이블을 가진 데이터 세트 수준으로 증식
- 레이블의 분포도를 균형있게 맞춤
y_train.value_counts()
# 0 199020
# 1 342
# Name: Class, dtype: int64
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 적용 후 레이블 값 분포:
# 0 199020
# 1 199020
# Name: Class, dtype: int64
- Logistic Regression으로 Oversampling된 X_train_over, y_train_over & X_test, y_test을 이용하여 학습 / 예측 / 평가 진행
lr_clf = LogisticRegression(max_iter=1000)
# 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
- precision_recall_curve 그리기
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으로 Oversampling된 X_train_over, y_train_over & X_test, y_test을 이용하여 학습 / 예측 / 평가 진행
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,
)
# 오차 행렬
# [[85283 12]
# [ 22 124]]
# 정확도: 0.9996, 정밀도: 0.9118, 재현율: 0.8493, F1: 0.8794, AUC:0.9814
728x90