Поиск по сайту Поиск

Истина где‑то рядом — ищем аномалии с Python. Часть 2: практика

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

Начнём с того, что визуально оценим набор данных и посмотрим, сможем ли мы найти аномалии. Файл Jupyter Notebook с нижеизложенным кодом можно найти здесь.

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

— ФИО сотрудников организации (для 100 человек)

— их ежемесячная заработная плата (в долларах США) в диапазоне от 1000 до 2500.

Чтобы сгенерировать похожие на настоящие имена, мы будем использовать Python-библиотеку Faker, а для зарплаты подойдёт привычная numpy. После этого объединим созданные столбцы в Pandas DataFrame. 

Примечание: не пренебрегайте работой с фиктивными наборами данных, это действительно важный экспериментальный навык!

# Импортируем необходимые пакеты
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
 
# Раскомментируйте следующую строчку, если используете Jupyter Notebook
# %matplotlib inline
# Use a predefined style set
plt.style.use('ggplot')
 
# Импортируем Faker
from faker import Faker
 
# Убеждаемся, что результаты воспроизводимые
Faker().seed(4321)
 
names_list = []
 
fake = Faker()
for _ in range(100):
  names_list.append(fake.name())
 
# Убеждаемся, что результаты воспроизводимые
np.random.seed(7)
salaries = []
for _ in range(100):
    salary = np.random.randint(1000,2500)
    salaries.append(salary)
 
# Создаём pandas DataFrame
salary_df = pd.DataFrame(
    {'Person': names_list,
     'Salary (in USD)': salaries
    })
 
# Печатаем часть DataFrame
print(salary_df.head())

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

salary_df.at[16, 'Salary (in USD)'] = 23
salary_df.at[65, 'Salary (in USD)'] = 17
 
# Убеждаемся, что зарплата изменилась
print(salary_df.loc[16])
print(salary_df.loc[65])

Теперь можно начать эксперименты.

Видеть значит верить: находим аномалии глазами

Подсказка: ящики с усами великолепны!

Как упоминалось в предыдущей статье, появление аномалий напрямую зависит от генерации самих данных. Рассмотрим немного базовой статистики (минимальное значение, максимальное, значение 1-го квартиля и т. д.) в виде ящика с усами (диаграммы размаха):

Образец ящика с усами
# Генерируем ящик с усами
salary_df['Salary (in USD)'].plot(kind='box')
plt.show()

Мы получим:

https://paper-attachments.dropbox.com/s_1185AEC62427E23657579AF288686866FF5B3F65A0E36E86D1A293C6B0CCF4B4_1554276006076_download.png

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

Как насчёт гистограмм?

# Генерируем гистограмму
salary_df['Salary (in USD)'].plot(kind='hist')
plt.show()

Результат:

https://paper-attachments.dropbox.com/s_1185AEC62427E23657579AF288686866FF5B3F65A0E36E86D1A293C6B0CCF4B4_1554276086683_download+1.png

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

Какой же способ сразу подтвердит наличие аномалий в наборе данных? Давайте посмотрим на минимальное и максимальное значение столбца «Заработная плата» (Salary):

# Минимальная и максимальная зарплата
print('Minimum salary ' + str(salary_df['Salary (in USD)'].min()))
print('Maximum salary ' + str(salary_df['Salary (in USD)'].max()))

Получим:

Minimum salary 17
Maximum salary 2498

Минимальное значение явно отклоняется от того, что было задано раньше (1000 долларов). Следовательно, это действительно аномалия. 

Примечание: хотя наш набор данных содержит только один признак (зарплату), в настоящих датасетах аномалии могут встречаться в разных признаках. Но даже там такие визуализации помогут вам их обнаружить.

Кластерный подход для обнаружения аномалий

Мы выяснили, что кластеризация и поиск аномалий тесно связаны, хоть и служат разным целям. Но кластеризацию можно использовать для детектирования выбросов. В этом разделе мы рассмотрим аномалии в виде групп похожих объектов. Математически их схожесть измеряется такими функциями, как евклидово расстояние, манхэттенское расстояние и т. д. Евклидово расстояние — самое распространённое, поэтому остановимся на нём подробнее.

