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

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

Введение данных

  • ID: представляет номер строки транзакции в данных.
  • TRANSACTION_ID: представляет собой уникальный идентификатор для каждой транзакции.
  • TX_DATETIME: представляет дату и время транзакции.
  • CUSTOMER_ID: уникальный идентификатор клиента, совершившего транзакцию.
  • TERMINAL_ID: уникальный идентификатор терминала, на котором была совершена транзакция.
  • TX_AMOUNT: представляет сумму транзакции.
  • TX_TIME_SECONDS: представляет продолжительность транзакции в секундах.
  • TX_TIME_DAYS: представляет продолжительность транзакции в днях.
  • TX_FRAUD: указывает, является ли транзакция мошеннической или нет, где 0 представляет законную транзакцию, а 1 представляет мошенническая транзакция.
  • TX_FRAUD_SCENARIO: представляет тип обнаруженного мошеннического сценария, если таковой имеется. Значение 0 указывает на отсутствие мошенничества, а другие значения указывают на различные типы сценариев мошенничества.

Шаг 1 — проверка и предварительная обработка данных

Во-первых, мы импортируем библиотеки, которые будем использовать в этом проекте. Затем мы читаем CSV-файл, содержащий набор данных, который мы будем использовать для анализа.

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

df = pd.read_csv('/Final Transactions.csv')

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

df.shape

(1754155, 10)


df.head(10)

Мы видим, что набор данных имеет 1 754 155 строк и 10 столбцов. Целевой функцией является столбец TX_FRAUD.

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

df.dtypes

Unnamed: 0             int64
TRANSACTION_ID         int64
TX_DATETIME           object
CUSTOMER_ID            int64
TERMINAL_ID            int64
TX_AMOUNT            float64
TX_TIME_SECONDS        int64
TX_TIME_DAYS           int64
TX_FRAUD               int64
TX_FRAUD_SCENARIO      int64
dtype: object

Мы видим несколько категориальных (объектных) признаков. Мы также видим, что целью является целое число, представленное как 1 для мошеннических транзакций и 0 для немошеннических транзакций. Это делает targetбинарной функцией.

df.head(10)

Далее мы рассмотрим статистические данные, такие как процентиль и среднее значение. а также проверить, есть ли какие-либо пропущенные значения в наборе данных.

df.describe().round(2)

df.isna().sum()


Unnamed: 0           0
TRANSACTION_ID       0
TX_DATETIME          0
CUSTOMER_ID          0
TERMINAL_ID          0
TX_AMOUNT            0
TX_TIME_SECONDS      0
TX_TIME_DAYS         0
TX_FRAUD             0
TX_FRAUD_SCENARIO    0
dtype: int64

Судя по выходным данным, во фрейме данных нет пропущенных значений, поэтому нам не нужно удалять или заменять какие-либо выборки из фрейма данных.

Шаг 2 — Исследовательский анализ данных

EDA, сокращенно от исследовательского анализа данных, представляет собой начальную фазу процесса обработки данных, цель которого — получить как можно больше информации о данных, сводя к минимуму время, затрачиваемое на это. С помощью EDA мы можем получить интуитивно понятный и практичный обзор данных, который включает в себя структуру набора данных, возможные подходы к очистке, целевую переменную и методы моделирования.

Мы изучим распределение целевой функции, чтобы изучить баланс целевой функции. Мы будем использовать диаграмму, чтобы построить распределение и получить интуитивное представление о распределении.

count1 = 0 
count0 = 0
for i in df['TX_FRAUD'].values:
    if i == 1:
        count1 += 1
    else:
        count0 += 1
        
count1 = (count1/len(df['TX_FRAUD']))*100
count0 = (count0/len(df['TX_FRAUD']))*100

x = ['Fraudulent Transaction(TARGET=1)','Legitimate Transaction(TARGET=0)']
y = [count1, count0]


explode = (0.1, 0)  # only "explode" the 1st slice

fig1, ax1 = plt.subplots()
ax1.pie(y, explode=explode, labels=x, autopct='%1.1f%%',
        shadow=True, startangle=110)
ax1.axis('equal')  # Equal aspect ratio ensures that pie is drawn as a circle.
plt.title('Data Distribution',fontsize=25)
plt.show()

Прежде всего, давайте выясним корреляцию между каждыми функциями в наборе данных.

df_corr = df.drop(['TRANSACTION_ID', 'Unnamed: 0','CUSTOMER_ID','TERMINAL_ID'], axis=1)
corr = df_corr.corr()

plt.figure(figsize=(6,6))
sns.heatmap(corr, cmap='RdBu_r', annot=True, vmax=1, vmin=-1)
plt.show()

