5 min read

Разведочный анализ данных (EDA), ч. 1

Разведочный анализ данных (EDA), ч. 1

Разведочный анализ данных (Exploratory Data Analysis) – предварительное исследование Датасета (Dataset) с целью определения его основных характеристик, взаимосвязей между признаками, а также сужения набора методов, используемых для создания Модели (Model) Машинного обучения (Machine Learning).

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

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

Довольно увесистый датасет для восприятия, особенно если учесть, что записей в нем – более 40 тысяч. Однако приступим! Для начала импортируем датасет и посмотрим на "шапку". Параметр 'sep' используется, чтобы указать на нестандартный разделитель, в данном случае – точку с запятой, которая используется в Apple Numbers.

df = pd.read_csv('https://www.dropbox.com/s/62xm9ymoaunnfg6/bank-full.csv?dl=1', sep=';')
df.head()

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

Удаление дубликатов

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

>>> df.shape
(41188, 21)

Удалим дублирующие записи с помощью Pandas.drop_duplicates() и обновим данные о размере данных:

>>> df = df.drop_duplicates()
>>> df.shape
(41176, 21)

Pandas нашел и удалил 12 дубликатов. Хоть число и небольшое, все же качество данные мы повысили.

Обработка пропусков

Стоит помнить, что в случае, если пропусков у признака слишком много (более 70%), такой признак удаляют. Проверим, насколько полны наши признаки: метод isnull() пройдется по каждой ячейке каждого столбца и определит, кто пуст, а кто нет, составив датафрейм такого же размера, состоящий из True / False. Метод mean() суммирует все значения True, определит концентрацию пропусков в каждом столбце. На 100 мы умножаем, чтобы получить значение в процентах:

df.isnull().mean() * 100

Среди всех признаков слишком много пропусков оказалось в переменной 'День':

Возраст 0.000000
Работа 0.801438
Семейный статус 0.194288
Образование 4.201477
Кредитный дефолт 0.000000
Ипотека 0.000000
Займ 0.000000
Контакт 0.000000
Месяц 0.000000
День недели 0.000000
Длительность 0.000000
Кампания 0.000000
День 96.320672
Предыдущий контакт 0.000000
Доходность 0.000000
Колебание уровня безработицы 0.000000
Индекс потребительских цен 0.000000
Индекс потребительской уверенности 0.000000
Европейская межбанковская ставка 0.000000
Количество сотрудников в компании 0.000000
y 0.000000
dtype: float64

Таким образом, переменная подлежит удалению с помощью drop():

df = df.drop(columns=['День'])

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

  • NaN / NaT (упрощенно: "не число" / "не время")
  • Пустая ячейка
  • Для числовых признаков – радикальный выброс. К примеру, для столбца "День" это число 999.
  • Маркер или нестандартный символ

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

>>> column_values = df[['Работа', 'Семейный статус', 'Образование', 'Контакт', 'Месяц', 'День недели', 'Доходность']].values.ravel()
>>> unique_values =  pd.unique(column_values)
>>> print(unique_values)

['Самозанятый' 'Не женат / не замужем' 'Университетская степень'
 'Городской телефон' 'Октябрь' 'Пятница' 'Отсутствует' 'Преддприниматель'
 'Женат / замужем' 'Голубой воротничок' 'Базовое (9 классов)' 'Менеджер'
 'Высшая школа' 'Базовое (4 класса)' 'Техник' 'Профессиональный курс'
 'Разведен(-а)' 'Неизвестно' 'Сотовый телефон' 'Август' 'Понедельник'
 'Студент' 'Домохозяйка' 'Обслуживающий персонал' 'Базовое (6 классов)'
 'Пенсионер' 'Четверг' 'Вторник' 'Не присутствует' 'Июль' 'Среда' 'Июнь'
 'Неграмотный' 'Май' 'Ноябрь' 'Присутствует' 'Cамозанятый' 'Декабрь'
 'Март' 'Апрель' 'Сентябрь']

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

Процесс обработки пропусков, к счастью, можно сократить с помощью sklearn.impute.SimpleImputer. Мы выбираем все категориальные переменные и применяем стратегию "[вставить вместо пропуска] самое распространенное значение":

from sklearn.impute import SimpleImputer

imputer = SimpleImputer(missing_values = np.nan, strategy = 'most_frequent')

