ML & DL/파이썬 머신러닝 실전 가이드

[Python ML Guide] Section 4.7(분류 Classification): 분류 실습 - Kaggle Credit Card Fraud Detection / Feature Engineering

Jae. 2023. 9. 19. 03:18
728x90

https://www.inflearn.com/course/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C

 

[개정판] 파이썬 머신러닝 완벽 가이드 - 인프런 | 강의

이론 위주의 머신러닝 강좌에서 탈피하여 머신러닝의 핵심 개념을 쉽게 이해함과 동시에 실전 머신러닝 애플리케이션 구현 능력을 갖출 수 있도록 만들어 드립니다., [사진]상세한 설명과 풍부

www.inflearn.com

 

 


1. Credit Card Fraud Detection

 

DataSet

 

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

 

Credit Card Fraud Detection

Anonymized credit card transactions labeled as fraudulent or genuine

www.kaggle.com

 

데이터 셋은 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가 됨
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