Вы подписаны на Машинное обучение доступно
Отлично! завершите оплату для полного доступа к Машинное обучение доступно
Снова приветствуем Вас! Вы успешно авторизовались
Успех! Аккаунт активирован, и Вы имеете полный доступ к контенту.
Обнаружение мошеннических операций (Fraud Detection)

Обнаружение мошеннических операций (Fraud Detection)

in

Обнаружение мошеннических операций – одна из популярнейших задач Машинного обучения (ML), нацеленная на выделение правонарушений из общего потока событий. Рассмотрим в качестве примера распознавание воровства средств с банковских карт.

Для начала импортируем необходимые библиотеки:

import pandas as pd
import numpy as np

import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler
from xgboost import XGBClassifier
from xgboost import plot_importance
import xgboost as xgb

from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold
from sklearn.model_selection import GridSearchCV

from sklearn.metrics import classification_report
from sklearn.model_selection import cross_val_predict
from sklearn.ensemble import RandomForestClassifier
from sklearn import svm
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import MLPClassifier

from sklearn.metrics import confusion_matrix
from sklearn import metrics
from sklearn.metrics import roc_auc_score
from sklearn.metrics import average_precision_score
from sklearn.metrics import roc_curve, auc

Импортируем хронологию операций:

data = pd.read_csv('../input/creditcardfraud/creditcard.csv')

Посмотрим, из чего состоит Датасет (Dataset):

data.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

Кроме Признаков (Feature) «Время» (Time), «Количество» (Amount) и «Класс» (Class) другие не стоит интерпретировать в одиночку. Но все мы знаем, что значения столбцов V1 - V28 были преобразованы с помощью Анализа главных компонент (PCA). Эти загадочные колонки – результат защиты конфиденциальных данных пользователей.

<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

Посмотрим, насколько наши данные сбалансированы:

plt.figure(figsize=(10,10))
sns.countplot(
    y="Class", 
    data=data,
    facecolor=(0, 0, 0, 0),
    linewidth=5, 
    edgecolor=sns.color_palette("dark", 2))

plt.title('Fraudulent Transaction Summary')
plt.xlabel('Count')
plt.ylabel('Fraudulent Transaction   Non-Fraudulent Transaction', fontsize=12)

Мы имеем дело с Несбалансированным датасетом (Imbalanced Dataset), то есть соотношение представителей класса неравное.

На столбчатой диаграмме почти не видно красный столбец с транзакциями мошенников 

График показывает, что существует огромная разница между классами операций. Несбалансированные данные могут вызвать проблемы Классификации (Classification), такие как неправильная Точность (Accuracy). В этом проекте мы будем использовать Метод удаления примеров мажоритарного класса (Undersampling Method).

Преобразуем признак "Класс" в категориальный:

data['Class']= data['Class'].astype('category')

Посмотрим, как транзакции распределены по времени. Time –  это количество секунд, прошедших между рассматриваемой и первой транзакцией в наборе данных:

plt.figure(figsize=(15,10))
sns.distplot(data['Time'])

Следующим делом посмотрим на распределение признака "Количество":

plt.figure(figsize=(10,10))
sns.distplot(data['Amount'])

Приведенные выше графики показывают, что столбцы "Время" и "Количество" необходимо подвергнуть Стандартизации (Standartization). Этот метод позволит создавать признаки, которые имеют схожие диапазоны значений.

Перед стандартизацией я хочу создать функцию «Час», которая поможет лучше использовать «Время» и его связь с остальными столбцами.

data['Hour'] = data['Time'].apply(lambda x: np.ceil(float(x)/3600) % 24)

pd.pivot_table(
    columns="Class", 
    index="Hour", 
    values= 'Amount', 
    aggfunc='count', 
    data=data)

Посмотрим, в какое время дня мошенники наиболее активны и сравним с активностью нормальных операций:

#Hour vs Class
fig, axes = plt.subplots(2, 1, figsize=(15, 10))

sns.countplot(
    x="Hour",
    data=data[data['Class'] == 0], 
    color="#98D8D8",  
    ax=axes[0])
axes[0].set_title("Non-Fraudulent Transaction")


sns.countplot(
    x="Hour",
    data=data[data['Class'] == 1],
    color="#F08030", 
    ax=axes[1])
axes[1].set_title("Fraudulent Transaction")

Приведенные выше графики показывают, что обычные и мошеннические транзакции совершались каждый час. Для мошеннических транзакций третий и двенадцатый часы – самые "горячие".

Понижение размерности

