Пропущенные значения. Часть 1.

Все курсы > Анализ и обработка данных > Занятие 6 (часть 1)

В реальных данных встречаются не только ошибки, но и пропущенные значения (missing values). При этом не все алгоритмы машинного обучения умеют работать с данными, в которых есть пропуски.

Вначале немного теории.

Типы пропусков

В 1976 году математик Дональд Рубин (Donald B. Rubin) предложил следующую классификацию пропущенных значений.

Полностью случайные пропуски

Полностью случайные пропуски (missing completely at random, MCAR) предполагают, что вероятность появления пропуска никак не связана с данными. Такие пропуски возникают, например, если измерительный прибор неисправен и случайным образом не записал часть наблюдений, или если один из образцов крови, изучаемых в лаборатории, оказался поврежден и по этой причине его характеристики выпали из исследования.

Интересно посмотреть на эту классификацию с точки зрения условной вероятности. Введем обозначения.

  • П — пропуски в данных (missing data)
  • Н — наблюдаемые значения, то есть те данные, которые мы собрали (observed data)
  • О — отсутствующие значения, или те данные, которые собрать не удалось (unobserved data)

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

P (П | Н, О) = константа

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

Эту же идею можно выразить и так.

P (П | Н, О) = P (П)

Считается, что в реальности наблюдать полностью случайные пропущенные значения очень сложно. Какие-либо закономерности (то есть связь с наблюдаемыми или отсутствующими значениями) все равно существуют. Это приводит нас ко второй категории пропусков.

Случайные пропуски

Случайные пропуски (missing at random, MAR) — вероятность появления пропуска зависит от некоторой известной нам переменной. Например, отсутствие ответа на определенный вопрос анкеты может зависеть от возраста респондента. Молодые охотнее отвечают на вопрос, люди более пожилого возраста скорее избегают ответа.

Если мы знаем об этой особенности, то можем, правильно собирая и корректируя данные, добиться большей объективности.

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

Р (П | Н, О) = $f$(Н)

В нашем примере, такой функцией является функция возраста респондентов, $f$(возраст).

Неслучайные пропуски

Неслучайные пропуски (missing not at random, MNAR) — вероятность появления пропуска зависит, в том числе, от фактора, о котором мы ничего не знаем. Например, у весов может быть верхний предел измерения и любой образец выше этого предела автоматически не записывается. В опросах общественного мнения MNAR возникает, когда люди с более активной жизненной позицией (переменная, которую мы не измеряем) чаще дают ответы на вопросы интервьюера.

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

Р (П | Н, О) = $f$(Н, O)

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

Откроем ноутбук к этому занятию

Выявление пропусков

В первую очередь подготовим необходимые данные. Сегодня мы снова будем работать с датасетом «Титаник».

Базовые методы

Метод .info()

Рассмотрим базовые методы обнаружения пропусков. В первую очередь, можно использовать метод .info(). Этот метод соотносит максимальное количество записей в датафрейме с количеством записей в каждом столбце.

Как мы видим, всего в датасете может быть до 891 записи. При этом в столбцах Age, Cabin и Embarked записей меньше, а значит есть пропуски.

Также обратите внимание на одну особенность Питона. Столбец Age логично преобразовать в тип int, однако из-за того, что в нем есть пропущенные значения, сделать этого не получится. Для количественных данных с пропусками доступен только тип float.

тип float в числовых данных с пропусками

Конечно, если столбцов много, результат метода .info() становится трудно воспринимать.

Методы .isna() и .sum()

Можно последовательно использовать методы .isna() и .sum().

Процент пропущенных значений

Также не сложно посчитать процент пропущенных значений.

Теперь нам гораздо проще оценить «масштаб бедствия».

Библиотека missingno

Библиотека missingno предоставляет удобные средства для визуальной оценки пропусков.

Кроме того, для повышения качества визуализации сделаем стиль графиков seaborn основным.

В первую очередь на пропуски можно посмотреть с помощью столбчатой диаграммы (функция msno.bar()).

функция msno.bar() для визуализации пропущенных значений

На этом графике мы четко видим процент (слева) и абсолютное количество (справа и сверху) заполненных значений.

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

Для этого подойдет матрица пропущенных значений (функция msno.matrix()).

функция msno.matrix() для построения матрицы пропущенных значений

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

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

Матрица корреляции пропущенных значений

Еще один интересный инструмент — матрица корреляции пропущенных значений (nullity correlation matrix).

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

Если мы знаем, в каких столбцах есть пропуски, то можем просто последовательно применить к ним методы .isnull() и .corr().

матрица корреляции пропущенных значений, библиотека Pandas

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

матрица корреляции пропущенных значений, собственная функция

Значения корреляции могут быть от −1 (если значения одного признака присутствуют, значения другого — отсутствуют) до 1 (если присутствуют значения одного признака, то присутствуют значения и другого). Более подробно про корреляцию мы поговорим при изучении взаимосвязи переменных.

Визуально, корреляцию пропущенных значений можно представить с помощью тепловой карты (heatmap). Для этого есть функция msno.heatmap().

тепловая карта пропущенных значений, функция msno.heatmap()

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

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

Удаление пропусков

Во многих случаях удаление пропусков (missing values deletion) может оказаться неплохим решением, потому что в этом случае мы не «портим» данные.

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

Удаление строк

Удаление строк (deleting rows или listwise deletion, также называется анализом полных наблюдений, complete case analysis), в которых есть пропуски — наиболее очевидный подход к работе с пропущенными значениями. Рассмотрим этот способ на практике.

В датасете «Титаник» только два пропущенных значения в столбце Embarked. Удалим соответствующие строки.

