ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 머신러닝(분류): 신용카드 사기 검출 분석
    데이터사이언스/데이터 분석 2023. 2. 28. 15:42

     

     

     

     

     

    데이터 소개 :  

    • European Card의 사용 내역으로 생성된 데이터.
    • 총 거래내역 284,807건 중에서 사기 당한 표본은 492건. (0.172%로 매우 희박함)
    • 변수 V1~V28은 거래내역의 개인정보 문제로 PCA된 변수. 
    • 출처: https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud

     

     

    Credit Card Fraud Detection

    Anonymized credit card transactions labeled as fraudulent or genuine

    www.kaggle.com

     

     


    분석 목적: 

     

    • 신용카드 사기를 예측하는 분류 모델 구축
    • 모든 거래가 정상이라고 예측하는 모델도 정확도는 99.828.
    • 따라서 실제 사기를 사기라고 판단한 "재현율" 지표가 중요함
    • 라벨 불균형한 데이터에 대한 해결 방안 실습 (오버샘플링) 

     

     

     

     


    분석 순서 소개 

     

     

    1. 데이터 로드

     

     

    2. 학습데이터와 테스트 데이터로 분리

    •  데이터프레임을 나누는 함수를 정의하여 수행

     

     

    3. 1차 학습

    • 모델: 로지스틱
    • 전처리: none 
    • 하이퍼파라미터 튜닝: 학습횟수 제한(1000회)

     

     

    4. 데이터프레임을 입력하면, 분류결과를 출력하는 함수 정의

     

    5. 2차 학습

    • 모델: LGBM 
    • 전처리 : none 
    • 하이퍼파라미터 튜닝: 
      n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False

     

     

    6. 3차 학습

    • 모델: 로지스틱, LGBM
    • 전처리 : 변수(amount)에 대한 StandardScaling
    • 하이퍼파라미터 튜닝: 
      -로지스틱 : 학습횟수 제한(1000회)
      -LGBM:  n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False

     

     

    7. 4차 학습 

    • 모델: 로지스틱, LGBM
    • 전처리 : 변수(amount)에 대한 로그변환
    • 하이퍼파라미터 튜닝: 
      -로지스틱 : 학습횟수 제한(1000회)
      -LGBM: n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False


    8. 5차 학습

    • 모델: 로지스틱, LGBM
    • 전처리 : 변수(amount)에 대한 로그변환 + 변수(V14)에 대한 이상치 제거(IQR*1.5기준)
    • 하이퍼파라미터 튜닝: 
      -로지스틱 : 학습횟수 제한(1000회)
      -LGBM:  n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False

     


    9. 6차 학습

    • 모델: 로지스틱, LGBM
    • 전처리 : 변수(amount)에 대한 로그변환 + 변수(V14)에 대한 이상치 제거(IQR기준)+ SMOTE 오버샘플링(사기 데이터 표본 증대) 
    • 하이퍼파라미터 튜닝: 
      -로지스틱 : 학습횟수 제한(1000회)
      -LGBM:  n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False

     

     

     

     


    데이터 로드

     

    In [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)
    
     
     
    Out[1]:
     
    Ti
    me
    V1 V2 V3 V4 V5 V6 ... V24 V25 V26 V27 V28 Amount Class
    0.0 -1.359807 -0.072781 2.536347 1.378155 -0.338321 0.462388 ... 0.066928 0.128539 -0.189115 0.133558 -0.021053 149.62 0
    0.0 1.191857 0.266151 0.166480 0.448154 0.060018 -0.082361 ... -0.339846 0.167170 0.125895 -0.008983 0.014724 2.69 0
    1.0 -1.358354 -1.340163 1.773209 0.379780 -0.503198 1.800499 ... -0.689281 -0.327642 -0.139097 -0.055353 -0.059752 378.66 0

    3 rows × 31 columns

     

    In [3]:
    card_df.shape
    
     
    Out[3]:
    (284807, 31)

     

    원본 DataFrame은 유지하고 데이터 가공을 위한 DataFrame을 복사하여 반환

    밑에서 전처리 제대로 하니까 일단 복사본으로 실행하기 위해서

     

    In [24]:
    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
    

     

     


    학습데이터와 테스트 데이터로 분리

     

     

    학습과 테스트 데이터 세트를 반환하는 함수 생성. 사전 데이터 처리가 끝난 뒤 해당 함수 호출

     

    In [21]:
    # 사전 데이터 가공 후 학습과 테스트 데이터 세트를 반환하는 함수.
    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)
    
     
    In [6]:
    y_train.value_counts()/y_train.shape[0]*100
    
     
     
    Out[6]:
    0    99.827451
    1     0.172549
    Name: Class, dtype: float64
    In [7]:
    print('학습 데이터 레이블 값 비율')
    print(y_train.value_counts()/y_train.shape[0] * 100)
    print('테스트 데이터 레이블 값 비율')
    print(y_test.value_counts()/y_test.shape[0] * 100)
    
    학습 데이터 레이블 값 비율
    0    99.827451
    1     0.172549
    Name: Class, dtype: float64
    테스트 데이터 레이블 값 비율
    0    99.826785
    1     0.173215
    Name: Class, dtype: float64
    
     
     
     
     

    분류결과(성능)를 출력하는 함수 정의

     
     
     
    In [10]:
    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))
    

     

     


    1차 학습

     
     
    In [28]:
    from sklearn.linear_model import LogisticRegression
    
    
    X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)
    
    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]
     [   56    92]]
    정확도: 0.9992, 정밀도: 0.8679, 재현율: 0.6216,    F1: 0.7244, AUC:0.9702
    
     

    앞으로 피처 엔지니어링을 수행할 때마다 모델을 학습/예측/평가하므로 이를 위한 함수 생성

    넣고 모델 넣으면 평가지표를 출력해주는 함수를 정의하자
     
     
    In [14]:
    # 인자로 사이킷런의 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)
        
    

     

     

     


    2차 학습

     

    LightGBM 학습/예측/평가 

     

    LightGBM 2.1.0 이상 버전에서 boost_from_average가 True가 Default가 됨. boost_from_average가 True일 경우 레이블 값이 극도로 불균형 분포를 이루는 경우 재현률 및 ROC-AUC 성능이 매우 저하됨. 레이블 값이 극도로 불균형할 경우 boost_from_average를 False로 설정하는 것이 유리

     

    ->

    다시 말해서,

     

    LGBM에서

    데이터라벨이 불균일 하다면 boost_from_average=False 을 넣어준다!

     

    In [27]:
    from lightgbm import LGBMClassifier
    
    X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)
    
    
    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의 분포도 확인

    결제금액은 중요하니까 확인하고, 또 결제 금액은 일반적으로 편향되어있다.

     

    In [32]:
    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()
    

     

    데이터 사전 가공을 위한 별도의 함수에 StandardScaler를 이용하여 Amount 피처 변환

     

    In [29]:
    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))              #reshape?
        # 변환된 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
    

     

    StandardScaler 변환 후 로지스틱 회귀 및 LightGBM 학습/예측/평가

     

    In [30]:
    # 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)
    
    ### 로지스틱 회귀 예측 성능 ###
    오차 행렬
    [[85281    14]
     [   58    90]]
    정확도: 0.9992, 정밀도: 0.8654, 재현율: 0.6081,    F1: 0.7143, AUC:0.9702
    ### LightGBM 예측 성능 ###
    오차 행렬
    [[85290     5]
     [   37   111]]
    정확도: 0.9995, 정밀도: 0.9569, 재현율: 0.7500,    F1: 0.8409, AUC:0.9779
    

     

     


    4차 학습 : 

     

    그렇게 개선되지 않았다, 이번에는 Amount를 로그변환

     

    In [31]:
    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
    
    In [18]:
    print(np.log(1e-1000))
    
    -inf
    
    In [19]:
    # 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
    
    In [20]:
    var_1 = np.log1p(100)                   # 1 더하고 로그취하는 함수
    var_2 = np.expm1(var_1)                 # 위 함수의 역변환 
    print(var_1, var_2)
    
    4.61512051684126 100.00000000000003
    

     

    로그변환도 그다지 큰 향상은 없다

     

    In [32]:
    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)
    
    ### 로지스틱 회귀 예측 성능 ###
    오차 행렬
    [[85283    12]
     [   59    89]]
    정확도: 0.9992, 정밀도: 0.8812, 재현율: 0.6014,    F1: 0.7149, AUC:0.9727
    ### LightGBM 예측 성능 ###
    오차 행렬
    [[85290     5]
     [   35   113]]
    정확도: 0.9995, 정밀도: 0.9576, 재현율: 0.7635,    F1: 0.8496, AUC:0.9796
    

     

    로그변환의 결과물을 시각화해보자

     

    In [31]:
    import seaborn as sns
    
    plt.figure(figsize=(8, 4))
    sns.histplot(X_train['Amount_Scaled'], bins=50, kde=True)
    plt.show()
    

     

     

     


    5차 학습 : 

    이상치 데이터 제거 후 모델 학습/예측/평가

     

    각 피처들의 상관 관계를 시각화. 결정 레이블인 class 값과 가장 상관도가 높은 피처 추출

     

    상관계수 행렬 출력 

     

    In [34]:
    card_df.corr().shape
    
    Out[34]:
    (31, 31)
    In [35]:
    import seaborn as sns
    
    plt.figure(figsize=(12, 12))
    corr = card_df.corr()
    sns.heatmap(corr, annot=True, fmt='.1f',  cmap='RdBu')
    
    Out[35]:
    <AxesSubplot:>

     

    Dataframe에서 outlier에 해당하는 데이터를 필터링하기 위한 함수 생성. outlier 레코드의 index를 반환함

    np.percentile(,25)는 사분위수 Q1을 보여준다

     

    In [1]:
    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)                                       #Q1
        quantile_75 = np.percentile(fraud.values, 75)                                       #Q3
        # IQR을 구하고, IQR에 1.5를 곱하여 최대값과 최소값 지점 구함. 
        iqr = quantile_75 - quantile_25
        iqr_weight = iqr * weight                                                           #IQR*1.5
        lowest_val = quantile_25 - iqr_weight           
        highest_val = quantile_75 + iqr_weight                                                         #최댓값과 최솟값의 출력 
        # 최대값 보다 크거나, 최소값 보다 작은 값을 아웃라이어로 설정하고 DataFrame index 반환. 
        outlier_index = fraud[(fraud < lowest_val) | (fraud > highest_val)].index                    # or 조건 인덱싱으로 인덱스 출력
        return outlier_index
        
    
    In [37]:
    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)                         # IQR범위 출력
    print(quantile_25, quantile_75)
    
    -0.4255740124549935 0.493149849218149
    
     
    신용카드 데이터셋의 V14에 이상치는 잘 알려진 사실이다. 
     
    배경지식이 없는 현업의 분석에서는 최대한 많은 변수에 대하여 이상치를 제거해보고, 모델 성능을 확인하자.  
     
     
     
    In [38]:
    outlier_index = get_outlier(df=card_df, column='V14', weight=1.5)                      
    print('이상치 데이터 인덱스:', outlier_index)
    이상치 데이터 인덱스: Int64Index([8296, 8615, 9035, 9252], dtype='int64')
    

     

    로그 변환 + V14 피처의 이상치 데이터를 삭제한 뒤 모델들을 재 학습/예측/평가

     

    많이 개선된 것을 알 수 있다

     

    In [39]:
    # 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)
    
    ### 로지스틱 회귀 예측 성능 ###
    오차 행렬
    [[85281    14]
     [   48    98]]
    정확도: 0.9993, 정밀도: 0.8750, 재현율: 0.6712,    F1: 0.7597, AUC:0.9743
    ### LightGBM 예측 성능 ###
    오차 행렬
    [[85290     5]
     [   25   121]]
    정확도: 0.9996, 정밀도: 0.9603, 재현율: 0.8288,    F1: 0.8897, AUC:0.9780
    
     

     

     


    6차 학습 : 

    SMOTE 오버 샘플링 적용 후 모델 학습/예측/평가

     

    test 데이터는 아무것도 건들지 마라!!!

     

    In [1]:
    #pip install imblearn          설치
    
     
    In [3]:
    import imblearn
    
    print(imblearn.__version__)
    
    0.10.0
    
    In [45]:
    y_train.value_counts()                               #밑에 분포와 비교하자
    
    Out[45]:
    0    199020
    1       342
    Name: Class, dtype: int64
    In [46]:
    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
    
     
     
    In [47]:
    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
    
    정밀도가 너무 낮다! (재현율은 좋아졌다. )  #로지스틱
     
     
     
     
    In [48]:
    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()
        
    
    In [49]:
    #테스트데이터에 대한 p-r곡선 
    
    precision_recall_curve_plot( y_test, lr_clf.predict_proba(X_test)[:, 1] )

     

    In [50]:
    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
    
     

    LGBM, 재현율이 높은 상황에서 정밀도를 높혔다. (가장 준수한 성능)

     

     

     


     

     

     

     

     

     

     

    댓글