Очень короткая заметка о евклидовом расстоянии

Если в двумерном пространстве есть n точек (см. следующий рисунок) и их координаты обозначены (x_i, y_i), то евклидово расстояние между любыми двумя точками x1, y1 и x2, y2 равно:

https://paper-attachments.dropbox.com/s_1185AEC62427E23657579AF288686866FF5B3F65A0E36E86D1A293C6B0CCF4B4_1554546141896_image.png
Уравнение евклидова расстояния
https://paper-attachments.dropbox.com/s_1185AEC62427E23657579AF288686866FF5B3F65A0E36E86D1A293C6B0CCF4B4_1554298964709_Capture+1.PNG
График с точками на двумерной плоскости

Для кластеризации мы будем использовать метод k-средних. Начнём:

# Конвертируем зарплату в массив numpy
salary_raw = salary_df['Salary (in USD)'].values
 
# Для совместимости со SciPy 
salary_raw = salary_raw.reshape(-1, 1)
salary_raw = salary_raw.astype('float64')

Теперь импортируем модуль kmeans из scipy.cluster.vq. SciPy (Scientific Python) — это библиотека для различных научных расчётов.  Применим kmeans к salary_raw:

# Импортируем cluster и kmeans из SciPy
from scipy import cluster
from scipy.cluster.vq import kmeans
    
# Передаём данные и число кластеров в kmeans()
centroids, avg_distance = kmeans(salary_raw, 4)

Во фрагменте выше мы указали в kmeans данные о зарплате и количество кластеров, по которым хотим сгруппировать точки. centroids — это центроиды, сгенерированные kmeans, а avg_distance — усреднённое евклидово расстояние между ними и точками. Давайте извлечём наши кластеры с помощью метода vq(). Его аргументы это:

— точки данных

— центроид, сгенерированный алгоритмом кластеризации.

Метод возвращает группы точек (кластеры) и расстояния между точками и ближайшими кластерами.

# Получаем кластеры и расстояния
groups, cdist = cluster.vq.vq(salary_raw, centroids)

# Рисуем график
plt.scatter(salary_raw, np.arange(0,100), c=groups)
plt.xlabel('Salaries in (USD)')
plt.ylabel('Indices')
plt.show()
https://paper-attachments.dropbox.com/s_1185AEC62427E23657579AF288686866FF5B3F65A0E36E86D1A293C6B0CCF4B4_1554276804886_download+2.png

Теперь вы точно видите аномалии. Итак, несколько моментов, которые необходимо учитывать перед обучением модели:

  1. Тщательно изучите данные — взгляните на каждый признак в наборе, соберите статистику.
  2. Постройте несколько полезных графиков (как показано выше), так вам будет легче заметить отклонения.
  3. Посмотрите, как признаки связаны друг с другом. Это поможет выбрать наиболее значимые из них и отказаться от тех, что не влияют на целевую переменную (не коррелируют с ней). 

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

А почему бы и нет?

Обнаружение аномалий как проблема классификации

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

# Устанавливаем метки для всех объектов 
salary_df['class'] = 0
 
# Вручную исправляем метки для аномалий
salary_df.at[16, 'class'] = 1
salary_df.at[65, 'class'] = 1
 
# Проверяем 
print(salary_df.loc[16])

Снова взглянем на датасет:

https://blog.floydhub.com/content/images/2019/04/image-1.png

Теперь мы решаем задачу бинарной классификации. Будем искать выбросы, основываясь на подходе близости (proximity-based anomaly detection). Основная идея в том, что близость аномальной точки к её соседним точкам сильно отличается от близости других точек к их соседям. Если вам ничего не понятно, не пугайтесь — на наглядном примере всё станет ясно.

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

# Импортируем модуль KNN из PyOD
from pyod.models.knn import KNN

Столбец “Person” для модели совершенно бесполезен, поскольку служит лишь идентификатором. Подготовим обучающую выборку:

# Разделяем значения зарплаты и метки классов 
X = salary_df['Salary (in USD)'].values.reshape(-1,1)
y = salary_df['class'].values
 
