Выполнение обнаружения выбросов в реальном времени для выявления мошеннических покупок по кредитным картам с использованием DBSCAN и Deephaven

Дж.Дж. Броснан

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

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

Код в этом блоге использует SciKit-Learn, который не поставляется с базовыми изображениями Deephaven. Чтобы запустить этот код, убедитесь, что у вас установлен модуль. Вот как вы можете Установить пакеты Python и Использовать пакеты Python в запросах. У нас также есть руководство Как использовать SciKit-Learn в Дипхейвене.

Набор данных

Мы используем набор данных о мошенничестве с кредитными картами, который находится в открытом доступе на Kaggle и в репозитории примеров Deepphaven.

Набор данных состоит из 284 807 покупок по кредитным картам в течение 48 часов европейскими держателями карт. Только 492 из этих покупок являются мошенническими, что делает этот набор данных сильно несбалансированным по действительным покупкам. Из-за конфиденциального характера данных показатели покупки, состоящие из 28 переменных (с именами от V1 до V28), были преобразованы и анонимизированы, чтобы в них не содержалась идентифицируемая информация.

Примем следующие основные положения:

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

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

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

Сначала нам нужно импортировать данные в память.

# Required imports
from deephaven.TableTools import readCsv
from deephaven import Plot
# Read the CSV file into a Deephaven table
creditcard = readCsv("/data/examples/CreditCardFraud/csv/creditcard.csv")

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

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

# Required imports
from deephaven import Plot
def plot_valid_vs_fraud(col_name):
    # Set the creditcard table as global to make sure we can access it for the plot
    global creditcard
# Make sure the input corresponds to a column
    allowed_col_names = [item for item in range(1, 29)] + ["V" + str(item) for item in range(1, 29)]
    if col_name not in allowed_col_names:
        raise ValueError("The column name you specified is not valid.")
    if isinstance(col_name, int):
        col_name = "V" + str(col_name)
# Some convenience variables for plotting
    num_valid_bins = 50
    num_fraud_bins = 50
    valid_label = col_name + "_Valid"
    fraud_label = col_name + "_Fraud"
    valid_string = "Class = 0"
    fraud_string = "Class = 1"
# Create a fancy histogram plot
    valid_vs_fraud = \
        Plot\
        .histPlot(valid_label, creditcard.where(valid_string), col_name, num_valid_bins)\
        .twinX()\
        .histPlot(fraud_label, creditcard.where(fraud_string), col_name, num_valid_bins)\
        .show()
    return valid_vs_fraud

Есть 28 столбцов данных для изучения, поэтому мы не будем показывать их все здесь. Гистограммы столбцов V4, V12 и V14 показывают значительные различия в том, как действительные и мошеннические покупки распределяются в этих областях. Таким образом, мы будем использовать эти три столбца для нашего анализа.

valid_vs_fraud_V4 = plot_valid_vs_fraud("V4")
valid_vs_fraud_V12 = plot_valid_vs_fraud("V12")
valid_vs_fraud_V14 = plot_valid_vs_fraud("V14")

В дальнейшем наши запросы будут анализировать только следующие столбцы:

  • Time
  • V4
  • V12
  • V14
  • Class

Решение​

Алгоритм кластеризации

Итак, мы хотим классифицировать пространственные выбросы. Как это можно сделать с имеющимися данными?

Мы собираемся использовать DBSCAN — спектральную кластеризацию приложений с шумом на основе плотности. Зачем использовать DBSCAN?

  • DBSCAN может находить кластеры произвольной формы — мы мало знаем о том, как будут выглядеть наши кластеры.
  • DBSCAN может найти произвольное количество кластеров — нам нужен один-единственный кластер.
  • DBSCAN устойчив к выбросам — это именно то, что нам нужно.
  • Реализация SciKit-Learn проста в использовании в Deephaven.

Реализация SciKit-Learn, sklearn.cluster.DBSCAN, имеет два обязательных входа:

  • Расстояние по соседству.
  • Количество соседей, которые считаются частью кластера.

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

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

Наш обучающий набор будет состоять из покупок, которые происходят между 12 и 16 часами. Затем наш тестовый набор будет состоять из четырехчасового окна, которое происходит через 24 часа — покупки, которые происходят между 36 и 40 часами. Мы разделяем наши обучающие и тестовые наборы. на 24 часа, потому что мы ожидаем, что покупки будут иметь сходство в течение дня.

Выбор расстояния окрестности

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

Расстояние соседства DBSCAN обычно выбирается на основе расстояния каждой точки до ближайшего соседа. Если эти расстояния ближайших соседей расположены в порядке возрастания, создается кривая. Наиболее распространенный выбор расстояния соседства — колено или локоть кривой». Давайте создадим эту кривую, построим ее и выберем наше соседнее расстояние.

