본문으로 바로가기

은행 분야의 데이터 사이언스: 이상거래 탐지

가장 흔한 활용 사례 중 하나인 이상거래(사기) 탐지를 통해 은행 부문에서 데이터 사이언스가 어떻게 구현되는지 알아보세요.
업데이트됨 2026년 8월 31일  · 11분 읽다

AI로 탐색하기

ChatGPTClaudePerplexity

핀테크 개념 일러스트

은행은 역사적으로 많은 구조화된 데이터를 축적해 온 행운아 중 하나이며, 데이터 사이언스 기술이 가장 먼저 적용된 분야이기도 합니다.

그렇다면 데이터 사이언스는 은행에서 어떻게 활용될까요? 오늘날 이 분야에서 데이터는 가장 가치 있는 자산이 되었습니다. 데이터 사이언스는 은행이 경쟁사를 따라잡고 더 많은 고객을 유치하며 기존 고객의 충성도를 높이고, 보다 효율적인 데이터 기반 의사결정을 내리고, 비즈니스를 강화하고, 운영 효율을 개선하고, 기존 서비스/제품을 개선하고 새로운 것을 도입하며, 보안을 강화하고, 그 결과로 더 많은 수익을 창출하는 데 필수적입니다. 데이터 사이언스 인력 수요의 상당수가 은행에서 발생하는 것은 놀랍지 않습니다.

데이터 사이언스는 은행 산업이 다음과 같은 다양한 업무를 성공적으로 수행하도록 돕습니다: 

  • 투자 리스크 분석
  • 고객 생애가치 예측
  • 고객 세분화
  • 이탈률 예측
  • 맞춤형 마케팅
  • 고객 감성 분석
  • 가상 비서 및 챗봇

아래에서는 은행에서 가장 흔한 데이터 사이언스 활용 사례 중 하나를 자세히 살펴보겠습니다.

은행 분야 데이터 사이언스 활용: 이상거래 탐지 

사기 행위는 은행뿐 아니라 정부, 보험, 공공 부문, 판매, 헬스케어 등 많은 분야에서 어려운 문제입니다. 온라인 거래가 많은 비즈니스는 사기 위험이 큽니다. 금융 범죄는 사기성 신용카드 거래, 위조 수표, 탈세, 자금 세탁, 사이버 공격, 고객 계정 도난, 합성 신원, 허위 신청, 피싱 사기 등 다양한 형태로 나타납니다.

사기 탐지는 사기 행위와 재무 손실을 식별하고 예방하기 위한 선제적 조치들의 집합입니다. 핵심 분석 기법은 두 범주로 나눌 수 있습니다:

  • 통계적 접근: 통계 파라미터 계산, 회귀, 확률분포, 데이터 매칭
  • 인공지능(AI): 데이터 마이닝, 머신러닝, 딥러닝

머신러닝은 사기 탐지의 핵심 축입니다. 머신러닝 도구 상자는 두 가지 접근을 제공합니다:

  • 지도학습: k-최근접 이웃, 로지스틱 회귀, 서포트 벡터 머신, 의사결정나무, 랜덤 포레스트, 시계열 분석, 신경망 등
  • 비지도학습: 군집 분석, 링크 분석, 자기조직화 지도, 주성분 분석, 이상치 탐지 등

사기 탐지에 보편적이고 신뢰할 만한 단일 머신러닝 알고리즘은 없습니다. 대신 실제 활용에서는 여러 기법 또는 그 조합을 시험하고, 예측 정확도를 계산해 최적의 접근을 선택합니다.

사기 탐지 시스템의 주요 과제는 끊임없이 변하는 사기 패턴과 범죄자들의 전술에 신속히 적응하고, 점점 정교해지는 새로운 수법을 빠르게 포착하는 것입니다. 사기 건수는 항상 소수이며 실제 거래 속에 잘 숨겨져 있습니다.

데이터셋 준비

이제 파이썬으로 신용카드 이상거래 탐지의 머신러닝 구현을 살펴보겠습니다. Kaggle의 Credit Card Fraud Detection 데이터셋을 기반으로 수정한 샘플인 creditcard_data 데이터셋을 사용합니다. 원본 데이터는 2013년 9월 이틀 동안 유럽 카드 소지자의 신용카드 거래를 나타냅니다.

