Путь к обнаружению мошенничества с кредитными картами

Для краткости опущен некоторый код. За подробностями обращайтесь на мой GitHub.

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

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

  • Класс: Отсутствие мошенничества (0) и мошенничества (1)
  • Время: секунды, прошедшие с момента начала сбора данных.
  • Сумма: сумма транзакции.
  • V1-V28: функции, созданные с помощью PCA, что также сохраняет анонимность пользователей.

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

import pandas as pd
from sklearn.model_selection import train_test_split
credit = pd.read_csv('creditcard.csv')
credit_train, credit_test = train_test_split(credit, test_size=0.2)

Исследование данных

Сначала я решил заглянуть в функцию Время, чтобы узнать, каков был временной интервал.

import numpy as np
import matplotlib.pyplot as plt
bins = len(np.histogram_bin_edges(credit_train['Time'], 'auto'))
plt.hist(credit_train['Time'], bins=bins)

Из гистограммы мы можем видеть данные за два дня, где я предполагаю, что пики - дневные, а впадины - ночные. Поскольку это, кажется, обычная закономерность, я решил преобразовать Время в час дня. Для этого я сделал предположение, что полночь наступает в минимумах.

from sklearn.base import BaseEstimator, TransformerMixin
class TimeToHour(BaseEstimator, TransformerMixin)
    def __init__(self, copy=True):
        self.copy = bool(copy)
    def fit(self, X, y=None):
        bins = len(np.histogram_bin_edges(X['Time'], bins='auto'))
        cuts = pd.cut(X['Time'], bins)
        counts = cuts.value_counts()
        midnight = int(counts.index[-1].mid)
        self.midnight_ = midnight
        return self
    def transform(self, X):
        X = X.copy() if self.copy else X
        day_sec = 24 * 60 * 60
        hr_sec = 60 * 60
        X['Hour'] = ((X[Time] — self.midnight_) % day_sec) / hr_sec
        return X

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

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

import seaborn as sns
is_fraud = (credit_train['Class'] == 1)
nonfraud = credit_train[~is_fraud]
fraud = credit_train[is_fraud]
x, y = 'Hour', 'Amount'
fig, (ax0, ax1) = plt.subplots(1, 2, sharey=True, sharex=True)
sns.scatterplot(nonfraud[x], nonfraud[y], ax=ax0)
sns.scatterplot(fraud[x], fraud[y], ax=ax1)

Глядя на диаграмму рассеяния, трудно увидеть какие-либо закономерности из-за огромного диапазона значений y и плотно сгруппированных точек внизу. К счастью, мы можем применить логарифмы, чтобы исправить вертикальную проблему, и мы можем использовать шестиугольники, чтобы сгруппировать близлежащие значения в ячейки, чтобы оценить, какие регионы имеют наибольшую плотность. Обратите внимание: поскольку некоторые транзакции составляют 0,00 долларов США, я использовал журнал (Amount + 1), чтобы обойти неопределенные значения.

x, y = 'Hour', 'Log1pAmount'
fig, (ax0, ax1) = plt.subplots(1, 2, sharey=True, sharex=True)
ax0.hexbin(nonfraud[x], nonfraud[y], gridsize=10)
ax1.hexbin(fraud[x], fraud[y], gridsize=10)

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

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

from sklearn.neighbors import LocalOutlierFactor
lof = LocalOutlierFactor(contamination='auto')
nf_preds = lof.fit_predict(nonfraud)
nf_inliers = nonfraud[nf_preds == 1]
nf_outliers = nonfraud[nf_preds == -1]
fr_preds = lof.fit_predict(fraud)
fr_inliers = fraud[fr_preds == 1]
fr_outliers = fraud[fr_preds == -1]

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

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

Уравновешивание

Когда дело доходит до несбалансированных данных, нам следует подумать о балансировании классов меньшинства и большинства, чтобы в противном случае избежать вводящих в заблуждение ловушек. В нашем распоряжении есть несколько вариантов: недостаточная выборка класса большинства, передискретизация класса меньшинства или их комбинация. Проблема с недостаточной выборкой заключается в том, что многие данные отбрасываются, что обычно нежелательно. Поэтому вместо этого я использовал два метода передискретизации, которые создают новые, синтетические точки данных из существующих данных выборки меньшинств: S синтетическое M несоответствие O версампинг TE chnique (SMOTE) и ADA ptive SYN thetic (ADASYN).

Для каждой выборки меньшинства и SMOTE, и ADASYN находят соседние выборки из этого класса, вычисляют их разницу и масштабируют со случайным коэффициентом от 0 до 1. Основное различие между двумя алгоритмами заключается в том, что SMOTE передискретизирует каждую точку одинаково, тогда как ADASYN помещает больший акцент на точках, которые труднее различить. Поскольку эти алгоритмы используют пространственное расстояние, нам необходимо стандартизировать функции с помощью StandardScaler, который вычитает среднее значение, а затем делит его на стандартное отклонение.

