Обнаружение мошенничества с использованием многомерного распределения Гаусса

Мотивация

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

Обнаружение мошенничества с кредитными картами в Python

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

Набор данных состоит из транзакций по кредитным картам, функции которых являются результатом анализа PCA, и поэтому мы не знаем, что они представляют, кроме как «Сумма», «Время» и «Класс». «Сумма» - это цена каждой транзакции, «Время» - это ‹транзакцию мошенничества, если ее значение равно 1 и действительная транзакция, если ее значение равно 0. Сначала мы установим необходимые пакеты, чтобы получить файл «requirements.txt» из моего репозитория GitHub, создать виртуальную среду и установить их:

pip install -r requirements.txt

Сначала создайте файл main.py и импортируйте следующее:

import numpy as np
import pandas as pd
import sklearn
from scipy.stats import norm
from scipy.stats import multivariate_normal
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
import seaborn as sns

Теперь мы прочитаем набор данных и проверим, нет ли пропущенных значений:

df = pd.read_csv('creditcardfraud/creditcard.csv')
# missing values
print("missing values:", df.isnull().values.any())

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

# plot normal and fraud
count_classes = pd.value_counts(df['Class'], sort=True)
count_classes.plot(kind='bar', rot=0)
plt.title("Distributed Transactions")
plt.xticks(range(2), ['Normal', 'Fraud'])
plt.xlabel("Class")
plt.ylabel("Frequency")
plt.show()

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

# heatmap
sns.heatmap(df.corr(), vmin=-1)
plt.show()

Похоже, что у нас нет сильно коррелированных функций, хотя есть немного отрицательная корреляция между «V2» и «Amount» (метка не показана на приведенной выше тепловой карте, но находится рядом с функцией «Class» ) Особенности. Функция «Сумма» также немного коррелирует с другими функциями, что означает, что она может быть частично рассчитана ими, поэтому мы можем попробовать отказаться от нее (по моим тестам, это дало хорошее улучшение окончательной оценки). Также существует небольшая корреляция между «Время» и другими функциями, но мы увидим более важную причину, чтобы отказаться от нее после прочтения этой статьи.

До сих пор мы проводили хороший анализ данных, но не делали самой важной части, а именно изучения того, «откуда пришли наши данные». Таким образом, следующий график будет графиком распределения данных:

fig, axs = plt.subplots(6, 5, squeeze=False)
for i, ax in enumerate(axs.flatten()):
    ax.set_facecolor('xkcd:charcoal')
    ax.set_title(df.columns[i])
    sns.distplot(df.iloc[:, i], ax=ax, fit=norm,
                 color="#DC143C", fit_kws={"color": "#4e8ef5"})
    ax.set_xlabel('')
fig.tight_layout(h_pad=-1.5, w_pad=-1.5)
plt.show()

Я рекомендую вам выполнить приведенный выше фрагмент кода на своем локальном компьютере, чтобы лучше увидеть результаты. Синяя линия показывает фактическое распределение Гаусса, а красная - функцию плотности вероятности наших данных. Мы видим, что почти все функции происходят из нормального (или гауссовского) распределения, за исключением временного. Таким образом, это будет нашей мотивацией для проведения обнаружения мошенничества с многомерным распределением Гаусса. Этот метод работает только для функций из гауссовского распределения, поэтому, если ваши данные не похожи на гауссовские, вы можете преобразовать их с помощью метода, который я описываю в этой статье. Например, вы можете опробовать эту технику на функциях V26, V4, V1 и посмотреть, улучшится ли наша окончательная оценка. Функция Время происходит от бимодального распределения, которое нельзя преобразовать в гауссовское, поэтому мы откажемся от него. Еще одна причина отказаться от функции Время заключается в том, что она, похоже, не содержит экстремальных значений, как функции, показанные на других графиках.

Более того, алгоритм, который мы будем использовать, чувствителен к метрикам расстояния, поэтому, если мы масштабируем функции до фиксированного диапазона, мы получим гораздо лучшие результаты. Добавьте следующий фрагмент кода, который отбросит упомянутые выше функции и масштабирует другие так, чтобы они принимали значения от 0 до 1:

classes = df['Class']
df.drop(['Time', 'Class', 'Amount'], axis=1, inplace=True)
cols = df.columns.difference(['Class'])
MMscaller = MinMaxScaler()
df = MMscaller.fit_transform(df)
df = pd.DataFrame(data=df, columns=cols)
df = pd.concat([df, classes], axis=1)

Обратите внимание, что MinMaxScaler не изменит форму распределения, поэтому выбросы по-прежнему будут в нужном месте.

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

  1. Найти функции, которые могут содержать аномальные примеры (готово)
  2. Вычислите среднее значение каждой функции в обучающем наборе, который обычно содержит 60% от обычных транзакций.
  3. Вычислить ковариационную матрицу на обучающем наборе
  4. Вычислить многомерный нормальный PDF (функция плотности вероятности) на обучающем наборе (приведенном ниже)
  5. Рассчитайте многомерный нормальный PDF-файл для проверочного набора (содержит 50% мошеннических транзакций и обычно 20% обычных единицы)
  6. Рассчитайте многомерный нормальный PDF-файл на тестовом наборе (содержит 50% мошеннических транзакций и обычно 20% нормальных единицы)
  7. Найдите порог на основе набора pdf из проверки, который указывает, что значения PDF (из любого набора), которые меньше , чем порог, являются выбросами
  8. Вычислите выбросы на тестовом наборе, которые представляют собой сумму значений PDF, которые меньше предыдущего порогового значения.