데이터를 불러와 간단히 살펴보겠습니다:

import pandas as pd

creditcard_data = pd.read_csv('creditcard_data.csv', index_col=0)
print(creditcard_data.info())
print('\n')
pd.options.display.max_columns = len(creditcard_data)
print(creditcard_data.head(3))
<class 'pandas.core.frame.DataFrame'>
Int64Index: 5050 entries, 0 to 5049
Data columns (total 30 columns):
#   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
0   V1      5050 non-null   float64
1   V2      5050 non-null   float64
2   V3      5050 non-null   float64
3   V4      5050 non-null   float64
4   V5      5050 non-null   float64
5   V6      5050 non-null   float64
6   V7      5050 non-null   float64
7   V8      5050 non-null   float64
8   V9      5050 non-null   float64
9   V10     5050 non-null   float64
10  V11     5050 non-null   float64
11  V12     5050 non-null   float64
12  V13     5050 non-null   float64
13  V14     5050 non-null   float64
14  V15     5050 non-null   float64
15  V16     5050 non-null   float64
16  V17     5050 non-null   float64
17  V18     5050 non-null   float64
18  V19     5050 non-null   float64
19  V20     5050 non-null   float64
20  V21     5050 non-null   float64
21  V22     5050 non-null   float64
22  V23     5050 non-null   float64
23  V24     5050 non-null   float64
24  V25     5050 non-null   float64
25  V26     5050 non-null   float64
26  V27     5050 non-null   float64
27  V28     5050 non-null   float64
28  Amount  5050 non-null   float64
29  Class   5050 non-null   int64 
dtypes: float64(29), int64(1)
memory usage: 1.2 MB


        V1        V2        V3        V4        V5        V6        V7  \
0  1.725265 -1.337256 -1.012687 -0.361656 -1.431611 -1.098681 -0.842274  
1  0.683254 -1.681875  0.533349 -0.326064 -1.455603  0.101832 -0.520590  
2  1.067973 -0.656667  1.029738  0.253899 -1.172715  0.073232 -0.745771  

        V8        V9       V10       V11       V12       V13       V14  \
0 -0.026594 -0.032409  0.215113  1.618952 -0.654046 -1.442665 -1.546538  
1  0.114036 -0.601760  0.444011  1.521570  0.499202 -0.127849 -0.237253  
2  0.249803  1.383057 -0.483771 -0.782780  0.005242 -1.273288 -0.269260  

        V15       V16       V17       V18       V19       V20       V21  \
0 -0.230008  1.785539  1.419793  0.071666  0.233031  0.275911  0.414524  
1 -0.752351  0.667190  0.724785 -1.736615  0.702088  0.638186  0.116898  
2  0.091287 -0.347973  0.495328 -0.925949  0.099138 -0.083859 -0.189315  

        V22       V23       V24       V25       V26       V27       V28  \
0  0.793434  0.028887  0.419421 -0.367529 -0.155634 -0.015768  0.010790  
1 -0.304605 -0.125547  0.244848  0.069163 -0.460712 -0.017068  0.063542  
2 -0.426743  0.079539  0.129692  0.002778  0.970498 -0.035056  0.017313  

  Amount  Class 
0  189.00      0 
1  315.17      0 
2   59.98      0 

데이터셋에는 다음 변수가 포함되어 있습니다:

  • V1부터 V28까지의 수치형 변수: PCA 변환으로 얻은 주성분입니다. 기밀성 문제로 원래 특성에 대한 배경 정보는 제공되지 않았습니다.
  • Amount 변수는 거래 금액을 나타냅니다.
  • Class 변수는 해당 거래가 사기(1)인지 아닌지(0)를 표시합니다.

사기는 그 특성상 어떤 거래 목록에서도 극히 소수입니다. 그러나 머신러닝 알고리즘은 일반적으로 데이터셋에 포함된 서로 다른 클래스의 비율이 어느 정도 비슷할 때 가장 잘 동작합니다. 그렇지 않으면 학습할 데이터가 부족합니다. 이를 클래스 불균형 문제라고 합니다.

데이터셋의 사기 비율 계산