from deephaven.TableTools import readCsv
from deephaven import Plot
from deephaven import tableToDataFrame
from deephaven import dataFrameToTable
from sklearn.neighbors import KDTree as kdtree
import pandas as pd
import numpy as np
# Read external data, remove unwanted parts, and split into train/test
creditcard = readCsv("/data/examples/CreditCardFraud/csv/creditcard.csv")
creditcard = creditcard.select("Time", "V4", "V12", "V14", "Amount", "Class")
train_data = creditcard.where("Time >= 43200 && Time < 57600")
test_data = tableToDataFrame(creditcard.where("Time >= 129600 && Time < 144000"))
# Turn the training data into a Pandas DataFrame
data = tableToDataFrame(train_data.select("V4", "V12", "V14")).values
# Get nearest neighbor distances using a K-d tree
tree = kdtree(data)
dists, inds = tree.query(data, k = 2)
# Sort the nearest neighbor distances in ascending order
neighbor_dists = np.sort(dists[:, 1])
x = np.array(range(len(neighbor_dists)))
# Turn our x and y (sorted neighbor distances) into a Deephaven table
nn_dists = pd.DataFrame({"X": x, "Y": neighbor_dists})
nn_dists = dataFrameToTable(nn_dists)
# Plot the last few hundred points so we can see the "elbow"
neighbor_dists = Plot.plot("Nearest neighbor distance", nn_dists.where("X > 30000"), "X", "Y").show()

В нашем окне обучения 31 181 покупка. Изгиб кривой находится прямо возле конца нашего графика, поэтому мы вырезаем первые 30 000 точек из нашего графика. Изгиб этой кривой возникает там, где расстояние соседства почти точно равно единице. Таким образом, мы будем использовать 1 в качестве нашего первого входа в DBSCAN.

Количество соседей

Второй входной параметр, который мы должны выбрать, — это количество соседей. Этот ввод соответствует количеству других точек, которые находятся в пределах расстояния окрестности данной точки, чтобы она считалась частью кластера. Таким образом, DBSCAN проверяет каждую точку в наборе, чтобы увидеть, сколько других точек находится в ее окрестности. Если точка имеет большее или равное указанному количеству соседей, она является частью кластера. Если меньше, точка считается шумом. Существует некоторая специальная обработка, позволяющая отличить кластеры друг от друга, но это не относится к этой проблеме. Нам нужен единый кластер действительных покупок. Любая точка, не принадлежащая этому единственному кластеру, считается мошеннической покупкой.

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

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

Подгонка модели и использование подогнанной модели

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

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

Код

Хорошо, много разговоров, но пока мало кода. Давайте перейдем к фактическому решению, как оно работает в Дипхейвене.

Подгонка модели

Сначала нам нужно подогнать модель под наши данные. Наши функции обучения состоят из столбцов V4, V12 и V14. Наши тренировочные цели указаны в столбце Class. Для тренировки нам нужны часы 12, 13, 14 и 15.

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

В приведенном ниже коде происходит довольно много. Разобьем на этапы:

  • Импортируйте все, что нам нужно для статического анализа и анализа в реальном времени.
  • Считайте данные CSV в память и создайте из них обучающие и тестовые таблицы.
  • Добавьте метки времени в таблицу тестирования для последующего использования.
  • Создайте функцию, чтобы соответствовать модели DBSCAN с выбранными нами входными данными для обучающего набора.
  • Создавайте функции для распределения и сбора данных в таблицы Deephaven и из них.
  • Примените DBSCAN к тренировочным данным с помощью функции learn.
  • Проверьте, как работает модель.
# Deephaven imports
replayer = jpy.get_type("io.deephaven.db.v2.replay.Replayer")
from deephaven import DBTimeUtils as dbtu
from deephaven.TableTools import readCsv
from deephaven import dataFrameToTable
from deephaven import tableToDataFrame
from deephaven import learn
# Python imports
from sklearn.neighbors import KDTree as kdtree
from sklearn.cluster import DBSCAN as dbscan
import pandas as pd
import numpy as np
import scipy as sp
# Read external data, remove unwanted parts, and split into train/test
creditcard = readCsv("/data/examples/CreditCardFraud/csv/creditcard.csv")
creditcard = creditcard.select("Time", "V4", "V12", "V14", "Amount", "Class")
train_data = creditcard.where("Time >= 43200 && Time < 57600")
test_data = creditcard.where("Time >= 129600 && Time < 144000")
# This base time will be used to generate time stamps
base_time = dbtu.convertDateTime("2021-11-16T00:00:00 NY")
# This function will create a timestamp column from the time offset column
def timestamp_from_offset(t):
    global base_time
    db_period = "T{}S".format(t)
    return dbtu.plus(base_time, dbtu.DBPeriod(db_period))
# Add a timestamp column to the test data for later replay
test_data = test_data.update("TimeStamp = (DBDateTime)timestamp_from_offset(Time)")
# This placeholder will be replaced by our trained DBSCAN model
db = 0
# A function to apply DBSCAN with eps = 1 and min_samples = 10
def perform_dbscan(data):
    global db
    db = dbscan(eps = 1, min_samples = 10).fit(data)
    return db.labels_
# Our gather function for DBSCAN
def dbscan_gather(idx, cols):
    gathered = np.empty([idx.getSize(), len(cols)], dtype = float)
    iter = idx.iterator()
    i = 0
while(iter.hasNext()):
        it = iter.next()
        j = 0
