Loading [MathJax]/jax/output/CommonHTML/jax.js
본문 바로가기
공부기록/데이터사이언스

LightGBM 이상 거래 예측하기 실습 with 데싸 노트

by tankwoong 2024. 3. 29.
반응형

XGBoost 이후 나온 최신 부스팅 모델로, 캐글에서 좋은 퍼포먼스를 많이 보여주어 그 성능을 인정받았으며, 리프 중심 트리 분할 방식을 사용한다. XGBoost보다 빠르고, 높은 정확도를 보이며, 예측에 영향을 미치는 변수의 중요도를 확인할 수 있다. 변수 종류가 많고, 데이터가 클수록 상대적으로 뛰어난 성능을 보여준다. XGBoost와 마찬가지로 복잡한 모델인 만큼, 해석의 어려움이 있고, 하이퍼파라미터 튜닝이 까다롭다.

문제정의 

데이터셋을 활용하여 이상거래를 탐지한다.

 

라이브러리 및 데이터 불러오고, 확인하기 

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

file_url='https://media.githubusercontent.com/media/musthave-ML10/data_source/main/fraud.csv'
data = pd.read_csv(file_url)

라이브러리 임포트 

data.info(show_counts=True)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1852394 entries, 0 to 1852393
Data columns (total 22 columns):
 #   Column                 Non-Null Count    Dtype  
---  ------                 --------------    -----  
 0   trans_date_trans_time  1852394 non-null  object 
 1   cc_num                 1852394 non-null  int64  
 2   merchant               1852394 non-null  object 
 3   category               1852394 non-null  object 
 4   amt                    1852394 non-null  float64
 5   first                  1852394 non-null  object 
 6   last                   1852394 non-null  object 
 7   gender                 1852394 non-null  object 
 8   street                 1852394 non-null  object 
 9   city                   1852394 non-null  object 
 10  state                  1852394 non-null  object 
 11  zip                    1852394 non-null  int64  
 12  lat                    1852394 non-null  float64
 13  long                   1852394 non-null  float64
 14  city_pop               1852394 non-null  int64  
 15  job                    1852394 non-null  object 
 16  dob                    1852394 non-null  object 
 17  trans_num              1852394 non-null  object 
 18  unix_time              1852394 non-null  int64  
 19  merch_lat              1852394 non-null  float64
 20  merch_long             1852394 non-null  float64
 21  is_fraud               1852394 non-null  int64  
dtypes: float64(5), int64(5), object(12)
memory usage: 310.9+ MB

데이터 확인 

trans_date_trans_time:거래시간-> object이므로 타입을 바꿔줄 필요가 있음 

cc_num: 카드 번호

merchant: 거래 상점 

category:거래 상점의 범주 

amt:거래 금액 

first/last:이름

gender:성별

street/state/zip:고객 거주지 정보

lat/long 고객 주소에 대한 위도 및 경도

city_pop: 고객의 zipcode에 속하는 인구수 

job: 직업

dob:  생년월일 

trans_num: 거래번호

unix_time: 거래 시간

merch_lat/merch_long:상점 위치에 대한 위도 및 경도

is_fraud:사기거래 여부

round(data.describe(),2)

통계치를 확인 

amt와 city_pop은 이상치를 보이고 있는데, 가격이나 인구수는 얼마든지 변할 수 있음으로 처리하지 않음

is_fraud의 평균이 0.01이므로 1%밖에 되지 않는다. 0에 치우는 비대칭 데이터임으로 하면서 오버샘플링을 해주어서 예측 정확도를 향상할 수 있다.

전처리: 데이터 클리닝

data.drop(['first','last','street','city','state','zip','trans_num','unix_time','job','merchant'], axis=1, inplace=True)

필요 없는 변수 제외

이름, 도시, 길, 주, 우편번호 이런 것은 영향이 없을 것으로 예상되고, trans_num, unix_time도 trans_date_trans_time을 사용하면 됨으로 필요 없어 보인다. 

job과 merchant도 제외시키겠다. job은 갯수가 너무 많아 더미변수로 사용하기 어렵고, 상점 별로 이상거래가 높을 수 있지만, category 변수의 사용을 통해 대체해 보겠다.

data['trans_date_trans_time'] = pd.to_datetime(data['trans_date_trans_time'])

데이터 형식을 바꿔준다.

전처리: 피처 엔지니어링

결제금액 

갑자기 결제를 많이 하면 이상치로 의심해 볼 수 있다.

amt_info = data.groupby('cc_num').agg(['mean','std'])['amt'].reset_index()

cc_num을 기준으로 amt에 대한 평균과 표준편차를 구해준다.

data = data.merge(amt_info,on='cc_num', how='left')

cc_num을 키값으로 해서 기존 데이터에 left join을 해준다.

