Обнаружение мошеннических операций (Fraud Detection)
Обнаружение мошеннических операций – одна из популярнейших задач Машинного обучения (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-кривая (AUC ROC):
#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