Data Science/머신러닝

[PySpark] 신용카드 사기거래 탐지 모델링(1) - 데이터 탐색

히또아빠 2023. 6. 21. 16:44

실제 데이터 모델링 업무를 하다보면 클래스 불균형(class imbalanced, 라벨 불균형)인 데이터를 자주 만나게 된다. 
일반적으로, 머신러닝 모델은 클래스의 비중이 비슷할때 가장 잘 작동하지만 그런 균형있는 데이터를 다루기엔 현실적으로 쉽지않다. 따라서, 데이터가 심각하게 불균형(highly imbalanced)인 상황에서 어떤 문제가 있고, 어떻게 의사결정하여 문제를 해결했는지 정리하고자 한다.

 

해당 데이터는 kaggle: Credic Card Fraud Detection에서 다운로드 가능하다.

 

데이터는 2013년 9월 유럽에서 발생한 신용 카드 거래내역이다. 이 데이터는 이틀 동안 발생한 거래내역을 보여주며, 여기서  284,807건의 거래중 492건의 신용카드 거래가 사기로 판별됐다. 클래스가 양(Positive), 사기거래는 전체 거래의 0.172%를 차지한다.


1.스파크 세션 설정 및 데이터 불러오기

데이터를 불러오기 위해 spark 세션을 세팅하고 local에 저장된 csv 파일을 불러왔다.

from pyspark.sql import SparkSession
from pyspark import SparkConf


# spark-conf 
conf = SparkConf()
conf.set("spark.driver.memory", "50g")
conf.set("spark.executor.memory", "30g")
conf.set("spark.ui.port","4051")

spark = SparkSession.builder \
    .appName("credit") \
    .master("local[*]") \
    .config(conf=conf) \
    .getOrCreate()
data = spark.read.load("/DATA/JupyterLab/modeling/creditcard.csv",
                       format = 'csv',
                       header = 'true',
                       inferSchema = 'true')
type(data)

> pyspark.sql.dataframe.DataFrame

data.printSchema()

> root
 |-- Time: double (nullable = true)
 |-- V1: double (nullable = true)
 |-- V2: double (nullable = true)
 |-- V3: double (nullable = true)
 |-- V4: double (nullable = true)
 |-- V5: double (nullable = true)
 |-- V6: double (nullable = true)
 |-- V7: double (nullable = true)
 |-- V8: double (nullable = true)
 |-- V9: double (nullable = true)
 |-- V10: double (nullable = true)
 |-- V11: double (nullable = true)
 |-- V12: double (nullable = true)
 |-- V13: double (nullable = true)
 |-- V14: double (nullable = true)
 |-- V15: double (nullable = true)
 |-- V16: double (nullable = true)
 |-- V17: double (nullable = true)
 |-- V18: double (nullable = true)
 |-- V19: double (nullable = true)
 |-- V20: double (nullable = true)
 |-- V21: double (nullable = true)
 |-- V22: double (nullable = true)
 |-- V23: double (nullable = true)
 |-- V24: double (nullable = true)
 |-- V25: double (nullable = true)
 |-- V26: double (nullable = true)
 |-- V27: double (nullable = true)
 |-- V28: double (nullable = true)
 |-- Amount: double (nullable = true)
 |-- Class: integer (nullable = true)

1.데이터 셋

위의 데이터 스키마 확인결과

  • V1 ~ V28: 개인 정보로 공개 되지 않는 28개의 PCA 변환후 주성분
  • Time: 거래시간
  • Amount: 거래금액
  • Class: 사기여부 {1: 사기, 0: 정상거래} 

총 컬럼의 개수는 31개이며 이후에 사기를 판별하는 PySpark ML 모델을 만들어볼 예정이다.

2.EDA

2-1.클래스(Class) 탐색

data.groupBy("Class").count().show()

+-----+------+
|Class| count|
+-----+------+
|    1|   492|
|    0|284315|
+-----+------+

전체 284,807건의 거래 데이터중 사기 거래는 단 492건의 데이터 셋이다. 만약, 데이터 사이즈가 굉장히 큰 경우에는 아래와 같이 pandas 데이터 프레임으로 바꾼후 그래프를 그리는데 리소스 낭비가 있지만 지금은 데이터 셋이 작은 관계로 굳이굳이 그려봤다 ㅇㅅㅇ

사기거래가 전체 거래의 약 0.17%로 굉장히 클래스 불균형(Class imbalanced)임을 알 수 있다.

import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt

pd_data = data.toPandas()
print('Distribution of the Classes in the dataset')
print(pd_data['Class'].value_counts()/len(pd_data))
sns.countplot('Class', data=pd_data)
plt.title('Equally Distributed Classes', fontsize=14)
plt.show()

#Distribution of the Classes in the dataset
#0    0.998273
#1    0.001727
#Name: Class, dtype: float64

클래스 불균형으로 모델링시 발생하는 문제를 해결하는 방법으로는 오버샘플링, 언더샘플링, 성능 지표 정의 등 다양한 방식이 존재하는데 현재 기존의 데이터를 변형하지않고 성능 평가지표를 활용하여 모델 성능 비교를 하는 방식으로 진행하려고 한다. 클래스 불균형으로 인한 문제와 해결방안에 대해서는 따로 포스팅 진행하도록 하겠다.

 

2-2.상관관계 탐색

# Make sure we use the subsample in our correlation
f, (ax1) = plt.subplots(1, 1, figsize=(15,8))
# Entire DataFrame
corr = pd_data.corr()
sns.heatmap(corr, cmap='coolwarm_r', annot_kws={'size':10}, ax=ax1)
ax1.set_title("Imbalanced Correlation Matrix", fontsize=14)