데이터셋 전체 거래 중 사기 거래의 비율을 계산해 보겠습니다:

round(creditcard_data['Class'].value_counts()*100/len(creditcard_data)).convert_dtypes()
0    99
1     1
Name: Class, dtype: Int64

그리고 사기와 정상 데이터 포인트를 시각화하는 플롯을 만들어 보겠습니다:

import matplotlib.pyplot as plt
import numpy as np

def prep_data(df):
    X = df.iloc[:, 1:28]
    X = np.array(X).astype(float)
    y = df.iloc[:, 29]
    y = np.array(y).astype(float)
    return X, y

def plot_data(X, y):
    plt.scatter(X[y==0, 0], X[y==0, 1], label='Class #0', alpha=0.5, linewidth=0.15)
    plt.scatter(X[y==1, 0], X[y==1, 1], label='Class #1', alpha=0.5, linewidth=0.15, c='r')
    plt.legend()
    return plt.show()

X, y = prep_data(creditcard_data)

plot_data(X, y)

사기 거래 시각화

SMOTE로 데이터 균형 재조정 

사기 거래의 비율이 매우 낮아 클래스 불균형 문제가 있음을 확인했습니다. 이를 해결하기 위해 SMOTE(synthetic minority oversampling technique)로 데이터를 재균형할 수 있습니다. 무작위 오버샘플링과 달리 SMOTE는 단순히 관측치를 그대로 복제하지 않습니다. 대신, 사기 사례의 최근접 이웃 특성을 이용해 소수 클래스의 기존 관측치와 상당히 유사한 새로운 합성 샘플을 생성합니다. 이제 신용카드 데이터에 SMOTE를 적용해 보겠습니다:

from imblearn.over_sampling import SMOTE

method = SMOTE()
X_resampled, y_resampled = method.fit_resample(X, y)
plot_data(X_resampled, y_resampled)

SMOTE 그래프

보시다시피 SMOTE를 사용하면 소수 클래스의 관측치가 크게 늘어납니다. 이 접근의 결과를 더 잘 보기 위해 원본 데이터와 비교해 보겠습니다:

def compare_plot(X, y, X_resampled, y_resampled, method):
    f, (ax1, ax2) = plt.subplots(1, 2)
    c0 = ax1.scatter(X[y==0, 0], X[y==0, 1], label='Class #0',alpha=0.5)
    c1 = ax1.scatter(X[y==1, 0], X[y==1, 1], label='Class #1',alpha=0.5, c='r')
    ax1.set_title('Original set')
    ax2.scatter(X_resampled[y_resampled==0, 0], X_resampled[y_resampled==0, 1], label='Class #0', alpha=.5)
    ax2.scatter(X_resampled[y_resampled==1, 0], X_resampled[y_resampled==1, 1], label='Class #1', alpha=.5,c='r')
    ax2.set_title(method)
    plt.figlegend((c0, c1), ('Class #0', 'Class #1'), loc='lower center', ncol=2, labelspacing=0.)
    plt.tight_layout(pad=3)
    return plt.show()

print(f'Original set:\n'
      f'{pd.value_counts(pd.Series(y))}\n\n'
      f'SMOTE:\n'
      f'{pd.value_counts(pd.Series(y_resampled))}\n')

compare_plot(X, y, X_resampled, y_resampled, method='SMOTE')
Original set:
0.0    5000
1.0      50
dtype: int64


SMOTE:
0.0    5000
1.0    5000
dtype: int64

Smote 방식 그래프

따라서 SMOTE 방식은 데이터를 완전히 균형 있게 만들었고, 소수 클래스의 크기가 이제 다수 클래스와 동일해졌습니다.

곧 실무 적용으로 돌아가겠지만, 우선 원본 데이터로 돌아가 사기 사례를 탐지해 보겠습니다. "올드스쿨" 방식으로는 사기를 잡기 위한 규칙을 만드는 과정이 필요합니다. 예를 들어, 거래 위치가 비정상적이거나 거래 빈도가 수상하게 높은 경우 등이 있을 수 있습니다. 아이디어는 일반적인 통계, 흔히 평균값을 기준으로 임계값을 정의하고, 해당 임계값을 특성에 적용해 사기를 탐지하는 것입니다.

