Идентификация мошенничества с использованием Enron dataset. Часть 1-ая, подготовка данных и отбор признков

Корпорация Enron — это одна из наиболее известных фигур в американском бизнесе 2000-ых годов. Этому способствовала не их сфера деятельности (электроэнергия и контракты на ее поставку), а резонанс в связи с мошенничеством в ней. В течении 15 лет доходы корпорации стремительно росли, а работа в ней сулила неплохую заработную плату. Но закончилось всё так же быстротечно: в период 2000-2001гг. цена акций упала с 90$/шт практически до нуля по причине вскрывшегося мошенничества с декларируемыми доходами. С тех пор слово "Enron" стало нарицательным и выступает в качестве ярлыка для компаний, которые действуют по аналогичной схеме.


В ходе судебного разбирательства, 18 человек (в том числе крупнейшие фигуранты данного дела: Эндрю Фастов, Джефф Скиллинг и Кеннет Лей) были осуждены.


image![image](http://https://habrastorage.org/webt/te/rh/1l/terh1lsenbtg26n8nhjbhv3opfi.jpeg)


Вместе с тем были опубликованы архив электронной переписки между сотрудниками компании, более известный как Enron Email Dataset, и инсайдерская информация о доходах сотрудников данной компании.


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


Описание датасета


Enron dataset (датасет) — это сводный набор открытых данных, что содержит записи о людях, работающих в приснопамятной корпорации с соответствующим названием.
В нем можно выделить 3 части:


  • payments_features — группа, характеризующая финансовые движения;
  • stock_features — группа, отражающая признаки связанные с акциями;
  • email_features — группа, отражающая информацию об email-ах конкретного человека в агрегированном виде.

Конечно же, присутствует и целевая переменная, которая указывает, подозревается ли данный человек в мошенничестве (признак 'poi').

Загрузим наши данные и начнём с работу с ними:


import pickle
with open("final_project/enron_dataset.pkl", "rb") as data_file:
    data_dict = pickle.load(data_file)

После чего превратим набор data_dict в Pandas dataframe для более удобной работы с данными:


import pandas as pd
import warnings
warnings.filterwarnings('ignore')
source_df = pd.DataFrame.from_dict(data_dict, orient = 'index')
source_df.drop('TOTAL',inplace=True)

Сгруппируем признаки в соответствии с ранее указанными типами. Это должно облегчить работу с данными впоследствии:


payments_features = ['salary', 'bonus', 'long_term_incentive',
                     'deferred_income', 'deferral_payments', 'loan_advances',
                     'other', 'expenses', 'director_fees', 'total_payments']
stock_features = ['exercised_stock_options', 'restricted_stock', 'restricted_stock_deferred','total_stock_value']

email_features = ['to_messages', 'from_poi_to_this_person',
                  'from_messages', 'from_this_person_to_poi',
                  'shared_receipt_with_poi']
target_field = 'poi'

Финансовые данные


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


payments = source_df[payments_features]
payments = payments.replace('NaN', 0)

Проверка данных


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


errors = payments[payments[payments_features[:-1]].sum(axis='columns') != payments['total_payments']]
errors.head()

2 некорректные строки
Мы видим, что BELFER ROBERT и BHATNAGAR SANJAY имеют неверные суммы по платежам.


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


import numpy as np
shifted_values = payments.loc['BELFER ROBERT', payments_features[1:]].values

expected_payments = shifted_values.sum()
shifted_values = np.append(shifted_values, expected_payments)
payments.loc['BELFER ROBERT', payments_features] = shifted_values

shifted_values = payments.loc['BHATNAGAR SANJAY', payments_features[:-1]].values
payments.loc['BHATNAGAR SANJAY', payments_features] = np.insert(shifted_values, 0, 0)

Данные по акциям


stocks = source_df[stock_features]
stocks = stocks.replace('NaN', 0)

Выполним проверку корректности и в этом случае:


errors = stocks[stocks[stock_features[:-1]].sum(axis='columns') != stocks['total_stock_value']]
errors.head()

image


Исправим аналогично ошибку в акциях:


shifted_values = stocks.loc['BELFER ROBERT', stock_features[1:]].values

expected_payments = shifted_values.sum()
shifted_values = np.append(shifted_values, expected_payments)
stocks.loc['BELFER ROBERT', stock_features] = shifted_values

shifted_values = stocks.loc['BHATNAGAR SANJAY', stock_features[:-1]].values
stocks.loc['BHATNAGAR SANJAY', stock_features] = np.insert(shifted_values, 0, shifted_values[-1])

Сводные данные по электронной переписке


Если для данных финансов или акций NaN был эквивалентен 0, и это вписывалось в итоговый результат по каждой из этих групп, в случае с email NaN разумнее заменить на некое дефолтное значение. Для этого можно воспользоваться Imputer-ом:


from sklearn.impute import SimpleImputer
imp = SimpleImputer()

Вместе с тем будем считать дефолтное значение для каждой категории (подозреваем ли человек в мошеничестве) отдельно:


target = source_df[target_field]

email_data = source_df[email_features]
email_data = pd.concat([email_data, target], axis=1)
email_data_poi = email_data[email_data[target_field]][email_features]
email_data_nonpoi = email_data[email_data[target_field] == False][email_features]

email_data_poi[email_features] = imp.fit_transform(email_data_poi)
email_data_nonpoi[email_features] = imp.fit_transform(email_data_nonpoi)
email_data = email_data_poi.append(email_data_nonpoi)

Итоговый датасет после коррекции:


df = payments.join(stocks)
df = df.join(email_data)
df = df.astype(float)

Выбросы


На финальном шаге данного этапа удалим все выбросы (outliers), что могут исказить обучение. В то же время всегда стоит вопрос: как много данных мы можем удалить из выборки и при этом не потерять в качестве обучаемой модели? Я придерживался совета одного из лекторов ведущих курс по ML (машинное обучение) на Udacity — ”Удалите 10 штук и проверьте на выбросы еще раз”.


first_quartile = df.quantile(q=0.25)
third_quartile = df.quantile(q=0.75)
IQR = third_quartile - first_quartile
outliers = df[(df > (third_quartile + 1.5 * IQR)) | (df < (first_quartile - 1.5 * IQR))].count(axis=1)
outliers.sort_values(axis=0, ascending=False, inplace=True)
outliers = outliers.head(10)
outliers

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


target_for_outliers = target.loc[outliers.index]
outliers = pd.concat([outliers, target_for_outliers], axis=1)
non_poi_outliers = outliers[np.logical_not(outliers.poi)]
df.drop(non_poi_outliers.index, inplace=True)

Приведение к итоговом виду


Нормализуем наши данные:


from sklearn.preprocessing import scale
df[df.columns] = scale(df)

Приведем целевую переменную target к совместимому виду:


target.drop(non_poi_outliers.index, inplace=True)
target = target.map({True: 1, False: 0})
target.value_counts()

image
В итоге 18 подозреваемых и 121 тех, кто не попал под подозрение.


Отбор признаков


Пожалуй один из наиболее ключевых моментов перед обучением любой модели — это отбор наиболее важных признаков.


Проверка на мультиколлинеарность


import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
sns.set(style="whitegrid")

corr = df.corr() * 100

# Select upper triangle of correlation matrix
mask = np.zeros_like(corr, dtype=np.bool)
mask[np.triu_indices_from(mask)] = True

# Set up the matplotlib figure
f, ax = plt.subplots(figsize=(15, 11))

# Generate a custom diverging colormap
cmap = sns.diverging_palette(220, 10)

# Draw the heatmap with the mask and correct aspect ratio
sns.heatmap(corr, mask=mask, cmap=cmap, center=0,
            linewidths=1, cbar_kws={"shrink": .7}, annot=True, fmt=".2f")

image
Как видно из изображения, у нас присутствует выраженная взаимосвязь между ‘loan_advanced’ и ‘total_payments’, а также между ‘total_stock_value’ и ‘restricted_stock’. Как уже было упомянуто ранее, ‘total_payments’ и ‘total_stock_value’ являются всего лишь результатом сложения всех показателей в конкретной группе. Поэтому их можно удалить:


df.drop(columns=['total_payments', 'total_stock_value'], inplace=True)

Создание новых признаков


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


df['ratio_of_poi_mail'] = df['from_poi_to_this_person']/df['to_messages']
df['ratio_of_mail_to_poi'] = df['from_this_person_to_poi']/df['from_messages']

Отсев лишних признаков


В инструментарии людей, связанных с ML, есть множество прекрасных инструментов для отбора наиболее значимых признаков (SelectKBest, SelectPercentile, VarianceThreshold и др.). В данном случае будет использован RFECV, поскольку он включает в себя кросс-валидацию, что позволяет вычислить наиболее важные признаки и проверить их на всех подмножествах выборки:


from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(df, target, test_size=0.2, random_state=42)

from sklearn.feature_selection import RFECV
from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier(random_state=42)
rfecv = RFECV(estimator=forest, cv=5, scoring='accuracy')
rfecv = rfecv.fit(X_train, y_train)
plt.figure()
plt.xlabel("Number of features selected")
plt.ylabel("Cross validation score of number of selected features")
plt.plot(range(1, len(rfecv.grid_scores_) + 1), rfecv.grid_scores_, '--o')
indices = rfecv.get_support()
columns = X_train.columns[indices]
print('The most important columns are {}'.format(','.join(columns)))

image
Как можно увидеть, RandomForestClassifier посчитал, что только 7 признаков из 18 имеют значение. Использование остальных приводит к снижению точности модели.


The most important columns are bonus, deferred_income, other, exercised_stock_options, shared_receipt_with_poi, ratio_of_poi_mail, ratio_of_mail_to_poi

Эти 7 признаков будут использованы в дальнейшем, дабы упростить модель и уменьшить риск переобучения:


  • bonus
  • deferred_income
  • other
  • exercised_stock_options
  • shared_receipt_with_poi
  • ratio_of_poi_mail
  • ratio_of_mail_to_poi

Изменим структуру обучающей и тестовой выборок для будущего обучения модели:


X_train = X_train[columns]
X_test = X_test[columns]

Это конец первой части, описывающей использование Enron Dataset в качестве примера задачи классификации в ML. За основу взяты материалы из курса Introduction to Machine Learning на Udacity. Также есть python notebook, отражающий всю последовательность действий.


Вторая часть находится здесь

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 4

    0
    Замена пропусков на нули в самом начале работы с массивом без какого-либо обоснования — это причина, например, рецензенту в научном журнале прекратить чтение и вернуть статью автору на доработку. Просто потому что такая операция вносит в массив информацию, ранее в нем отсутствующую и искажает статистические распределения переменных, средние, дисперсии и пр.

    Ну чем вам помешали пропуски при вычислении матрицы корреляций?
      0

      Спасибо за Ваш вопрос. Сперва попробую пояснить почему заполнено 0.
      О природе исходных данных
      Как уже было упомянуто — датасет основан на на PDF отчете (также известном как Enron Statement of Financial Affairs). Компания, которая производила проверку счетов сотрудников/движений по ним — просто не нашла данных по некоторым из них. И поэтому приняла за основу что таких данных и не существует, и посчитала итоговые суммы.
      Почему не заполнить средним/чем то еще? Потому что некоторые столбцы содержат очень мало данных (например данные о займах — loan advance) и заполнять пробелы на их основе — просто нелогично(не не может что у 140 человек были займы в одинаковую сумму). Также есть столбцы которые связаны с определенными людьми(director fees — жалованье для управленцев) — среднее по которым не может быть перенесена на всех людей, т.к среди них есть и рядовые сотрудники. Также есть и другие столбцы заполнение которых подобным образом внесет еще большую сумятицу в исходные данные, а как следствие придется перерассчитывать итоговые суммы, поскольку они также не будут совпадать. Иными словами — используя привычное заполнение средним/медианой — мы только исказим исходные данные, а не приблизимся к настоящим значениям.


      Может возникнуть резонный вопрос — "А почему значения по email — признакам заполнены средним?" Тут дело в том что данные по ним — как раз неполные. Enron email dataset это только часть всех email сотрудников. Но часть email всё равно отсутствует. Поэтому и использовано среднее значение по всем сотрудникам.
      Иными словами, тут используется принцип — "платежи, акции — мы доверяем источнику. поскольку у нас нет причин не доверять и способов доказать это", "email — мы знаем что данные неполные(в текущий момент вроде бы версия 2) — поэтому заполняем средним"


      Матрица корреляций и NaN.
      Да, можно попытаться посчитать корреляцию на основе данных что есть.Но в тех же финансах нет не одной строчки что не содержит пропусков.
      image
      Возможно ли при этом посчитать корреляцию? Что она будет показывать?


      Исходя из этого всего и было заменено на 0, дабы сохранить изначальный строй датасета, а не исказить его.

      0
      Возможно ли при этом посчитать корреляцию?

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


      Но в тех же финансах нет не одной строчки что не содержит пропусков.

      Это нормально, чем больше показателей, тем эта ситуация вероятней.


      P.S. Посмотрел я на pdf по ссылке, вот например параметер "Loan Advances" просто подлежит исключению из рассмотрения — там одни пропуски за мелкими исключениями.


      P.P.S. Глядя на pdf-файл, я не вижу в нем нулей. Положительные числа вижу, некоторые из них в скобках, а вот нолей нет. Т.е., может быть, это просто такое форматирование вывода для улучшения читабельности? Но это надо проверять. Авторов файла спрашивать, рисовать статистические распределения и смотреть, вписываются ли сюда пропуски, как нули или нет.

        0

        Добавлено продолжение

        Only users with full accounts can post comments. Log in, please.