또, 굳이 굳이 변수간의 상관관계를 히트맵(heatmap)을 활용하여 봤다. 당연하게도 v1 ~ v28 각각의 PCA 주성분들은 독립성이 만족하기 때문에 상관계수가 0으로 나타난다.

PySpark를 활용하는 이유는 데이터 크기 때문에 효율적으로 빅데이터를 처리하는데 장점이 있다. 데이터 분석시 위와같이 판다스 데이터 프레임으로 바꾼후 시각화 하는 방법도 있지만 예를들어, 몇천만건의 데이터를 판다스 데이터 프레임으로 바꾼후 시각화하여 탐색하기엔 리소스 문제로 쉽지는 않다. 따라서 데이터 크기, 리소스 용량 등 여러가지 상황을 고려해 판단해서 데이터 탐색을 어떤 방식을 통해 진행할지 적절한 조율이 필요하다.

2-3.결측치 체크

# 결측치 체크
# isnan(): pyspark.sql.functions 라이브러리, isnan('컬럼이름')
# isNull(): pyspark.sql.Column 라이브러리, col('컬럼이름').isNull()
from pyspark.sql.functions import isnan, count, when, col
data.select([count(when(isnan(c) | col(c).isNull(), c)).alias(c) for c in data.columns]).show()

+----+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+------+-----+
|Time| V1| V2| V3| V4| V5| V6| V7| V8| V9|V10|V11|V12|V13|V14|V15|V16|V17|V18|V19|V20|V21|V22|V23|V24|V25|V26|V27|V28|Amount|Class|
+----+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+------+-----+
|   0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|     0|    0|
+----+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+------+-----+

해당 데이터의 결측치 탐색결과 빈값은 없다.

2-4.Time(거래시간),  Amount(거래금액) 탐색

data.filter("Class =='0'").select("Time", "Amount").summary().show()
+-------+-----------------+------------------+
|summary|             Time|            Amount|
+-------+-----------------+------------------+
|  count|           284315|            284315|
|   mean|94838.20225805884| 88.29102242231278|
| stddev| 47484.0157855508|250.10509222589235|
|    min|              0.0|               0.0|
|    25%|          54221.0|              5.64|
|    50%|          84719.0|              22.0|
|    75%|         139335.0|              77.0|
|    max|         172792.0|          25691.16|
+-------+-----------------+------------------+

data.filter("Class == '1'").select("Time", "Amount").summary().show()
+-------+-----------------+------------------+
|summary|             Time|            Amount|
+-------+-----------------+------------------+
|  count|              492|               492|
|   mean|80746.80691056911|122.21132113821137|
| stddev|47835.36513767505| 256.6832882977121|
|    min|            406.0|               0.0|
|    25%|          41237.0|               1.0|
|    50%|          75556.0|              9.21|
|    75%|         128471.0|            105.89|
|    max|         170348.0|           2125.87|
+-------+-----------------+------------------+

클래스가 불균형한 데이터로 직접적인 비교가 어렵지만 거래 유형이 정산인 경우에 비해 사기거래인 경우가 평균적으로 거래시간이 짧고 거래금액은 크다. 또한, 거래금액과 거래시간의 분포를 확인해 보면 아래와 같다. 나머지 변수 v1 ~ v28은 이미 독립된 주성분으로 따로 탐색 진행은 하지 않았다.

pd_data = data.toPandas()
amount_val = pd_data['Amount'].values
time_val = pd_data['Time'].values

plt.subplots(constrained_layout=True)

plt.subplot(2, 1, 1)
plt.title('Transaction Amount Distribution')
plt.hist(amount_val, bins=50)

plt.subplot(2, 1, 2)
plt.title('Transaction Time Distribution')
plt.hist(time_val, bins=50)

plt.show()

거래금액(Amount)와 거래시간(Time)분포를 확인해본 결과 거래금액은 왼쪽으로 치우쳐진 분포이며 거래시간을 보면 거래가 활발하게 이루어지는 시간과 아닌 시간이 구분되어 있다.

import numpy as np
pd_data["log_Amount"] = np.log(pd_data["Amount"] + 1) 

amount_val1 = pd_data['log_Amount'].values
plt.subplots(constrained_layout=True)
plt.subplot(2, 1, 1)
plt.title('Transaction log_Amount Distribution')
plt.hist(amount_val1, bins=50)

해당 분포는 거래금액에 log값을 취한 분포이다. pyspark로는 아래와같이 expr함수를 이용해 log를 취한 컬럼 생성이 가능하다.

from pyspark.sql.functions import expr
data = data.withColumn("log_amount", expr("log(Amount) + 1"))
data.select("Amount", "log_amount").show()

+------+------------------+
|Amount|        log_amount|
+------+------------------+
|149.62| 6.008098746444259|
|  2.69|1.9895411936137477|
|378.66| 6.936638704824203|
| 123.5| 5.816241156068032|
| 69.99| 5.248352374701448|
|  3.67| 2.300191662066479|
|  4.99|2.6074359097634274|
|  40.8| 4.708682081410116|
|  93.2| 5.534747721691546|
|  3.68|2.3029127521808395|
|   7.8|3.0541237336955462|
|  9.99| 3.301584592660462|
| 121.5| 5.799914262780603|
|  27.5| 4.314186004672526|
|  58.8| 5.074141854904581|
| 15.99| 3.771963526845863|
| 12.99|3.5641798306825083|
|  0.89|0.8834661837440485|
|  46.8| 4.845883202923601|
|   5.0|2.6094379124341005|
+------+------------------+
only showing top 20 rows

2부에서는 해당 데이터에서 성능지표를 정의하고, 3부에서는 실제 모델링 파이프라인을 구축해 모델 결과를 산출, 4부에서는 모델 해석 및 이슈, 보완점에 대해 논의 하려고 한다.

300x250
반응형