for col in cols:
            gathered[i, j] = col.get(it)
            j += 1
i += 1
return np.squeeze(gathered)
# Our scatter function for DBSCAN
def dbscan_scatter(data, idx):
    if data[idx] == -1:
        data[idx] = 1
    return data[idx]
# Perform DBSCAN on our train_data table
clustered = learn.learn(
    table = train_data,
    model_func = perform_dbscan,
    inputs = [learn.Input(["V4", "V12", "V14"], dbscan_gather)],
    outputs = [learn.Output("PredictedClass", dbscan_scatter, "short")],
    batch_size = train_data.size()
)
# Split DBSCAN guesses (correct and incorrect) into separate tables
dbscan_correct_valid = clustered.where("Class == 0 && PredictedClass == 0")
dbscan_correct_fraud = clustered.where("Class == 1 && PredictedClass == 1")
dbscan_false_positives = clustered.where("Class == 0 && PredictedClass == 1")
dbscan_false_negatives = clustered.where("Class == 1 && PredictedClass == 0")
# Report the accuracy of the model
print("DBSCAN guesses valid - correct! " + str(dbscan_correct_valid.size()))
print("DBSCAN guesses fraud - correct! " + str(dbscan_correct_fraud.size()))
print("DBSCAN guesses valid - wrong! " + str(dbscan_false_positives.size()))
print("DBSCAN guesses fraud - wrong! " + str(dbscan_false_negatives.size()))

Сладкий! Разберем производительность модели:

  • 31063 из 31075 действительных покупок идентифицированы правильно (>99%).
  • 33 из 45 мошеннических покупок идентифицируются правильно (73%).
  • 12 мошеннических покупок ошибочно идентифицированы как действительные — это ложноотрицательные результаты (27%).
  • 71 действительная покупка ошибочно идентифицирована как мошенническая — это ложные срабатывания (‹1%).

Эти результаты довольно хороши. Давайте двигаться вперед!

Обнаружение мошенничества в режиме реального времени

У нас уже есть таблица test_data в памяти. Мы хотим воспроизвести его в реальном времени на основе меток времени, которые мы создали в столбце DateTime. Мы можем сделать это, используя Replayer.

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

  • Подогнанная модель имеет один кластер.
  • Точка является частью этого единственного кластера, если она имеет 10 соседей в радиусе 1.
  • Точка не является частью кластера, если любой из ее ближайших 10 соседей находится дальше, чем на расстоянии 1 от нее.

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

Код ниже можно разбить на шаги:

  • Воспроизведите таблицу test_data в режиме реального времени.
  • Создайте функцию для прогнозирования действительности входящих покупок, используя обучающие данные и нашу модель.
  • Создайте функцию для распределения и сбора данных в таблицу Deephaven и из нее.
  • Используйте learn, чтобы применить нашу модель к таблице обновления в реальном времени.
  • Создавайте производные таблицы, которые показывают, как наши модели работают в режиме реального времени.
start_time = dbtu.convertDateTime("2021-11-17T12:00:00 NY")
end_time = dbtu.convertDateTime("2021-11-17T16:01:00 NY")
# Replay the test_data table
test_data_replayer = replayer(start_time, end_time)
creditcard_live = test_data_replayer.replay(test_data, "TimeStamp")
test_data_replayer.start()
creditcard_live = creditcard_live.view("Time", "V4", "V12", "V14", "Amount", "Class")
# A function to place new observations into our existing clusters
def dbscan_predict(X_new):
    n_rows = X_new.shape[0]
    data_with_new = np.vstack([dbscanned, X_new])
    tree = kdtree(data_with_new)
    dists, points = tree.query(data_with_new, 10)
    dists = dists[-n_rows:]
    detected_fraud = [0] * n_rows
    for idx in range(len(dists)):
        if any(dists[idx] > 1):
            detected_fraud[idx] = 1
    return np.array(detected_fraud)
# A function to gather data from a Deephaven table into a NumPy array
def gather(idx, cols):
    rst = np.empty([idx.getSize(), len(cols)], dtype = np.single)
    iter = idx.iterator()
    i = 0
    while (iter.hasNext()):
        it = iter.next()
        j = 0
        for col in cols:
            rst[i, j] = col.get(it)
            j += 1
        i += 1
return np.squeeze(rst)
# A function to scatter data back into an output table
def scatter(data, idx):
    return data[idx]
predicted_fraud_live = learn.learn(
    table = creditcard_live,
    model_func = dbscan_predict,
    inputs = [learn.Input(["V4", "V12", "V14"], gather)],
    outputs = [learn.Output("PredictedClass", scatter, "short")],
    batch_size = 1000
)
dbscan_correct_valid = predicted_fraud_live.where("PredictedClass == 0 && Class == 0")
dbscan_correct_fraud = predicted_fraud_live.where("PredictedClass == 1 && Class == 1")
dbscan_false_positive = predicted_fraud_live.where("PredictedClass == 1 && Class == 0")
dbscan_false_negative = predicted_fraud_live.where("PredictedClass == 0 && Class == 1")

Заключение

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

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

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