Результаты исследования данных показывают, что набор данных большой, а размеры классов несбалансированы, поэтому уменьшение размерности поможет интерпретировать результаты. Для этого будет использоваться Стохастическое вложение соседей с t-распределением (t-SNE). Этот метод хорошо работает с данными большого размера и "проецирует" их в двух- или трехмерном пространстве.

data_nonfraud = data[data['Class'] == 0].sample(2000)
data_fraud  = data[data['Class'] == 1]

data_new = data_nonfraud.append(data_fraud).sample(frac=1)
X = data_new.drop(['Class'], axis = 1).values
y = data_new['Class'].values

tsne = TSNE(n_components=2, random_state=42)
X_transformation = tsne.fit_transform(X)

plt.figure(figsize=(10, 10))
plt.title("t-SNE Dimensionality Reduction")

def plot_data(X, y):
    plt.scatter(X[y == 0, 0], X[y == 0, 1], label="Non_Fraudulent", alpha=0.5, linewidth=0.15, c='#17becf')
    plt.scatter(X[y == 1, 0], X[y == 1, 1], label="Fraudulent", alpha=0.5, linewidth=0.15, c='#d62728')
    plt.legend()
    return plt.show()

plot_data(X_transformation, y)

На приведенном выше графике показано, что мошеннические и нормальные транзакции плохо разделены на два разных кластера в двухмерном пространстве. Это означает, что два типа операций сильно похожи. Также этот график демонстрирует, что показаний точности недостаточно для выбора лучшего алгоритма.

Стандартизация

data[['Time', 'Amount']] = StandardScaler().fit_transform(data[['Time', 'Amount']])

Оптимизация гиперпараметров

Этот метод помогает найти оптимальные параметры для алгоритмов машинного обучения. Алгоритм поиска по сетке (Grid Search) будет использоваться для настройки Гиперпараметров (Hyperparameter). Затем будет выполнен Экстремальный бустинг градиента (XGBoost) для построения графика Важности признаков (Feature Importance). Этот график помогает выбрать параметры, которые будут использоваться в Модели (Model).

train_data, label_data = data.iloc[:,:-1],data.iloc[:,-1]

data_dmatrix = xgb.DMatrix(data=train_data, label= label_data)

X_train, X_test, y_train, y_test = train_test_split(
                                    train_data, label_data, test_size=0.3,random_state=42)
                                    
params = {
    'objective':'reg:logistic',
    'colsample_bytree': 0.3,
    'learning_rate': 0.1,
    'bootstrap': True, 
    'criterion': 'gini', 
    'max_depth': 4, 
    'max_features': 'auto', 
    'n_estimators': 50
}
xg_reg = xgb.train(params=params, dtrain=data_dmatrix, num_boost_round=10)

#Feature importance graph
plt.rcParams['figure.figsize'] = [20, 10]
xgb.plot_importance(xg_reg)
График важности признаков

На приведенном выше графике показано, что самый важный столбец – это V16. Параметры с наименьшей важностью - V13, V25, Time, V20, V22, V8, V15, V19 и V2 будут удалены из данных перед построением модели.

data_model = data.drop(['V13', 'V25', 'Time', 'V20', 'V22', 'V8', 'V15', 'V19', 'V2'], axis=1)

Метод удаления примеров мажоритарного класса

Перед построением модели будет применен метод случайного недосэмплирования. В этом проекте было выбрано 5% нормальных транзакций.

data_under_nonfraud = data_model[data_model['Class'] == 0].sample(15000)
data_under_fraud  = data_model[data_model['Class'] == 1]

data_undersampling = data_under_nonfraud.append(data_under_fraud, 
                                                ignore_index=True, sort=False)
                                                
plt.figure(figsize=(10,10))
sns.countplot(y="Class", data=data_undersampling,palette='Dark2')
plt.title('Fraudulent Transaction Summary')
plt.xlabel('Count')
plt.ylabel('Fraudulent Transaction,        Non-Fraudulent Transaction')

Новые данные будут случайным образом разделены на Тренировочные данные (Train Data) и Тестовые данные (Test Data). Доля первых составляет 70%, вторых – 30%.

k-блочная кросс-валидация

kfold_cv=KFold(n_splits=5, random_state=42, shuffle=True)

for train_index, test_index in kfold_cv.split(X,y):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]

Случайный лес

modelRF = RandomForestClassifier(
    n_estimators=500, 
    criterion = 'gini', 
    max_depth = 4, 
    class_weight='balanced', 
    random_state=42
).fit(X_train, y_train)

# Obtain predictions from the test data 
predict_RF = modelRF.predict(X_test)

Метод опорных векторов