где μ - среднее значение, det - определитель, Σ - ковариационная матрица, а κ - размерность пространства, где x принимает значения.

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

И да, я знаю, что вышеупомянутый PDF-файл болит вам глаза (как и мой), но он уже реализован в пакете scipy ...

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

import pandas as pd
import numpy as np
def train_validation_splits(df):
    # Fraud Transactions
    fraud = df[df['Class'] == 1]
    # Normal Transactions
    normal = df[df['Class'] == 0]
    print('normal:', normal.shape[0])
    print('fraud:', fraud.shape[0])
    normal_test_start = int(normal.shape[0] * .2)
    fraud_test_start = int(fraud.shape[0] * .5)
    normal_train_start = normal_test_start * 2
    val_normal = normal[:normal_test_start]
    val_fraud = fraud[:fraud_test_start]
    validation_set = pd.concat([val_normal, val_fraud], axis=0)
    test_normal = normal[normal_test_start:normal_train_start]
    test_fraud = fraud[fraud_test_start:fraud.shape[0]]
    test_set = pd.concat([test_normal, test_fraud], axis=0)
    Xval = validation_set.iloc[:, :-1]
    Yval = validation_set.iloc[:, -1]
    Xtest = test_set.iloc[:, :-1]
    Ytest = test_set.iloc[:, -1]
    train_set = normal[normal_train_start:normal.shape[0]]
    Xtrain = train_set.iloc[:, :-1]
    return Xtrain.to_numpy(), Xtest.to_numpy(), Xval.to_numpy(), Ytest.to_numpy(), Yval.to_numpy()

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

def estimate_gaussian_params(X):
    """
    Calculates the mean and the covariance for each feature.
    Arguments:
    X: dataset
    """
    mu = np.mean(X, axis=0)
    sigma = np.cov(X.T)
    return mu, sigma

Вернемся к нашему «main.py», импортируем и вызовем вышеуказанные функции вместе с многомерным нормальным PDF-файлом для каждого из наших наборов:

(Xtrain, Xtest, Xval, Ytest, Yval) = train_validation_splits(df)
(mu, sigma) = estimate_gaussian_params(Xtrain)
# calculate gaussian pdf
p = multivariate_normal.pdf(Xtrain, mu, sigma)
pval = multivariate_normal.pdf(Xval, mu, sigma)
ptest = multivariate_normal.pdf(Xtest, mu, sigma)

Теперь пора обратиться к порогу (или «эпсилону»). В общем, рекомендуется инициализировать порог с минимальным значением PDF и увеличивать его с небольшим шагом, пока вы не достигнете максимального PDF, сохраняя при этом каждое пороговое значение в векторе. Что касается нашей проблемы, я обнаружил, что значения из PDF-файла можно эффективно использовать для создания порогового вектора. После создания вектора мы создаем цикл for и перебираем его. На каждой итерации мы сравниваем текущий порог со значениями PDF, которые дают наши прогнозы. Затем мы вычисляем оценку F1 на основе наших прогнозов и основных истин значений, и если полученная нами оценка F1 больше, чем предыдущая. , мы переопределяем переменную «лучший порог». В конце цикла for у нас есть значение эпсилон, которое дало лучший результат F1.

Обратите внимание, что мы не можем использовать точность в качестве показателя! Если мы определим все транзакции как «нормальные», мы получим 99% точность и бесполезный алгоритм.

Чтобы реализовать вышеизложенное, добавьте следующие функции в «functions.py»:

def metrics(y, predictions):
    fp = np.sum(np.all([predictions == 1, y == 0], axis=0))
    tp = np.sum(np.all([predictions == 1, y == 1], axis=0))
    fn = np.sum(np.all([predictions == 0, y == 1], axis=0))
    precision = (tp / (tp + fp)) if (tp + fp) > 0 else 0
    recall = (tp / (tp + fn)) if (tp + fn) > 0 else 0
    F1 = (2 * precision * recall) / (precision +
                                     recall) if (precision + recall) > 0 else 0
    return precision, recall, F1
def selectThreshold(yval, pval):
    e_values = pval
    bestF1 = 0
    bestEpsilon = 0
    for epsilon in e_values:
        predictions = pval < epsilon
        (precision, recall, F1) = metrics(yval, predictions)
        if F1 > bestF1:
            bestF1 = F1
            bestEpsilon = epsilon
    return bestEpsilon, bestF1

Наконец, импортируйте функции в наш файл «main.py» и вызовите их, чтобы вернуть наш порог и оценку F1 на проверочном наборе, а также оценить нашу модель на нашем тестовом наборе:

(epsilon, F1) = selectThreshold(Yval, pval)
print("Best epsilon found:", epsilon)
print("Best F1 on cross validation set:", F1)
(test_precision, test_recall, test_F1) = metrics(Ytest, ptest < epsilon)
print("Outliers found:", np.sum(ptest < epsilon))
print("Test set Precision:", test_precision)
print("Test set Recall:", test_recall)
print("Test set F1 score:", test_F1)

Результаты следующие:

Best epsilon found: 5e-324 
Best F1 on cross validation set: 0.7852998065764023
 
Outliers found: 210 
Test set Precision: 0.9095238095238095 
Test set Recall: 0.7764227642276422 
Test set F1 score: 0.837719298245614

которые довольно хороши!

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

Первоначально опубликовано на странице Обнаружение мошенничества в Python (devnal.com) .