Далее мы будем отображать количество мошеннических и немошеннических транзакций за каждый месяц, чтобы выяснить, есть ли какие-либо потенциальные тенденции или выбросы.

df["month"] = pd.DatetimeIndex(df["TX_DATETIME"]).month

grouped = df.groupby(["month", "TX_FRAUD"]).size().reset_index(name="count")

fig = px.bar(grouped, x="month", y="count", color="TX_FRAUD", barmode="group",
             labels={"month": "Month", "count": "Number of Transactions", "TX_FRAUD": "Transaction Type"})

fig.show()

Кроме того, мы также можем изучить частоту каждого сценария мошенничества для мошеннических транзакций.

df = df[(df['TX_FRAUD_SCENARIO'] != 0) & (df['TX_AMOUNT'] != 0)]
grouped = df.groupby("TX_FRAUD_SCENARIO").size().reset_index(name="count")

fig = px.bar(grouped, x="TX_FRAUD_SCENARIO", y="count", color="TX_FRAUD_SCENARIO",
             labels={"TX_FRAUD_SCENARIO": "Fraud Scenario", "count": "Number of Transactions"})

fig.show()

Кроме того, мы можем глубже изучить мошеннические транзакции, извлекая среднюю транзакцию для каждого сценария мошенничества.

df = df[(df['TX_FRAUD_SCENARIO'] != 0) & (df['TX_AMOUNT'] != 0)]

grouped = df.groupby('TX_FRAUD_SCENARIO')['TX_AMOUNT'].mean().reset_index(name='mean_amount')

fig = px.bar(grouped, x='TX_FRAUD_SCENARIO', y='mean_amount', labels={'TX_FRAUD_SCENARIO':'Fraud Scenario','mean_amount':'Average Transaction Amount'})

fig.show()

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

df['TX_DATETIME'] = pd.to_datetime(df['TX_DATETIME'])

df['date'] = df['TX_DATETIME'].dt.date

grouped = df[df['TX_FRAUD'] == 1].groupby('date').size().reset_index(name='count')

fig = px.line(grouped, x='date', y='count', labels={'date':'Date','count':'Number of Fraud Transactions'})

fig.show()

Шаг 3. Выбор показателей

Основным приоритетом в обнаружении мошенничества с кредитными картами является минимизация количества ложноотрицательных результатов, поскольку они могут привести к значительным финансовым потерям. Крайне важно принять необходимые меры для предотвращения мошеннических транзакций, и любой случай классификации мошеннических транзакций как законных транзакций будет крайне пагубным.

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

Шаг 4 — Манипуляции с данными

Выбор функций

df_features = df.drop(['TRANSACTION_ID', 'TX_DATETIME','Unnamed: 0','CUSTOMER_ID','TERMINAL_ID','TX_FRAUD_SCENARIO'], axis=1)

print(df_features.head())

Стратифицированное разделение набора данных для обучения и тестирования

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

from sklearn.model_selection import train_test_split

X = df_features.drop(["TX_FRAUD"], axis=1)
y = df_features["TX_FRAUD"]

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

Масштабирование функций

from sklearn.preprocessing import StandardScaler

def Standard_Scaler (df, col_names):
    features = df_features[col_names]
    scaler = StandardScaler().fit(features.values)
    features = scaler.transform(features.values)
    df_features[col_names] = features
    
    return df

col_names = ["TX_FRAUD"]
X_train = Standard_Scaler (X_train, col_names)
X_test = Standard_Scaler (X_test, col_names)

Шаг 4. Моделирование

4.1 Классификация случайного леса

4.1.1 Без передискретизации

Стратифицированная K-кратная перекрестная проверка

Самая оптимальная модель машинного обучения — это не та, которая просто дает точные прогнозы на обучающих данных, а та, которая хорошо работает с новыми данными, избегая при этом таких проблем, как переобучение и недообучение.

Перекрестная проверка — это метод, используемый для оценки моделей машинного обучения на небольшой выборке данных. Этот метод включает разбиение выборки данных на K групп, где K — единственный параметр, определяющий количество групп. Этот процесс известен как K-кратная перекрестная проверка.

from sklearn.model_selection import StratifiedKFold
from sklearn.ensemble import RandomForestClassifier

#We are going to ensure that we have the same splits of the data every time. 
#We can ensure this by creating a K-Fold object, kf, and passing cv=kf instead of the more common cv=5.

kf = StratifiedKFold(n_splits=5, shuffle=False)
rf = RandomForestClassifier(n_estimators=100, random_state=13)

Напомним: Способность модели находить все соответствующие наблюдения в наборе данных.