df["Работа"] = imputer.fit_transform(df["Работа"].values.reshape(-1,1))[:,0]

df["Семейный статус"] = imputer.fit_transform(df["Семейный статус"].values.reshape(-1,1))[:,0]

df["Образование"] = imputer.fit_transform(df["Образование"].values.reshape(-1,1))[:,0]

df["Месяц"] = imputer.fit_transform(df["Месяц"].values.reshape(-1,1))[:,0]

df["День недели"] = imputer.fit_transform(df["День недели"].values.reshape(-1,1))[:,0]

df["Доходность"] = imputer.fit_transform(df["Доходность"].values.reshape(-1,1))[:,0]

Такой код можно сократить еще с помощью Пайплайнов (Pipeline), однако здесь обработаем каждую переменную построчно.

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

imputer = SimpleImputer(missing_values = np.nan, strategy = 'most_frequent')

df["Кредитный дефолт"] = imputer.fit_transform(df["Кредитный дефолт"].values.reshape(-1,1))[:,0]

df["Ипотека"] = imputer.fit_transform(df["Ипотека"].values.reshape(-1,1))[:,0]

df["Займ"] = imputer.fit_transform(df["Займ"].values.reshape(-1,1))[:,0]

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

imputer = SimpleImputer(missing_values = np.nan, strategy = 'mean')

df["Возраст"] = imputer.fit_transform(df["Возраст"].values.reshape(-1,1))[:,0]

df["Длительность"] = imputer.fit_transform(df["Длительность"].values.reshape(-1,1))[:,0]

df["Кампания"] = imputer.fit_transform(df["Кампания"].values.reshape(-1,1))[:,0]

df["Предыдущий контакт"] = imputer.fit_transform(df["Предыдущий контакт"].values.reshape(-1,1))[:,0]

df["Колебание уровня безработицы"] = imputer.fit_transform(df["Колебание уровня безработицы"].values.reshape(-1,1))[:,0]

df["Индекс потребительских цен"] = imputer.fit_transform(df["Индекс потребительских цен"].values.reshape(-1,1))[:,0]

df["Индекс потребительской уверенности"] = imputer.fit_transform(df["Индекс потребительской уверенности"].values.reshape(-1,1))[:,0]

df["Европейская межбанковская ставка"] = imputer.fit_transform(df["Европейская межбанковская ставка"].values.reshape(-1,1))[:,0]

df["Количество сотрудников в компании"] = imputer.fit_transform(df["Количество сотрудников в компании"].values.reshape(-1,1))[:,0]

Метод isnull() пробегается по каждой ячейке каждого признака и определяет, пустая ли ячейка, возвращая True / False. Метод mean()

Обнаружение аномалий

Самый легкий способ обнаружить выбросы – визуальный. Мы построим разновидность графика "ящик с усами" для одной из числовых переменных –  "Возраст":

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

q = df["Возраст"].quantile(0.99)
q2 = df["Длительность"].quantile(0.99)
q3 = df["Кампания"].quantile(0.99)
q5 = df["Предыдущий контакт"].quantile(0.99)
q6 = df["Колебание уровня безработицы"].quantile(0.99)
q7 = df["Индекс потребительских цен"].quantile(0.99)
q8 = df["Индекс потребительской уверенности"].quantile(0.99)
q9 = df["Европейская межбанковская ставка"].quantile(0.99)
q10 = df["Количество сотрудников в компании"].quantile(0.99)

df[df["Возраст"] < q]
df[df["Длительность"] < q2]
df[df["Кампания"] < q3]
df[df["Предыдущий контакт"] < q5]
df[df["Колебание уровня безработицы"] < q6]
df[df["Индекс потребительских цен"] < q7]
df[df["Индекс потребительской уверенности"] < q8]
df[df["Европейская межбанковская ставка"] < q9]
df[df["Количество сотрудников в компании"] < q10]

Во второй части статьи о разведочном анализе Вы узнаете про:

  • Одномерный анализ (описательная статистика, важность признаков (Feature Importance)
  • Одномерный анализ (парный анализ, уменьшение размерности, стандартизация, Нормализация (Normalization)

Ноутбук, не требующий дополнительной настройки на момент написания статьи, можно скачать здесь.

Фото: @f7photo