print(creditcard_data.groupby('Class').mean().round(3)[['V1', 'V3']])
        V1     V3
Class             
0      0.035  0.037
1     -4.985 -7.294

이번 사례에서는 V1 < -3 그리고 V3 < -5라는 조건을 적용해 보겠습니다. 그런 다음 이 접근의 성능을 평가하기 위해 표시된 사기 사례를 실제와 비교합니다:

creditcard_data['flag_as_fraud'] = np.where(np.logical_and(creditcard_data['V1']<-3, creditcard_data['V3']<-5), 1, 0)
print(pd.crosstab(creditcard_data['Class'], creditcard_data['flag_as_fraud'], rownames=['Actual Fraud'], colnames=['Flagged Fraud']))
Flagged Fraud     0   1
Actual Fraud          
0              4984  16
1                28  22

로지스틱 회귀 적용 

50건의 사기 중 22건을 탐지했지만 나머지 28건은 놓쳤고, 오탐도 16건이 발생했습니다. 머신러닝 기법이 이 결과를 능가할 수 있는지 확인해 보겠습니다.

신용카드 데이터에 간단한 로지스틱 회귀 분류 알고리즘을 구현해 사기 발생을 식별하고, 혼동 행렬로 결과를 시각화하겠습니다:

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)

lr = LogisticRegression()
lr.fit(X_train, y_train)
predictions = lr.predict(X_test)
print(pd.crosstab(y_test, predictions, rownames=['Actual Fraud'], colnames=['Flagged Fraud']))
Flagged Fraud   0.0  1.0
Actual Fraud           
0.0            1504    1
1.0             1      9

여기서는 모델 결과를 계산하는 데 전체 데이터의 30%인 테스트 셋만 사용하기 때문에 혼동 행렬에서 확인할 수 있는 관측치 수가 더 적다는 점에 유의하세요.

사기 탐지 비율이 더 높아졌습니다. 이전의 44%(50건 중 22건)와 비교해 90%(10건 중 9건)를 포착했습니다. 오탐도 훨씬 줄어 개선되었습니다.

이제 앞서 논의한 클래스 불균형 문제로 돌아가, 로지스틱 회귀 모델과 SMOTE 리샘플링을 결합해 예측 결과를 더 향상시킬 수 있는지 살펴보겠습니다. 이를 효율적으로 한 번에 수행하려면 파이프라인을 정의해 데이터를 처리하면 됩니다:

from imblearn.pipeline import Pipeline

# 파이프라인에서 사용할 리샘플링 기법과 ML 모델 정의
resampling = SMOTE()
lr = LogisticRegression()

pipeline = Pipeline([('SMOTE', resampling), ('Logistic Regression', lr)])
pipeline.fit(X_train, y_train)
predictions = pipeline.predict(X_test)
print(pd.crosstab(y_test, predictions, rownames=['Actual Fraud'], colnames=['Flagged Fraud']))
Flagged Fraud   0.0  1.0
Actual Fraud           
0.0            1496    9
1.0               1    9

보시다시피 이번 사례에서는 SMOTE가 개선을 가져오지 못했습니다. 여전히 90%의 사기를 잡았지만, 오탐이 약간 늘었습니다. 리샘플링이 모든 경우에 더 나은 결과를 보장하지는 않는다는 점이 설명이 됩니다. 사기 사례가 데이터 전반에 매우 분산되어 있으면, 최근접 이웃이 반드시 사기 사례가 아니므로 SMOTE 사용이 편향을 초래할 수 있습니다.

결론 

향후 방향으로, 로지스틱 회귀 모델의 정확도를 높이기 위해 알고리즘 파라미터를 튜닝할 수 있습니다. 데이터셋을 두 부분으로만 나누는 대신 K-겹 교차검증을 고려할 수도 있습니다. 마지막으로, 다른 머신러닝 알고리즘(예: 의사결정나무, 랜덤 포레스트)을 시도해 더 나은 결과가 나오는지 확인해 보세요. 

사기 탐지 모델 구현의 이론적·기술적 측면을 더 배우고 싶다면 Fraud Detection in Python 과정을 살펴보세요.

주제
데이터 사이언스
Machine Learning