modelSVM = svm.SVC(
    kernel='rbf', 
    class_weight='balanced', 
    gamma='scale', 
    probability=True, 
    random_state=42
).fit(X_train, y_train)

# Obtain predictions from the test data 
predict_SVM = modelSVM.predict(X_test)

Логистическая регрессия

modelLR = LogisticRegression(
    solver='lbfgs', 
    multi_class='multinomial',
    class_weight='balanced', 
    max_iter=500, 
    random_state=42
).fit(X_train, y_train)

# Obtain predictions from the test data 
predict_LR = modelLR.predict(X_test)

Многослойный перцептрон

modelMLP = MLPClassifier(
    solver='lbfgs', 
    activation='logistic', 
    hidden_layer_sizes=(100,),
    learning_rate='constant', 
    max_iter=1500, 
    random_state=42
).fit(X_train, y_train)

# Obtain predictions from the test data 
predict_MLP = modelMLP.predict(X_test)

Сравнение методов

RF_matrix = confusion_matrix(y_test, predict_RF)
SVM_matrix = confusion_matrix(y_test, predict_SVM)
LR_matrix = confusion_matrix(y_test, predict_LR)
MLP_matrix = confusion_matrix(y_test, predict_MLP) 

fig, ax = plt.subplots(1, 2, figsize=(15, 8))

sns.heatmap(RF_matrix, annot=True, fmt="d",cbar=False, cmap="Paired", ax = ax[0])
ax[0].set_title("Random Forest", weight='bold')
ax[0].set_xlabel('Predicted Labels')
ax[0].set_ylabel('Actual Labels')
ax[0].yaxis.set_ticklabels(['Non-Fraud', 'Fraud'])
ax[0].xaxis.set_ticklabels(['Non-Fraud', 'Fraud'])

sns.heatmap(SVM_matrix, annot=True, fmt="d",cbar=False, cmap="Dark2", ax = ax[1])
ax[1].set_title("Support Vector Machine", weight='bold')
ax[1].set_xlabel('Predicted Labels')
ax[1].set_ylabel('Actual Labels')
ax[1].yaxis.set_ticklabels(['Non-Fraud', 'Fraud'])
ax[1].xaxis.set_ticklabels(['Non-Fraud', 'Fraud'])

fig, axe = plt.subplots(1, 2, figsize=(15, 8))

sns.heatmap(LR_matrix, annot=True, fmt="d",cbar=False, cmap="Pastel1", ax = axe[0])
axe[0].set_title("Logistic Regression", weight='bold')
axe[0].set_xlabel('Predicted Labels')
axe[0].set_ylabel('Actual Labels')
axe[0].yaxis.set_ticklabels(['Non-Fraud', 'Fraud'])
axe[0].xaxis.set_ticklabels(['Non-Fraud', 'Fraud'])

sns.heatmap(MLP_matrix, annot=True, fmt="d",cbar=False, cmap="Pastel1", ax = axe[1])
axe[1].set_title("Multilayer Perceptron", weight='bold')
axe[1].set_xlabel('Predicted Labels')
axe[1].set_ylabel('Actual Labels')
axe[1].yaxis.set_ticklabels(['Non-Fraud', 'Fraud'])
axe[1].xaxis.set_ticklabels(['Non-Fraud', 'Fraud'])

Для несбалансированных данных результаты матрицы путаницы могут быть неверными. Однако полезно сказать, сколько мошеннических транзакций предсказано верно. На основе графиков Многослойного персептрона (MLP), Случайного леса (Random Forest) и Логистической регрессии (Logistic Regression) предсказывают одну и ту же долю мошеннических транзакций (сумма нижних двух ячеек каждой из матриц равна 109).

print("Classification_RF:")
print(classification_report(y_test, predict_RF))
print("Classification_SVM:")
print(classification_report(y_test, predict_SVM))
print("Classification_LR:")
print(classification_report(y_test, predict_LR))
print("Classification_MLP:")
print(classification_report(y_test, predict_MLP))

В приведенной ниже таблице показаны результаты по точности, Отзыву (Recall) и Критерий F1 (F1 Score).

  • Модель логистической регрессии  имеет самый высокий уровень отзыва. Это означает, что она лучше "разыскивает" фактическую мошенническую транзакцию. Однако, когда мы смотрим на показатель точности, логистическая регрессия показывает один из самых худших результатов.
  • Наивысший удалось достигнуть случайному лесу. Высокая точность связана с низким уровнем ложных срабатываний, поэтому можно сказать, что модель случайного леса предсказывает наименьшее количество ложных мошеннических транзакций.
  • Критерий F1 дает лучшее объяснение на том основании, что он рассчитывается из Гармонических средних значений (Harmonic Mean) точности и отзыва. F1 – это лучшая метрика для выбора наиболее предсказуемой модели. В свете этой информации мы можем сказать, что алгоритм Случайного леса является наилучшим.