from imblearn.over_sampling import ADASYN, SMOTE
from imblearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import recall_score
from sklearn.linear_model import LogisticRegression
hour = TimeToHour()
scaler = StandardScaler()
smote = SMOTE()
adasyn = ADASYN()
logreg = LogisticRegression()
no_sampling_pipe = Pipeline([
    ('hour', hour),
    ('scaler', scaler),
    ('logreg', logreg)
])
smote_pipe = Pipeline([
    ('hour', hour),
    ('scaler', scaler), 
    ('smote', smote), 
    ('logreg', logreg)
])
adasyn_pipe = Pipeline([
    ('hour', hour),
    ('scaler', scaler), 
    ('adasyn', adasyn), 
    ('logreg', logreg)
])
X_train = credit_train.drop(columns=['Class'])
y_train = credit_train['Class']

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

Чтобы оценить, насколько хорошо работают модели, я решил использовать отзыв в качестве предпочтительной метрики. Отзыв определяется как TP / (TP + FN), который по сути измеряет эффективность модели, фиксирующей как можно больше мошеннических действий. При перекрестной проверке средние оценки отзыва от запуска этих конвейеров следующие:

Все данные о поездах

  • Без повторной выборки: 59,34%
  • SMOTE: 90,29%
  • АДАСИН: 92,33%

Обучение данных с удаленными выбросами, не связанными с мошенничеством

  • Без передискретизации: 66,24%
  • SMOTE: 90,29%
  • АДАСИН: 90,80%

Как и следовало ожидать, конвейер, в котором дисбаланс классов не был устранен, работал значительно хуже, и он был единственным, в котором удалялись выбросы, и улучшилось. Напротив, результаты передискретизации предполагают, что конвейер, который использует ADASYN для всех данных поезда, должен быть тем, который следует использовать для нашей окончательной модели. Однако сравнение матриц заблуждения SMOTE и ADASYN - это больше, чем кажется на первый взгляд. Чтобы проиллюстрировать, я выбрал одну из результирующих матриц путаницы, которые возникли во время свертки перекрестной проверки для каждой (другие свертки дали аналогичные результаты).

Из диаграмм видно, что ADASYN выявляет больше случаев мошенничества (в данном случае еще 2), но за это приходится платить - он вызывает почти в 4 раза больше ложных срабатываний, чем SMOTE. Следовательно, лучшая модель действительно зависит от затрат на аудиторскую проверку счетов, отмеченных как мошенничество, по сравнению со средней стоимостью оставления мошенничества невыявленным. Следовательно, я буду использовать обе модели с моим набором тестов удержания и вычислить, когда одна модель будет предпочтительнее другой.

В качестве дополнительной меры для повышения производительности я использовал GridSearchCV как в конвейерах SMOTE, так и в ADASYN, которые используют все данные обучения и сохраняют их для дальнейшего использования. В словаре параметров слово (а) перед двойным подчеркиванием соответствует шагу конвейера, а слово (а) после него - гиперпараметр для точной настройки.

params = dict(
    smote__k_neighbors=range(4, 6),
    logreg__C=np.power(10.0, range(-2, 3)),
)
smote_gridcv = GridSearchCV(smote_pipe, params, scoring='recall')
smote_gridcv.fit(X_train, y_train)
smote_best = smote_gridcv.best_estimator_
# ...
adasyn_best = adasyn_gridcv.best_estimator_

Результаты, достижения

Пора использовать данные, которые мы отложили вначале, и посмотреть, насколько хорошо модели подходят для новых данных.

from sklearn.metrics import confusion_matrix
X_test = credit_test.drop(columns=['Class'])
y_test = credit_test['Class']
smote_pred = smote_best.predict(X_test)
smote_conf_mtx = confusion_matrix(y_test, smote_pred)
# ...
adasyn_conf_mtx = confusion_matrix(y_test, adasyn_pred)

Отзыв SMOTE: 94,06%
Проверок: 1520
Не поймано: 6

Отзыв ADASYN: 97,03%
Проверок: 5555
Не поймано: 3

Наконец, давайте посчитаем, какую модель использовать.

  • Пусть F будет стоимостью оставления мошеннической учетной записи неперехваченной.
  • Пусть A будет стоимостью проверки учетной записи на предмет мошенничества.
  • Пусть f будет количеством неперехваченных мошеннических аккаунтов (TN) для модели.
  • Пусть a будет количеством помеченных учетных записей (TP + FP) для модели.

Из приведенного выше уравнения мы делаем вывод, что если средняя стоимость оставления мошеннической учетной записи неперехваченной в 1345 раз превышает стоимость аудита, следует использовать модель ADASYN. В противном случае SMOTE было бы лучше с финансовой точки зрения.

Заключение

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