data['amt_z_score'] = (data['amt']-data['mean'])/data['std']
data.drop(['mean','std'], axis=1, inplace=True)

z_score를 구하고, 평균과 표준편차를 지워준다.

범주

사람마다 많이 쓰는 범주가 있다.

category_info = data.groupby(['cc_num','category']).agg(['mean','std'])['amt'].reset_index()
data = data.merge(category_info, on=['cc_num','category'], how='left')
data['cat_z_score'] = (data['amt']-data['mean'])/data['std']
data.drop(['mean','std'], axis=1, inplace=True)

동일한 방식으로 진행해 준다.

거리

평상시에 쓰는 거리보다 훨씬 먼 거리라면 의심해 볼 수 있다.

import geopy.distance
data['merch_coord'] = pd.Series(zip(data['merch_lat'], data['merch_long'])) #위도, 경도 통합
data['cust_coord'] = pd.Series(zip(data['lat'],data['long']))#위도, 경도 통합
data['distance'] = data.apply(lambda x: geopy.distance.distance(x['merch_coord'], x['cust_coord']).km, axis=1)
# 1. 거리 통계량 계산
distance_info = data.groupby('cc_num').agg(['mean', 'std'])['distance'].reset_index()

# 2. 거리 통계량 병합
data = data.merge(distance_info, on='cc_num', how='left')

# 3. 거리 Z-점수 계산
data['distance_z_score'] = (data['distance'] - data['mean']) / data['std']

# 4. 결과 확인
data.head()

나이

data['age'] = 2024-pd.to_datetime(data['dob']).dt.year

불필요한 변수 제거 및 더미변수

data.drop(['cc_num', 'lat','long','merch_lat','merch_long', 'dob', 'merch_coord', 'cust_coord'], axis=1, inplace=True)

 

data=  pd.get_dummies(data, columns=['category','gender'], drop_first=True)
data.set_index('trans_date_trans_time', inplace=True)

불필요한 변수를 제거하고 더미변수를 만든 후 인덱스를 정해준다.

모델링 및 평가하기

train = data[data.index < '2020-07-01']
test = data[data.index >= '2020-07-01']

날짜를 기준으로 훈련 셋과 시험 셋을 나눈다.

len(test)/len(data)
0.2837738623640543

약 28% 정도가 시험셋임을 확인할 수 있다.

X_train = train.drop('is_fraud', axis=1)
X_test = test.drop('is_fraud', axis=1)
y_train = train['is_fraud']
y_test = test['is_fraud']

훈련 셋과 시험 셋을 만든다.

pip install lightgbm==3.2.1

lightgbm은 4.0 이상은 많이 바뀌었으므로, 이번 실습에서는 3.2.1로 설치해 주겠다.

import lightgbm as lgb
model_1 = lgb.LGBMClassifier(random_state =100)
model_1.fit(X_train, y_train)
pred_1 = model_1.predict(X_test)

모델을 만들어주고, 훈련시킨 후, 예측변수를 만들어준다.

from sklearn.metrics import accuracy_score, confusion_matrix, classification_report, roc_auc_score

이번에는 roc_auc_score도 불러와 주겠다.

accuracy_score(y_test, pred_1)
0.9970665504954714
print(confusion_matrix(y_test,pred_1))
[[522892    757]
 [   785   1227]]
print(classification_report(y_test, pred_1))
  precision    recall  f1-score   support

           0       1.00      1.00      1.00    523649
           1       0.62      0.61      0.61      2012

    accuracy                           1.00    525661
   macro avg       0.81      0.80      0.81    525661
weighted avg       1.00      1.00      1.00    525661

실제 이상거래에서는 재현율을 비교하는 것이 좋다. 왜냐하면 2종오류와 관련이 있기 때문에 이상이 있는데 없다고 하면 피해가 커질 수 있기 때문이다.

여기까지 예측값은 0과 1로 나뉘었는데 기준점을 어디로 잡느냐에 따라서 예측 결과가 달라진다. 따라서 predict_proba를 사용하여 0과 1이 아닌 소수점 형태의 결과를 얻을 수 있다.

proba_1 = model_1.predict_proba(X_test)
proba_1 = proba_1[:,1]

1에 대한 결과만 저장해 준다.

proba_int1 = (proba_1>0.2).astype('int')
proba_int2 = (proba_1>0.8).astype('int')

0.2와 0.8을 기준으로 혼동행렬과 분류리포트를 비교해 보겠다.

print(confusion_matrix(y_test, proba_int1))
print(confusion_matrix(y_test, proba_int2))

보면 0.8로 이동할 때 정밀도는 올라가지만 재현율은 떨어지는 것을 확인할 수 있다. 하지만 이것으로는 정확한 판단하기가 어렵다. 그래서 ROC곡선과 AUC라는 것을 활용해 보겠다.

roc_auc_score(y_test,proba_1)
0.9491871968462691