Количество истинных положительных результатов, деленное на количество истинных положительных результатов плюс количество ложноотрицательных результатов.

from sklearn.model_selection import cross_val_score

score = cross_val_score(rf, X_train, y_train, cv=kf, scoring='recall')
print("Cross Validation Recall scores are: {}".format(score))
print("Average Cross Validation Recall score: {}".format(score.mean()))

Настройка гиперпараметров с помощью GridSearchCV

GridSearchCV — это метод, используемый для определения идеальных значений гиперпараметров из указанной сетки параметров, и, по сути, это метод перекрестной проверки.

Хотя параметры, определенные GridSearchCV, считаются «наилучшими» возможными значениями, они оптимальны только в пределах параметров, включенных в сетку параметров.

from sklearn.model_selection import GridSearchCV

params = {
    'n_estimators': [1, 10, 20],
    'max_depth': [2, 4, 6, 8],
    'random_state': [13]
}

grid_rf = GridSearchCV(rf, param_grid=params, cv=kf, 
                          scoring='recall').fit(X_train, y_train)
print('Best parameters:', grid_rf.best_params_)
print('Best score:', grid_rf.best_score_)

y_pred = grid_rf.predict(X_test)
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.metrics import precision_score, recall_score, f1_score

cm = confusion_matrix(y_test, y_pred)

rf_Recall = recall_score(y_test, y_pred)
rf_Precision = precision_score(y_test, y_pred)
rf_f1 = f1_score(y_test, y_pred)
rf_accuracy = accuracy_score(y_test, y_pred)

print(cm)

[[455456      0]
 [  2471  68320]]

Давайте посмотрим на сводку баллов по тестовому набору.

ndf = [(rf_Recall, rf_Precision, rf_f1, rf_accuracy)]

rf_score = pd.DataFrame(data = ndf, columns=['Recall','Precision','F1 Score', 'Accuracy'])
rf_score.insert(0, 'Random Forest with', 'No Under/Oversampling')
rf_score

4.1.2 Случайная передискретизация

Случайная избыточная выборка включает в себя создание новой преобразованной версии обучающего набора данных путем дублирования примеров в классе меньшинства.

from imblearn.over_sampling import RandomOverSampler
from collections import Counter

# Randomly over sample the minority class
ros = RandomOverSampler(random_state=42)
X_train_ros, y_train_ros= ros.fit_resample(X_train, y_train)
# Check the number of records after over sampling
print(sorted(Counter(y_train_ros).items()))


[(0, 1062730), (1, 1062730)]

Мы будем визуализировать распределение данных после передискретизации, используя пир-диаграмму.

count1 = 0 
count0 = 0
for i in y_train_ros.values:
    if i == 1:
        count1 += 1
    else:
        count0 += 1
        
count1 = (count1/len(y_train_ros))*100
count0 = (count0/len(y_train_ros))*100

x = ['Fraudulent Transaction(TARGET=1)','Legitimate Transaction(TARGET=0)']
y = [count1, count0]


explode = (0.1, 0)  # only "explode" the 1st slice

fig1, ax1 = plt.subplots()
ax1.pie(y, explode=explode, labels=x, autopct='%1.1f%%',
        shadow=True, startangle=110)
ax1.axis('equal')  # Equal aspect ratio ensures that pie is drawn as a circle.
plt.title('Data Distribution After Balancing',fontsize=25)
plt.show()

Конвейер

Цель конвейера состоит в том, чтобы собрать несколько шагов, которые могут быть проверены вместе при установке различных параметров.

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

Конвейерный подход является эффективным методом для выполнения этих задач.

from imblearn.pipeline import Pipeline, make_pipeline

random_overs_pipeline = make_pipeline(RandomOverSampler(random_state=42), 
                              RandomForestClassifier(n_estimators=20, random_state=13))
#cross_val_score(random_overs_pipeline, X_train, y_train, scoring='recall', cv=kf)
score2 = cross_val_score(random_overs_pipeline, X_train, y_train, scoring='recall', cv=kf)
print("Cross Validation Recall Scores are: {}".format(score2))
print("Average Cross Validation Recall score: {}".format(score2.mean()))

new_params = {'randomforestclassifier__' + key: params[key] for key in params}
grid_over_rf = GridSearchCV(random_overs_pipeline, param_grid=new_params, cv=kf, scoring='recall',
                        return_train_score=True)
grid_over_rf.fit(X_train, y_train)
print('Best parameters:', grid_over_rf.best_params_)
print('Best score:', grid_over_rf.best_score_)

y_pred = grid_over_rf.best_estimator_.named_steps['randomforestclassifier'].predict(X_test)

cm = confusion_matrix(y_test, y_pred)

