Время прочтения: 6 мин.

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

Поведение разных поставщиков в некоторых случаях различается кардинально. Так, транзакции по таким поставщикам программного обеспечения как Microsoft могут исчисляться в сотнях миллионов рублей, в то время как местный поставщик недорогой компьютерной периферии может осуществлять небольшие поставки на незначительные суммы. Поэтому, первым шагом анализа является группировка поставщиков со схожим поведением в кластеры. Каждый кластер объединяет поставщиков с близкими показателями по суммам поставок, частоте оплат, а также длительности отношений с банком. Далее, я рассчитываю центроид каждого кластера и расстояние между центром кластера и каждым конкретным значением признака или переменной в кластере. Чем ближе значение признака находится к центру кластера, тем более типичным это значение является для данного кластера и наоборот, значения, максимально удаленные от центра, являются аномальными и могут расцениваться как подозрительные. Затем, я устанавливаю пороговые значения рассчитанных расстояний (97 процентилей), в пределах которых значения переменных являются типичными или нормальными для поставщиков конкретного кластера. В заключении, я помечаю значения переменных, которые превысили установленные пороговые значения как аномальные (схема 1).

Схема 1

Кластеризация, выявление аномалий и разметка данных с использованием алгоритма K-means

Датасет содержит код поставщика и четыре признака, которые описывают каждого из них.

  • «Дней_с_последней_оплаты» — метрика, показывающая, когда последний раз была сделка с поставщиком;
  • «Частота» — метрика, показывающая количество сделок за рассматриваемый период;
  • «Сумма» — метрика, показывающая сумму всех сделок с поставщиком;
  • «Длительность_отношений_в_днях» — метрика, показывающая длительность отношений с поставщиком.

Первым шагом необходимо подготовить данные для модели. Для этого необходимо улучшить распределение данных (boxcox метод) и нормализовать их чтобы mean и std были одинаковыми для всех признаков (StandardScaler).

# преобразую переменные используя boxcox метод
def boxcox_df(x):
    x_boxcox, _ = stats.boxcox(x)
    return x_boxcox
rfm_data_boxcox = rfm_data.apply(boxcox_df, axis=0)
# нормализую данные используя StandardScaler()
scaler = StandardScaler()
scaler.fit(rfm_data_boxcox)
rfm_norm = scaler.transform(rfm_data_boxcox)
rfm_norm = pd.DataFrame(rfm_norm, index = rfm_data_boxcox.index, columns = rfm_data_boxcox.columns)

Рассчитываю оптимальное количество кластеров используя Elbow criterion метод. Точка, в которой падение графика становится незначительным, показывает оптимальное количество кластеров.

# Рассчитываю оптимальное количество кластеров используя Elbow criterion метод
sse = {}
for k in range(1, 11):
    kmeans=KMeans(n_clusters = k, random_state=42)
    kmeans.fit(rfm_norm)
    sse[k] = kmeans.inertia_
sns.pointplot(x = list(sse.keys()), y=list(sse.values()))
plt.show()

Оптимальным количеством кластеров является 4. Далее, я строю модель с четырьмя кластерами и визуализирую кластеры используя heatmap и snake plot. Snake plot показывает различия между кластерами по признакам. Heatmap показывает средние значения признаков в каждом кластере, а также размер кластеров.

# строю модель с 4 кластерами
kmeans = KMeans(n_clusters=4, random_state=1)
kmeans.fit(rfm_norm)
# вывожу лейблы кластеров 
cluster_labels_4 = kmeans.labels_
# добавляю колонку 'Кластеры' в датасет с оригинальными данными
rfm_data_k4 = rfm_data.assign(Кластер = cluster_labels_4)
# рассчитываю средние значения переменных и размер каждого кластера
rfm_data_k4_grouped = rfm_data_k4.groupby('Кластер').agg(
    {'Дней_с_последней_оплаты':'mean', 'Частота':'mean', 
     'Сумма_общая_тыс_руб':'mean', 'Длительность_отношений_в_днях':['mean', 'count']})
# добавляю колонку 'Кластер'
rfm_norm_clusters = rfm_norm.assign(Кластер = cluster_labels_4)

Snake plot (справа) показывает, что наши четыре кластера различаются по всем четырем признакам и я могу интуитивно определить их экономический смысл и озаглавить. Так, кластер=0 показывает поставщиков, с которыми давно не было никаких отношений, а частота оплат и суммы контрактов по ним были не значительные. И наоборот, кластер=1 группирует действующих поставщиков с большими суммами и частотой оплат. Heatmap (слева) показывает средние значения признаков в каждом кластере в реальных единицах (рублях, днях, количествах поставок). Последний столбец демонстрирует количество поставщиков в каждом кластере. Оба графика говорят о том, что поставщики корректно разбиты на группы, которые значительно отличаются между собой.

Далее, я определяю аномальные значения переменных. Пороговое значение аномальных данных —  97 процентилей. Размечаю данные: 1-fraud, 0-not fraud


# сохраняю центроид кластера
x_cluster_centers = kmeans.cluster_centers_
# рассчитываю расстояния от центра кластера до каждого значения переменных
dist = kmeans.transform(rfm_norm)
# помечаю аномальные значения как 1 и нормальные значения как 0 на основе рассчитанных расстояний
km_y_pred = np.array(dist)
km_y_pred[dist>=np.percentile(dist, 97)] = 1
km_y_pred[dist<np.percentile(dist, 97)] = 0
# создаю DF 
fraud_data = pd.DataFrame(km_y_pred, index = rfm_norm.index, columns = rfm_norm.columns)
fraud_data = fraud_data.assign(Fraud_score = fraud_data.sum(axis=1))
# изолирую нормализованные данные с аномальными значениями
fraud_data_fraud = fraud_data[(fraud_data['Дней_с_последней_оплаты']==1)
                              |(fraud_data['Частота']==1)
                              |(fraud_data['Сумма_общая_тыс_руб']==1)
                              |(fraud_data['Длительность_отношений_в_днях']==1)]
rfm_norm_fraud = rfm_norm_clusters[rfm_norm_clusters.index.isin(fraud_data_fraud.index)]
rfm_norm_not_fraud = rfm_norm_clusters[~rfm_norm_clusters.index.isin(fraud_data_fraud.index)]

Рассчитываю количество поставщиков с аномальным поведением

# рассчитываю количество поставщиков, по которым 2 и более аномальных значения данных
fraudulent = fraud_data_fraud[(fraud_data_fraud['Fraud_score']>=2)
                              &(fraud_data_fraud['Сумма_общая_тыс_руб']==1)]
print('Количество поставщиков с аномальным поведением (1 и более аномалий): ', len(fraud_data_fraud))
print('Количество поставщиков с аномальным поведением (2 и более аномалии): ', fraudulent.shape[0])
Количество поставщиков с аномальным поведением (1 и более аномалий):  263
Количество поставщиков с аномальным поведением (2 и более аномалии):  22

Заключение

Была рассмотрена методика выявления аномалий и разметки данных при анализе поставщиков программного обеспечения и компьютерной техники. Анализ выявил 263 компании с не типичным поведением. Данная методика не позволяет с уверенностью сказать, что все сделки по этим компаниям являются мошенническими. Я лишь могу утверждать, что эти компании имели аномальное, не типичное поведение по отношению к компаниям со схожими характеристиками. Поэтому, хорошей практикой является обсуждение и корректировка результатов исследования со специалистом по fraud анализу который обладает более глубокой экспертизой в данной области.

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

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