하이퍼파라미터 튜닝: 랜덤 그리드 서치

from sklearn.model_selection import RandomizedSearchCV
params = {
        'n_estimators':[100,500,1000],
        'learning_rate':[0.01,0.05,0.1,0.3],
        'lambda_l1':[0,10,20,30,50],
        'lambda_l2':[0,10,20,30,50],
        'max_depth':[5,10,15,20],
        'subsample':[0.6,0.8,1]
}

여기서 lamda_l1, lambda_l2는 정규화로 기울기에 페널티를 부여하여 너무 큰 계수가 나오지 않도록 강제하는 방법이다.

l1정규화는 불필요한 변수를 제거해 버리는 효과가 있고, l2 정규화는 모든 변수들이 모델에 반영된다.

model_2 = lgb.LGBMClassifier(random_state=100)
rs = RandomizedSearchCV(model_2, param_distributions=params, n_iter=30, scoring='roc_auc',random_state=100,n_jobs=-1)

이번에는 roc_auc를 기준으로 모델을 만들어 줄 수 있다.

import time
start = time.time()
rs.fit(X_train,y_train)
print(time.time()-start)
7574.391634464264

훈련시간도 체크할 수 있다.

rs.best_params_
{'subsample': 1,
 'n_estimators': 1000,
 'max_depth': 15,
 'learning_rate': 0.05,
 'lambda_l2': 20,
 'lambda_l1': 0}

가장 좋은 파라미터를 출력해 주고

rs_proba = rs.predict_proba(X_test)
roc_auc_score(y_test, rs_proba[:,1])

1 부분만 골라서, 점수를 출력해 준다.

0.9944612776469138
rs_proba_int = (rs_proba[:,1]>0.2).astype('int')
print(confusion_matrix(y_test,rs_proba_int))
[[522535   1114]
 [   580   1432]]
print(classification_report(y_test, rs_proba_int))
precision    recall  f1-score   support

           0       1.00      1.00      1.00    523649
           1       0.56      0.71      0.63      2012

    accuracy                           1.00    525661
   macro avg       0.78      0.85      0.81    525661
weighted avg       1.00      1.00      1.00    525661

분류 점수와 혼동행렬도  0.2를 기준으로 구해본다.

LightGBM에서 train 함수 사용하기 

train은 fit과 다르게 검증 셋을 지원하고, 데이터 프레임을 별도의 포맷으로 변환이 필요하고, 하이퍼파라미터를 꼭 지정해야 하며, 사이킷런과 연동이 불가하다.

train = data[data.index < '2020-01-01'] #훈련셋 설정
val = data[(data.index >= '2020-01-01')& (data.index<'2020-07-01')] #검증셋 검증
test = data[data.index >= '2020-07-01'] #시험셋 설정
X_train = train.drop('is_fraud', axis=1)
X_val = val.drop('is_fraud', axis=1)
X_test = test.drop('is_fraud', axis=1)
y_train = train['is_fraud']
y_val = val['is_fraud']
y_test = test['is_fraud']

독립변수와 종속변수를 분리해 줌 

d_train = lgb.Dataset(X_train, label=y_train)
d_val = lgb.Dataset(X_val, label=y_val)

DataSet을 통해 훈련 셋과 검증 셋에 대해서 고유한 데이터셋으로 변화시켜 준다.

params_set = rs.best_params_
params_set['metrics'] = 'auc'

하이터파라미터를 지정해 준다.

params_set
{'subsample': 1,
 'n_estimators': 1000,
 'max_depth': 15,
 'learning_rate': 0.05,
 'lambda_l2': 20,
 'lambda_l1': 0,
 'metrics': 'auc'}
model_3 = lgb.train(params_set, d_train, valid_sets=[d_val], early_stopping_rounds=100, verbose_eval=100)

파라미터 셋을 정한 것을 model_3로 만들어준다.

pred_3 = model_3.predict(X_test)

예측변수를 만들고

roc_auc_score(y_test,pred_3)
roc_auc_score(y_test,pred_3)
0.9892873466222063

점수를 출력해 준다.

좀 전에 model_1과 model_3을 비교해 보겠다.

feature_imp = pd.DataFrame({'feature_name':X_train.columns, 'importance': model_1.feature_importances_}).sort_values('importance', ascending=False)
plt.figure(figsize=(20,10))
sns.barplot(x='importance', y='feature_name', data=feature_imp.head(10))
plt.show()
feature_imp_3 = pd.DataFrame(sorted(zip(model_3.feature_importance(), X_train.columns)), columns=['Value', 'Feature'])
plt.figure(figsize=(20,10))
sns.barplot(x="importance",y="feature_name", data=feature_imp.head(10))
plt.show()

그래프상으로 육안상으로 비교할 수 없을 만큼 비슷하게 나와서 한 그래프만 출력하였다.

 

반응형