-
머신러닝(분류): 신용카드 사기 검출 분석데이터사이언스/데이터 분석 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
meV1 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, 재현율이 높은 상황에서 정밀도를 높혔다. (가장 준수한 성능)
'데이터사이언스 > 데이터 분석' 카테고리의 다른 글
추천시스템 이해 (2) 2024.03.17 FDA에 대하여 (0) 2024.03.15 유용한 파이썬 코드 모음 (데이터 분석) (0) 2023.02.24 시계열 데이터에 대한 ARIMA 모델 with R (0) 2023.02.22 딥러닝(CNN)을 이용한 음성분류 (0) 2023.02.21