over_rf_Recall = recall_score(y_test, y_pred)
over_rf_Precision = precision_score(y_test, y_pred)
over_rf_f1 = f1_score(y_test, y_pred)
over_rf_accuracy = accuracy_score(y_test, y_pred)

print(cm)


[[455456      0]
 [  2471  68320]]
ndf = [(over_rf_Recall, over_rf_Precision, over_rf_f1, over_rf_accuracy)]

over_rf_score = pd.DataFrame(data = ndf, columns=['Recall','Precision','F1 Score', 'Accuracy'])
over_rf_score.insert(0, 'Random Forest with', 'Random Oversampling')
over_rf_score

4.2 Классификация повышения градиента

Мы повторим те же шаги, что и в предыдущей модели.

4.2.1 Без избыточной выборки

from sklearn.ensemble import GradientBoostingClassifier

gb = GradientBoostingClassifier()

score3 = cross_val_score(gb, X_train, y_train, cv=kf, scoring='recall')
print("Cross Validation Recall scores are: {}".format(score3))
print("Average Cross Validation Recall Score: {}".format(score3.mean()))

params = {
    'n_estimators': [1, 10, 20],
    'max_depth': [2, 4, 6, 8],
    'random_state': [13]
}

grid_gb = GridSearchCV(gb, param_grid=params, cv=kf, 
                          scoring='recall').fit(X_train, y_train)
print('Best parameters:', grid_gb.best_params_)
print('Best score:', grid_gb.best_score_)

y_pred_GB = grid_gb.predict(X_test)

cm = confusion_matrix(y_test, y_pred_GB)

gb_Recall = recall_score(y_test, y_pred_GB)
gb_Precision = precision_score(y_test, y_pred_GB)
gb_f1 = f1_score(y_test, y_pred_GB)
gb_accuracy = accuracy_score(y_test, y_pred_GB)

print(cm)


[[455456      0]
 [  2471  68320]]
ndf = [(gb_Recall, gb_Precision, gb_f1, gb_accuracy)]

gb_score = pd.DataFrame(data = ndf, columns=['Recall','Precision','F1 Score', 'Accuracy'])
gb_score.insert(0, 'Gradient Boosting with', 'No Under/Oversampling')
gb_score

4.2.2 Случайная передискретизация

#Pipeline

gb_overs_pipeline = make_pipeline(RandomOverSampler(random_state=42), 
                              GradientBoostingClassifier(n_estimators=20, random_state=13))
score4 = cross_val_score(gb_overs_pipeline, X_train, y_train, scoring='recall', cv=kf)
print("Cross Validation Recall Scores are: {}".format(score4))
print("Average Cross Validation Recall score: {}".format(score4.mean()))

new_params = {'gradientboostingclassifier__' + key: params[key] for key in params}
grid_over_gb = GridSearchCV(gb_overs_pipeline, param_grid=new_params, cv=kf, scoring='recall',
                        return_train_score=True)
grid_over_gb.fit(X_train, y_train)

print('Best parameters:', grid_over_gb.best_params_)
print('Best score:', grid_over_gb.best_score_)

y_pred_overGB = grid_over_gb.best_estimator_.named_steps['gradientboostingclassifier'].predict(X_test)

cm = confusion_matrix(y_test, y_pred_overGB)

over_gb_Recall = recall_score(y_test, y_pred_overGB)
over_gb_Precision = precision_score(y_test, y_pred_overGB)
over_gb_f1 = f1_score(y_test, y_pred_overGB)
over_gb_accuracy = accuracy_score(y_test, y_pred_overGB)

print(cm)


[[455449      7]
 [  2471  68320]]
ndf = [(over_gb_Recall, over_gb_Precision, over_gb_f1, over_gb_accuracy)]

over_gb_score = pd.DataFrame(data = ndf, columns=['Recall','Precision','F1 Score', 'Accuracy'])
over_gb_score.insert(0, 'Gradient Boosting with', 'Oversampling')
over_gb_score

Заключение

Наконец, мы сравним производительность всех моделей в таблице.

predictions = pd.concat([rf_score, over_rf_score, gb_score, over_gb_score], ignore_index=True, sort=False)
predictions.sort_values(by=['Recall'], ascending=False)

Подводя итог, можно сделать следующие наблюдения на основе таблицы:

  • Все модели имеют одинаковый показатель отзыва, который составляет 96,5%.
  • Модель Gradient Boosting Oversampling имеет более низкую точность и показатель точности по сравнению с другими моделями.

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

Методы повторной выборки:

  • Неполная выборка
  • УДАР
  • Класс Вес

Метрики:

  • Кривая AUC-ROC