machine_learning
불균형 데이터 분석
hayleyhell
2022. 11. 18. 19:05
In [1]:
!pip install kaggle
from google.colab import files
files.upload()
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/ Requirement already satisfied: kaggle in /usr/local/lib/python3.7/dist-packages (1.5.12) Requirement already satisfied: certifi in /usr/local/lib/python3.7/dist-packages (from kaggle) (2022.9.24) Requirement already satisfied: tqdm in /usr/local/lib/python3.7/dist-packages (from kaggle) (4.64.1) Requirement already satisfied: python-dateutil in /usr/local/lib/python3.7/dist-packages (from kaggle) (2.8.2) Requirement already satisfied: requests in /usr/local/lib/python3.7/dist-packages (from kaggle) (2.23.0) Requirement already satisfied: six>=1.10 in /usr/local/lib/python3.7/dist-packages (from kaggle) (1.15.0) Requirement already satisfied: urllib3 in /usr/local/lib/python3.7/dist-packages (from kaggle) (1.24.3) Requirement already satisfied: python-slugify in /usr/local/lib/python3.7/dist-packages (from kaggle) (6.1.2) Requirement already satisfied: text-unidecode>=1.3 in /usr/local/lib/python3.7/dist-packages (from python-slugify->kaggle) (1.3) Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests->kaggle) (2.10) Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests->kaggle) (3.0.4)
Saving kaggle.json to kaggle.json
Out[1]:
{'kaggle.json': b'{"username":"minaahayley","key":"3af569f6a8c028e6233a6d29ce2439d3"}'}
In [2]:
ls -1ha kaggle.json
kaggle.json
In [3]:
!mkdir -p ~/.kaggle #create folder name Kaggle
!cp kaggle.json ~/.kaggle #copy kaggle.jason into folder Kaggle
!chmod 600 ~/.kaggle/kaggle.json #ignore Permission Warning
In [4]:
#다운로드가 제대로 되었는지 확인한다.
#ls 명령어는 특정 경로에 어떤 파일이 있는지 확인해 보는 명령어다.
%ls ~/.kaggle
kaggle.json
In [5]:
#Copy API command 후 데이터셋 다운로드하기
!kaggle datasets download -d mlg-ulb/creditcardfraud
#파일 압축 풀기
!unzip creditcardfraud.zip
Downloading creditcardfraud.zip to /content 74% 49.0M/66.0M [00:00<00:00, 140MB/s] 100% 66.0M/66.0M [00:00<00:00, 152MB/s] Archive: creditcardfraud.zip inflating: creditcard.csv
In [6]:
!mkdir -p ~/.kaggle/competitions/creditcardfraud #create folder name Kaggle
!cp creditcard.csv ~/.kaggle/competitions/creditcardfraud #copy into folder Kaggle
In [7]:
#다운로드가 제대로 되었는지 확인한다.
#ls 명령어는 특정 경로에 어떤 파일이 있는지 확인해 보는 명령어다.
%ls ~/.kaggle/competitions/creditcardfraud
creditcard.csv
In [8]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
In [9]:
creditcard = pd.read_csv('~/.kaggle/competitions/creditcardfraud/creditcard.csv')
In [10]:
creditcard.sample()
Out[10]:
Time | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | ... | V21 | V22 | V23 | V24 | V25 | V26 | V27 | V28 | Amount | Class | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
79545 | 58070.0 | -0.370154 | 1.017746 | 1.167029 | -0.122934 | 0.219537 | -0.49632 | 0.544532 | 0.12973 | -0.543342 | ... | -0.240242 | -0.685805 | -0.06282 | -0.069051 | -0.165537 | 0.077085 | 0.237701 | 0.083719 | 1.29 | 0 |
1 rows × 31 columns
In [11]:
creditcard.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 284807 entries, 0 to 284806 Data columns (total 31 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Time 284807 non-null float64 1 V1 284807 non-null float64 2 V2 284807 non-null float64 3 V3 284807 non-null float64 4 V4 284807 non-null float64 5 V5 284807 non-null float64 6 V6 284807 non-null float64 7 V7 284807 non-null float64 8 V8 284807 non-null float64 9 V9 284807 non-null float64 10 V10 284807 non-null float64 11 V11 284807 non-null float64 12 V12 284807 non-null float64 13 V13 284807 non-null float64 14 V14 284807 non-null float64 15 V15 284807 non-null float64 16 V16 284807 non-null float64 17 V17 284807 non-null float64 18 V18 284807 non-null float64 19 V19 284807 non-null float64 20 V20 284807 non-null float64 21 V21 284807 non-null float64 22 V22 284807 non-null float64 23 V23 284807 non-null float64 24 V24 284807 non-null float64 25 V25 284807 non-null float64 26 V26 284807 non-null float64 27 V27 284807 non-null float64 28 V28 284807 non-null float64 29 Amount 284807 non-null float64 30 Class 284807 non-null int64 dtypes: float64(30), int64(1) memory usage: 67.4 MB
In [13]:
from sklearn.model_selection import train_test_split
# 불필요한 Time 피처만 삭제
def get_preprocessed_df(df=None):
df_copy = df.copy()
df_copy.drop('Time', axis=1, inplace=True)
return df_copy
In [14]:
# 사전 데이터 가공 후 학습과 테스트 데이터셋을 반환하는 함수
def get_train_test_dataset(df=None):
df_copy = get_preprocessed_df(df)
X = df_copy.iloc[:, :-1]
y = df_copy.iloc[:, -1]
# stratify=y으로 Stratified 기반 분할
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
return X_train, X_test, y_train, y_test
X_train, X_test, y_train, y_test = get_train_test_dataset(creditcard)
- Stratified 방식으로 추출해 학습 데이터 세트와 테스트 데이터 세트의 레이블 값 분포도를 서로 동일하게 만든다.
In [15]:
# 학습 데이터 세트와 테스트 데이터 세트의 레이블 값 비율을 백분율로 환산
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
Modeling¶
로지스틱 회귀¶
In [16]:
from sklearn.linear_model import LogisticRegression
lr_clf = LogisticRegression(max_iter=1000)
lr_clf.fit(X_train, y_train)
lr_predict = lr_clf.predict(X_test)
lr_predict_proba = lr_clf.predict_proba(X_test)[:, 1]
In [17]:
# 분류 평가 지표
from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
def get_clf_eval(y_test, predict=None, predict_proba=None):
confusion = confusion_matrix(y_test, predict)
accuracy = accuracy_score(y_test, predict)
precision = precision_score(y_test, predict)
recall = recall_score(y_test, predict)
f1 = f1_score(y_test, predict)
roc_auc = roc_auc_score(y_test, predict_proba)
print('오차 행렬')
print(confusion)
print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}, f1: {3:.4f}, auc: {4:.4f}'.format(accuracy, precision, recall, f1, roc_auc))
In [18]:
# 평가 수행
get_clf_eval(y_test, lr_predict, lr_predict_proba)
오차 행렬 [[85280 15] [ 56 92]] 정확도: 0.9992, 정밀도: 0.8598, 재현율: 0.6216, f1: 0.7216, auc: 0.9704
LightGBM¶
- 본 데이터 세트는 극도로 불균형한 레이블 값 분포도를 가지고 있으므로
- LGBMClassifier 객체 생성 시 boost_from_average=False로 파라미터 설정
In [19]:
from lightgbm import LGBMClassifier
lgbm_clf = LGBMClassifier(n_estimators=1000,
num_leaves=64,
n_jobs=-1,
boost_from_average=False) # True 설정은 recall, roc-auc 성능을 매우 크게 저하시킨다
lgbm_clf.fit(X_train, y_train)
lgbm_predict = lgbm_clf.predict(X_test)
lgbm_predict_proba = lgbm_clf.predict_proba(X_test)[:, 1]
In [21]:
# 평가 수행
get_clf_eval(y_test, lgbm_predict, lgbm_predict_proba)
오차 행렬 [[85289 6] [ 36 112]] 정확도: 0.9995, 정밀도: 0.9492, 재현율: 0.7568, f1: 0.8421, auc: 0.9797
StandardScaler¶
In [22]:
# Amount 피처는 신용 카드 사용 금액으로 정상/사기 트랜잭션을 결정하는 매우 중요한 속성일 가능성이 높다.
import seaborn as sns
plt.figure(figsize=(8,4))
plt.xticks(range(0, 30000, 1000), rotation=60)
sns.histplot(creditcard['Amount'], bins=100, kde=True)
plt.show()
인사이트
- 카드 사용금액이 1000불 이하인 데이터가 대부분
- Amount를 표준 정규 분포 형태로 변환한 뒤에 로지스틱 회귀의 예측 성능 측정
In [30]:
creditcard['Amount'].values
Out[30]:
array([149.62, 2.69, 378.66, ..., 67.88, 10. , 217. ])
In [31]:
creditcard['Amount'].values.reshape(-1, 1)
Out[31]:
array([[149.62], [ 2.69], [378.66], ..., [ 67.88], [ 10. ], [217. ]])
In [32]:
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로 피처명 변경후 ,데이터 프레임 맨 앞 컬럼으로 입력
df_copy.insert(0, 'Amount_Scaled', amount_n)
# 기존 Time, Amount 피처 삭제
df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
return df_copy
In [33]:
# Amount를 정규 분포 형태로 변환 후 로지스틱 회귀, LightGBM 수행
X_train, X_test, y_train, y_test = get_train_test_dataset(creditcard)
print('로지스틱 회귀 예측 성능')
lr_clf = LogisticRegression(max_iter=1000)
lr_clf.fit(X_train, y_train)
lr_predict = lr_clf.predict(X_test)
lr_predict_proba = lr_clf.predict_proba(X_test)[:, 1]
get_clf_eval(y_test, lr_predict, lr_predict_proba)
print('LightGBM 예측 성능')
lgbm_clf = LGBMClassifier(n_estimators=1000,
num_leaves=64,
n_jobs=-1,
boost_from_average=False) # True 설정은 recall, roc-auc 성능을 매우 크게 저하시킨다
lgbm_clf.fit(X_train, y_train)
lgbm_predict = lgbm_clf.predict(X_test)
lgbm_predict_proba = lgbm_clf.predict_proba(X_test)[:, 1]
get_clf_eval(y_test, lgbm_predict, lgbm_predict_proba)
로지스틱 회귀 예측 성능 오차 행렬 [[85281 14] [ 58 90]] 정확도: 0.9992, 정밀도: 0.8654, 재현율: 0.6081, f1: 0.7143, auc: 0.9702 LightGBM 예측 성능 오차 행렬 [[85289 6] [ 36 112]] 정확도: 0.9995, 정밀도: 0.9492, 재현율: 0.7568, f1: 0.8421, auc: 0.9773
로그 변환¶
- log1p() 함수
- 로그 변환은 데이터 분포도가 심하게 왜곡되어 있을 경우 적용
- 원래 값을 log 값으로 변환해 원래 큰 값을 상대적으로 작은 값으로 변환하기 때문에 데이터 분포도의 왜곡을 상당 부분 개선
In [34]:
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)
# 기존 Timet, Amount 피처 삭제
df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
return df_copy
In [35]:
# Amount를 로그 변환 후 로지스틱 회귀, LightGBM 수행
X_train, X_test, y_train, y_test = get_train_test_dataset(creditcard)
print('로지스틱 회귀 예측 성능')
lr_clf = LogisticRegression(max_iter=1000)
lr_clf.fit(X_train, y_train)
lr_predict = lr_clf.predict(X_test)
lr_predict_proba = lr_clf.predict_proba(X_test)[:, 1]
get_clf_eval(y_test, lr_predict, lr_predict_proba)
print('LightGBM 예측 성능')
lgbm_clf = LGBMClassifier(n_estimators=1000,
num_leaves=64,
n_jobs=-1,
boost_from_average=False) # True 설정은 recall, roc-auc 성능을 매우 크게 저하시킨다
lgbm_clf.fit(X_train, y_train)
lgbm_predict = lgbm_clf.predict(X_test)
lgbm_predict_proba = lgbm_clf.predict_proba(X_test)[:, 1]
get_clf_eval(y_test, lgbm_predict, lgbm_predict_proba)
로지스틱 회귀 예측 성능 오차 행렬 [[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.9786
인사이트
- 레이블이 극도로 불균형한 데이터 세트에서 로지스틱 회귀는 데이터 변환 시 약간 불안정한 성능 결과를 보여준다
이상치 데이터 제거¶
In [36]:
import seaborn as sns
plt.figure(figsize=(9,9))
corr = creditcard.corr()
sns.heatmap(corr, cmap='RdBu')
Out[36]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f6e7d8591d0>
- 결정 레이블인 Class 피처와 음의 상관관계가 가장 높은 피처는 v14, v17
- v14 이상치를 제거해 보자
In [37]:
import numpy as np
def get_outlier(df=None, column=None, weight=1.5):
# fraud에 해당하는 column 데이터만 추출, 1/4분위와 3/4분위 지점을 np.percetile로 구함
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
# 최대값보다 크거나, 최소값보다 작은 값을 이상치 데이터로 설정하고, index 반환
outlier_index = fraud[(fraud < lowest_val) | (fraud > highest_val)].index
return outlier_index
In [38]:
outlier_index = get_outlier(df=creditcard, column='V14', weight=1.5)
print('이상치 데이터 인덱스:', outlier_index)
이상치 데이터 인덱스: Int64Index([8296, 8615, 9035, 9252], dtype='int64')
In [39]:
# get_processed_df()를 로그 변환 후 v14 피처의 이상치 데이터를 삭제하는 로직으로 변경
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)
# 기존 Timet, Amount 피처 삭제
df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
# 이상치 데이터 삭제하는 로직 추가
outlier_index = get_outlier(df=creditcard, column='V14', weight=1.5)
df_copy.drop(outlier_index, axis=0, inplace=True)
return df_copy
In [40]:
X_train, X_test, y_train, y_test = get_train_test_dataset(creditcard)
print('로지스틱 회귀 예측 성능')
lr_clf = LogisticRegression(max_iter=1000)
lr_clf.fit(X_train, y_train)
lr_predict = lr_clf.predict(X_test)
lr_predict_proba = lr_clf.predict_proba(X_test)[:, 1]
get_clf_eval(y_test, lr_predict, lr_predict_proba)
print('LightGBM 예측 성능')
lgbm_clf = LGBMClassifier(n_estimators=1000,
num_leaves=64,
n_jobs=-1,
boost_from_average=False) # True 설정은 recall, roc-auc 성능을 매우 크게 저하시킨다
lgbm_clf.fit(X_train, y_train)
lgbm_predict = lgbm_clf.predict(X_test)
lgbm_predict_proba = lgbm_clf.predict_proba(X_test)[:, 1]
get_clf_eval(y_test, lgbm_predict, lgbm_predict_proba)
로지스틱 회귀 예측 성능 오차 행렬 [[85281 14] [ 48 98]] 정확도: 0.9993, 정밀도: 0.8750, 재현율: 0.6712, f1: 0.7597, auc: 0.9743 LightGBM 예측 성능 오차 행렬 [[85291 4] [ 25 121]] 정확도: 0.9997, 정밀도: 0.9680, 재현율: 0.8288, f1: 0.8930, auc: 0.9831
결론
- 이상치 데이터를 제거한 뒤 로지스틱 회귀와 LightGBM 모두 예측 성능이 크게 향상되었다.
SMOTE 오버 샘플링¶
- SMOTE를 적용할 때는 반드시 학습 데이터셋만 오버 샘플링을 해야 한다.
- 검증, 테스트 데이터셋을 오버 샘플링 할 경우 결국은 원본 데이터가 아닌 데이터셋에서 검증, 테스트를 수행하기 때문에 X
In [41]:
!pip install imbalanced-learn
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/ Requirement already satisfied: imbalanced-learn in /usr/local/lib/python3.7/dist-packages (0.8.1) Requirement already satisfied: scikit-learn>=0.24 in /usr/local/lib/python3.7/dist-packages (from imbalanced-learn) (1.0.2) Requirement already satisfied: numpy>=1.13.3 in /usr/local/lib/python3.7/dist-packages (from imbalanced-learn) (1.21.6) Requirement already satisfied: scipy>=0.19.1 in /usr/local/lib/python3.7/dist-packages (from imbalanced-learn) (1.7.3) Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.7/dist-packages (from imbalanced-learn) (1.2.0) Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.7/dist-packages (from scikit-learn>=0.24->imbalanced-learn) (3.1.0)
In [42]:
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 적용 후 레이블 값 분포: ', 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 [43]:
X_train, X_test, y_train, y_test = get_train_test_dataset(creditcard)
print('로지스틱 회귀 예측 성능')
lr_clf = LogisticRegression(max_iter=1000)
lr_clf.fit(X_train_over, y_train_over)
lr_predict = lr_clf.predict(X_test)
lr_predict_proba = lr_clf.predict_proba(X_test)[:, 1]
get_clf_eval(y_test, lr_predict, lr_predict_proba)
print('LightGBM 예측 성능')
lgbm_clf = LGBMClassifier(n_estimators=1000,
num_leaves=64,
n_jobs=-1,
boost_from_average=False) # True 설정은 recall, roc-auc 성능을 매우 크게 저하시킨다
lgbm_clf.fit(X_train_over, y_train_over)
lgbm_predict = lgbm_clf.predict(X_test)
lgbm_predict_proba = lgbm_clf.predict_proba(X_test)[:, 1]
get_clf_eval(y_test, lgbm_predict, lgbm_predict_proba)
로지스틱 회귀 예측 성능 오차 행렬 [[82937 2358] [ 11 135]] 정확도: 0.9723, 정밀도: 0.0542, 재현율: 0.9247, f1: 0.1023, auc: 0.9737 LightGBM 예측 성능 오차 행렬 [[85286 9] [ 22 124]] 정확도: 0.9996, 정밀도: 0.9323, 재현율: 0.8493, f1: 0.8889, auc: 0.9789
결론
- 로지스틱 회귀 모델의 경우 SMOTE로 오버 샘플링된 데이터로 학습 할 경우 정밀도가 5.4%로 극단적으로 낮아진다.
- SMOTE를 적용하면 재현율은 높아지나, 정밀도는 낮아지는 것이 일반적이다.