Classification_RF:
              precision    recall  f1-score   support

           0       0.98      0.99      0.99       389
           1       0.98      0.92      0.95       109

    accuracy                           0.98       498
   macro avg       0.98      0.96      0.97       498
weighted avg       0.98      0.98      0.98       498

Classification_SVM:
              precision    recall  f1-score   support

           0       0.83      0.50      0.62       389
           1       0.26      0.64      0.37       109

    accuracy                           0.53       498
   macro avg       0.55      0.57      0.50       498
weighted avg       0.71      0.53      0.57       498

Classification_LR:
              precision    recall  f1-score   support

           0       0.98      0.96      0.97       389
           1       0.86      0.94      0.89       109

    accuracy                           0.95       498
   macro avg       0.92      0.95      0.93       498
weighted avg       0.95      0.95      0.95       498

Classification_MLP:
              precision    recall  f1-score   support

           0       0.86      1.00      0.92       389
           1       0.98      0.41      0.58       109

    accuracy                           0.87       498
   macro avg       0.92      0.71      0.75       498
weighted avg       0.88      0.87      0.85       498

Окончательное сравнение будет выполнено с ROC-кривая (ROC AUC):

#RF AUC
rf_predict_probabilities = modelRF.predict_proba(X_test)[:,1]
rf_fpr, rf_tpr, _ = roc_curve(y_test, rf_predict_probabilities)
rf_roc_auc = auc(rf_fpr, rf_tpr)

#SVM AUC
svm_predict_probabilities = modelSVM.predict_proba(X_test)[:,1]
svm_fpr, svm_tpr, _ = roc_curve(y_test, svm_predict_probabilities)
svm_roc_auc = auc(svm_fpr, svm_tpr)

#LR AUC
lr_predict_probabilities = modelLR.predict_proba(X_test)[:,1]
lr_fpr, lr_tpr, _ = roc_curve(y_test, lr_predict_probabilities)
lr_roc_auc = auc(lr_fpr, lr_tpr)

#MLP AUC
mlp_predict_probabilities = modelMLP.predict_proba(X_test)[:,1]
mlp_fpr, mlp_tpr, _ = roc_curve(y_test, mlp_predict_probabilities)
mlp_roc_auc = auc(mlp_fpr, mlp_tpr)
plt.figure()
plt.plot(rf_fpr, rf_tpr, color='red',lw=2,
         label='Random Forest (area = %0.2f)' % rf_roc_auc)

plt.plot(svm_fpr, svm_tpr, color='blue',lw=2, 
         label='Support Vector Machine (area = %0.2f)' % svm_roc_auc)

plt.plot(lr_fpr, lr_tpr, color='green',lw=2, 
         label='Logistic Regression (area = %0.2f)' % lr_roc_auc)

plt.plot(mlp_fpr, mlp_tpr, color='orange',lw=2, 
         label='Multilayer Perceptron (area = %0.2f)' % mlp_roc_auc)

plt.plot([0, 1], [0, 1], color='black', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend(loc="lower right")
plt.show()

Основываясь на кривой, мы можем сказать, что алгоритмы логистической регрессии, случайного леса и нейронной сети – многослойного персептрона имеют почти одинаковые результаты AUC. У отличной модели AUC близка к 1, что означает, что у нее хороший показатель отделимости.

Этот вывод можно продемонстрировать и по результатам кривой ROC. Эти алгоритмы склоняются к истинно положительной скорости, а не к ложноположительной. В результате можно сказать, что эти алгоритмы имеют хорошую производительность классификации.

Наконец, мы можем вычислить средний балл точности для этих трех моделей. Результаты показывают, что все модели имеют почти одинаковый балл.

print("Average precision score of Logistic Regression", average_precision_score(y_test, modelLR.predict_proba(X_test)[:,1]))
print("Average precision score of Random Forest", average_precision_score(y_test, modelRF.predict_proba(X_test)[:,1]))
print("Average precision score of Multilayer Perceptron", average_precision_score(y_test, modelMLP.predict_proba(X_test)[:,1]))
Average precision score of Logistic Regression 0.9651191598439374
Average precision score of Random Forest 0.9728045908653973
Average precision score of Multilayer Perceptron 0.8624254915524178

Ноутбук, не требующий дополнительной настройки на момент написания статьи, можно скачать здесь.

Автор: Akashdeep Kuila