Удаление строк не стоит применять, если пропущенные значения зависят от какого-либо неизвестного нам фактора (MNAR). Например, если на вопрос анкеты не склонны отвечать менее активные граждане, удаление строк с пропусками оставит в данных только определенную группу населения (появится bias, искажение) и алгоритм не будет репрезентативен.

Кроме того, если в одном из столбцов большой процент пропусков, построчное удаление просто оставит нас без данных. В датасете «Титаник» это относится к столбцу Cabin. В этом случае, если мы выбираем стратегию удаления данных, разумнее удалить сам столбец.

Удаление столбцов

Удаление столбцов (column deletion) несложно выполнить с помощью метода .drop(). Например, удалим столбец Cabin, в котором более 77 процентов пропусков.

Попарное удаление пропусков

Попарное удаление пропусков (pairwise deletion или, как еще говорят, анализ доступных данных, available case analysis) проще понять, если представить, что мы не удаляем пропуски, а игнорируем их или используем только доступные значения.

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

попарное удаление пропусков

Как вы видите, если верить столбцу Age, пассажиров на борту меньше, чем если руководствоваться данными, например, столбца PassengerId.

Это значит, что метод .count() игнорировал пропуски. То же самое касается, например, метода .mean() или метода .corr().

метод .corr() предполагает попарное удаление пропусков

Построение модели. Преимуществом при построении модели будет то, что мы по максимуму используем имеющиеся данные. Например, у нас есть два признака, и в первом есть пропуск у четвертого наблюдения (с индексом «три»), а во втором — у пятого.

принцип попарного удаления пропущенных значений

Если мы построим первую модель (A) на основе признака 1 (и соответственно удалим только четвертое наблюдение), а вторую (B) — на основе признака 2 (удалив пятое), то избежим необходимости каждый раз удалять два наблюдения и терять информацию.

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

Заполнение пропусков

Как уже было сказано выше, удаление пропусков не всегда возможно. В этом случае прибегают к заполнению пропусков (missing values imputation). Подготовим данные.

пропущенные значения в датасете "Титаник"

Одномерные методы

Одномерные методы (Single Imputation) — это заполнение с использованием данных одного столбца. Другими словами, чтобы заполнить пропуски мы берем данные того же признака.

Заполнение константой

Количественные данные. Самый простой способ работы с пропусками в количественных данных — заполнить пропуски константой. Например, нулем (подходит для алгоритмов, чувствительных к масштабу признаков).

Воспользуемся методом .fillna().

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

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

Категориальные данные. Для категориальных признаков в некоторых случаях можно провести дополнительное исследование. В частности, в датасете «Титаник» есть два пассажира с неизвестным портом посадки.

пассажиры "Титаника" с неизвестным портом посадки

При этом в Интернете⧉ можно найти информацию о том, что обе пассажирки (Mrs Stone и ее служанка Amelie Icard) зашли на борт в порту Southampton (S).

информация о пассажирах "Титаника"

Для заполнения строковым значением также подойдет метод .fillna().

Конечно, такая информация о пропущенных значениях бывает доступна далеко не всегда.

Вместо метода .fillna() можно использовать инструмент библиотеки sklearn, который называется SimpleImputer. Создадим объект этого класса и обучим модель.

Теперь применим эту модель для заполнения пропусков.

В конце раздела мы проведем сравнение эффективности различных методов заполнения пропусков и столбец Embarked нам уже не понадобится.

результат заполнения пропущенных значений константой

Заполнение средним арифметическим или медианой

Количественные данные можно заполнить средним арифметическим или медианой (Statistical Imputation). Вначале воспольуемся методом .fillna().

У такого простого и понятного подхода тем не менее есть ряд недостатков.

  • Во-первых, когда в данных появляется большое количество одинаковых близких к среднему значений, мы снижаем ценную вариативность в данных.
  • Кроме того, такое заполнение пропусков может быть некорректно. Ниже мы рассмотрим пример данных кредитного скоринга, где, если заполнить пропуски в столбце «Стаж» средним значением или медианой, молодой сотрудник может получить больший стаж, чем у него есть на самом деле, а сотрудник в возрасте, меньший.

Еще раз обратимся к столбцу Age в датасете «Титаник» и рассмотрим распределение возраста до и после заполнения медианой.

распределение Age до заполнения пропусков

Посмотрим на среднее арифметическое и медиану.

Используем класс SimpleImputer библиотеки sklearn для заполнения пропусков этим медианным значением.

Посмотрим на распределение возраста и его медианное значение после заполнения пропусков.

распределение Age после заполнения медианой

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

Заполнение внутригрупповым значением

Справиться с этой проблемой можно, в частности, через более сложный способ заполнения пропусков количественного признака — вначале разбить пассажиров на категории (bins), например, по полу или классу каюты, вычислить медианное значение для каждой категории и только потом заполнять им пропущенные значения.

Выполним группировку с помощью метода .groupby() и найдем медианный возраст каждой группы.

Применим lambda-функцию к объекту SeriesGroupBy и заменим пропуски соответствующим медианным значением.

Убедимся, что в столбце Age не осталось пропусков.

Посмотрим на распределение.

распределение Age после заполнения внутригрупповой медианой

Мы видим, что медианное значение доминирует гораздо меньше.

Рассмотрим другие методы.

Заполнение наиболее частотным значением

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

Подготовим данные и посмотрим на распределение категорий в столбце Embarked.

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

Воспользуемся классом SimpleImputer для заполнения пропусков.

Проверим результат.

Примечание. Приведу еще один простой способ найти моду. Его можно использовать совместно с методом .fillna() для заполнения пропусков.

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

Очевидно, каким бы одномерным методом мы ни воспользовались, мы всегда ограничены данными одного признака.

Перейдем ко второй части занятия.