# Обучаем kNN-детектор
clf = KNN(contamination=0.02, n_neighbors=5)
clf.fit(X)

Аргументы, переданные в KNN():

contamination: количество аномалий в данных (в процентах), в нашем случае 2/100

n_neighbors: число соседей, упитывающихся при измерении близости 

Теперь получим прогнозируемые метки и оценку аномалий. Чем выше оценка, тем хуже данные. Для этого применим удобные функции PyOD:

# Получаем метки данных обучения
y_train_pred = clf.labels_ 
    
# Оценка аномалий
y_train_scores = clf.decision_scores_

Попробуем оценить KNN() относительно обучающей выборки с помощью функции evaluate_print():

# Импортируем утилиту для оценки модели
from pyod.utils import evaluate_print
 
# Оцениваем на обучающей выборке
evaluate_print('KNN', y, y_train_scores)

Получим:

KNN ROC:1.0, precision @ rank n:1.0

Видим, что KNN() достаточно хорошо работает на обучающих данных. Он выдаёт три метрики и их оценки:

ROC

— точность

—  доверительную оценку.

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

У нас нет тестовой выборки, но мы можем сгенерировать примерное значение зарплаты:

# Зарплата в $37 (аномалия, верно?)
X_test = np.array([[37.]])

Давайте проверим, может ли модель пометить это значение как выброс:

# Проверим, что спрогнозирует модель с этим значением
clf.predict(X_test)

Вывод должен быть: array([1])

Видим, что модель не ошиблась. Проверим, как она работает с нормальными данными:

# Зарплата в $1256
X_test_abnormal = np.array([[1256.]])
 
# Прогноз
clf.predict(X_test_abnormal)

Вывод: array([0])

Модель отметила значение как обычную точку данных. 

На этом мы завершаем исследование аномалий и переходим к заключению.

Проблемы и дальнейшие исследования

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

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

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

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

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

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

Другие сложные методы: здесь мы рассматривали только поиск точечных аномалий. Но есть ряд алгоритмов для обнаружения контекстуальных и коллективных аномалий. Более подробную информацию о них можно найти в книге “Data Mining. - Concepts and Techniques (3rd Edition)”.

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

С оригинальной статьёй можно ознакомиться в блоге floydhub.com.

DoS vs DDoS-атака: отличия и профилактика

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

Сыграем в города? .МОСКВА, .NYC, .PARIS и другие «городские» домены для локального бизнеса

Первое знакомство клиента с компанией часто происходит через интернет. Чем ярче проект — тем больше шансы выделиться среди конкурентов и...
Read More

Как напомнить клиентам о себе через экран смартфона

Любому бизнесу важно не только искать новых клиентов, но и поддерживать связь со старыми — с этой задачей отлично справляется...
Read More

Как создать свой сайт с нуля

Чтобы создать сайт, вам понадобится несколько обязательных элементов: домен, хостинг, SSL-сертификат и программа для создания внешнего вида сайта. Каждый из этих элементов можно...
Read More

Сарафанный маркетинг: как заставить всех о вас говорить

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

Там, где живет интернет: 9 ЦОД мира, которые вас удивят

Центр обработки данных (он же дата-центр) — крупномасштабное здание, внутри которого функционируют серверные и сетевое оборудование. Именно в таких зданиях...
Read More

Настройка сети: что такое VPN

Если вы хотите разобраться, что такое VPN — читайте нашу статью. В ней мы простыми словами рассказали, что значит Virtual Private Network, зачем подключаться...
Read More

Проще и быстрее: что такое безбумажные операции с доменами и как их подключить

Передача домена другому администратору или смена регистратора в .RU или .РФ  — процедура, которая подчинена определённым правилам и стандартам и...
Read More

Как настроить Ubuntu Wine

Несмотря на то, что для ОС Linux регулярно выпускаются новые программы, всегда найдётся какое-нибудь приложение, которого не хватает для работы...
Read More

Что такое ИТ-инфраструктура

В общем смысле ИТ-инфраструктура — это комплекс взаимосвязанных компонентов, с помощью которого можно организовать информационную среду и управлять ей. (далее…)
Read More