Пропущенные значения. Часть 2. | Анализ и обработка данных

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

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

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

Продолжим работу в том же ноутбуке

Про многомерные методы

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

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

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

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

Рассмотрим пример линейной регрессии и сразу два подхода к ее реализации, детерминированный и стохастический.

Линейная регрессия

Детерминированный подход

Детерминированный подход (deterministic approach) предполагает, что мы заполняем пропуски строго теми значениями, которые будут предсказаны линейной регрессией.

Подготовка данных

Теперь давайте подготовим данные.

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

В тестовую выборку мы поместим те наблюдения, в которых в столбце Age есть пропуски.

тестовая выборка (с пропусками)

В обучающей выборке напротив окажутся те строки, где в Age пропусков нет.

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

Из датафрейма train выделим столбец Age. Это будет наша целевая переменная.

Оценим результат.

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

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

Обучение модели и заполнение пропусков

Обучать модель линейной регрессии и строить прогноз мы уже умеем.

Пропущенные значения заполнены. Остается обратно «собрать» датафрейм.

тестовые данные + заполненные пропуски

Теперь у нас есть два датафрейма train и test со столбцом Age с заполненными пропусками.

повторно взглянем на обучающую выборку

Соединим их методом «один на другой» с помощью функции pd.concat().

соединим обучающую и тестовую выборки с помощью pd.concat()

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

датасет с обновленным индексом

Остается вернуть исходный масштаб.

результат заполнения пропусков с помощью линейной регрессии

В данном случае, если метод .fit_transform() вычитает из каждого значения среднее и делит на СКО,

$$ z = \frac{x-\bar{x}}{\sigma} $$

то метод .inverse_transform() в обратном порядке умножает каждое число на СКО и прибавляет среднее арифметическое.

$$ x = z * \sigma + \bar{x} $$

Проверим на наших данных. Подставим в формулу выше отмасштабированное значение возраста первого наблюдения (индекс 0).

Убедимся в отсутствии пропусков и посмотрим на размеры получившегося датафрейма.

Оценка результата

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

распределение Age после заполнения с помощью линейной регрессии (детерминированный подход)

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

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

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

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

Среднее арифметическое и медиана практически не изменились.

Особенность детерминированного подхода

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

Для того чтобы лучше это увидеть, во-первых, пометим изначальные (назовем их actual) и заполненные (imputed) значения столбца Age.

датасет с размеченными исходными и заполненными значениями

Во-вторых, создадим точечную диаграмму, где по оси x будет индекс датафрейма, по оси y — возраст, а цветом мы обозначим изначальное это значение, или заполненное.

распределение изначальных и заполненных значений (линейная регрессия, детерминированный подход)

На графике видно, что заполненные значения гораздо ближе к среднему значению (а зачастую просто равны ему), чем исходные данные. Аналогичную картину мы увидим, если рассчитаем соответствующие СКО.

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

Попробуем преодолеть этот недостаток через внесение в заполняемые данные элемента случайности (дополнительных колебаний).

Стохастический подход

При применении стохастического подхода (Stochastic Regression Imputation) мы будем использовать гауссовский шум (Gaussian noise), то есть такой шум (элемент случайности), который следует нормальному распределению. Объявим соответствующую функцию.

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

Заменим заполненные значения теми же значениями, но с добавлением шума.

добавление гауссовского шума в данные

Теперь соединим два датасета исходных и заполненных значений и оценим результат.

результат заполнения пропусков с помощью стохастической линейной регрессии
распределение Age после заполнения с помощью линейной регрессии (стохастический подход)

Как мы видим, распределение еще больше похоже на нормальное.

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

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

распределение изначальных и заполненных значений (линейная регрессия, стохастический подход)

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

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

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

MICE / IterativeImputer

Описанный выше алгоритм регрессии используется в алгоритме MICE или IterativeImputer. MICE расшифровывается как Multiple Imputation by Chained Equations, многомерный способ заполнения пропущенных данных с помощью цепных уравнений.

Принцип алгоритма MICE

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

данные о заемщиках: пропущенные значения

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

данные о заемщиках: истинные значения пропусков

Теперь перейдем непостредственно к алгоритму MICE. Это итерационный алгоритм, другими словами, мы будем раз за разом повторять определенный набор действий до тех пор, пока не придем к нужному результату.

Шаг 1. Заполняем данные с помощью среднего арифметического (целевую переменную мы отбросим). Это наша отправная точка.

данные о заемщиках: шаг 1

Эти значения очевидно далеко не оптимальны и еще раз демонстрируют ограниченность одномерных методов. В 25 лет сложно иметь семилетний стаж, три года стажа вряд ли обеспечат уровень заработной платы в 90 тысяч рублей (исходя из имеющихся данных), а человеку с 11 годами опыта скорее всего будет больше 29 лет.

Попробуем улучшить этот результат.

Шаг 2. Уберем заполненное только что значение возраста. Остальные заполненные значения трогать не будем.

данные о заемщиках: шаг 2

Шаг 3. Заполним пропуск с помощью линейной регрессии так, как мы это делали выше.

данные о заемщиках: шаг 3

В данном случае «Опыт» и «ЗП» (кроме последней строки) будут признаками обучающей выборки (X_train), возраст (кроме последней строки) — целевой переменной обучающей выборки (y_train), «Опыт» и «ЗП» последней строки — признаками тестовой выборки (X_test), а возраст последней строки — прогнозируемым значением (y_pred).

Шаги 4 и 5. Сделаем то же самое для двух других пропущенных значений.

данные о заемщиках: шаг 4
данные о заемщиках: шаг 5

Итак, мы завершили первую итерацию (цикл) работы алгоритма.

Шаг 6. Теперь давайте вычтем заполненные после первого цикла значения из исходного датасета, в котором пропуски представляют собой среднее арифметическое по столбцам.

данные о заемщиках: шаг 6

Получившиеся разницы — $-5,99, 6,02, 20$ — это критерий качества работы алгоритма.

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

Например, уже на второй итерации мы получим следующий результат.

Перейдем к практике.

Реализация на Питоне через класс IterativeImputer

Изначально алгоритм MICE был создан на языке R⧉, но сегодня доступен в качестве экспериментального класса IterativeImputer⧉ в библиотеке sklearn.

Теперь импортируем классы моделей, которые мы можем использовать внутри алгоритма MICE.

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

Создадим объект класса IterativeImputer и заполним пропуски.

результат заполнения пропущенных значений с помощью алгоритма MICE

Оценим качество получившегося распределения.

распределение Age после заполнения с помощью MICE

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

Количественно оценим получившееся распределение.

Рассмотрим принципиально другой алгоритм для заполнения пропусков, а именно метод ближайших соседей.

Метод k-ближайших соседей

Мы уже использовали алгоритм k-ближайших соседей (k-nearest neighbors algorithm, k-NN) для создания рекомендательной системы. Еще раз рассмотрим работу этого алгоритма на примере задачи классификации.

алгоритм k-ближайших соседей для заполнения пропусков
Источник: Википедия

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

  • Если мы возьмем k равное трем, то в число соседей войдут точки внутри меньшей окружности (сплошная линия).
  • Если пяти — внутри большей окружности (прерывистая линия)

При k = 3 большая часть соседей — красные треугольники. Именно к этой категории мы и относем зеленую точку. Если взять k = 5, зеленая точка будет классифицирована как синий квадрат.

Для количественной целевой переменной мы можем найти, например, среднее арифметическое k-ближайших соседей.

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

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

Перейдем к практике.

Sklearn KNNImputer

Вначале рассмотрим реализацию этого алгоритма в библиотеке Sklearn. Скопируем и масштабируем данные.

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

Вернем исходный масштаб данных.

результат заполнения пропусков с помощью KNNImputer

Осталось взглянуть на получившееся распределение.

распределение Age после заполнения с помощью KNNImputer

Распределение близко к нормальному.

impyute fast_knn

Особенности метода ближайших соседей

Метод ближайших соседей прост и в то же время эффективен.

При этом в базовом варианте его реализации у него есть один недостаток — долгое время работы или, как правильнее сказать, высокая временная сложность (time complexity) алгоритма.

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

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

Алгоритм k-ближайших соседей потребует двух циклов.

  1. В первом цикле мы будем поочередно брать по одному вектору запроса
  2. Во втором вложенном в него цикле мы будем для каждого вектора запроса находить расстояние до всех векторов сравнения.
  3. Наконец, найдя и отсортировав вектора сравнения по расстоянию, выберем для каждого вектора запроса k-ближайших.
  4. Далее приступим к решению задачи классификации, регрессии (хотя термин регрессия здесь не вполне корректно применять) или заполнению пропуска.

При большом количестве наблюдений (как векторов запроса, так и векторов сравнения) такая работа может занять очень много времени. Такой вариант реализации алгоритма, его еще называют методом перебора или методом опробования (brute force method), является возможным, но не единственным решением поставленной задачи.

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

Такой структурой данных может быть, в частности, k-мерное дерево или k-d-дерево (k-diminsional tree, k-d tree).

как устроен метод k-ближайших соседей

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

В дополнительных материалах к сегодняшнему занятию мы отдельно поговорим про временную сложность алгоритмов. Реализацию же различных вариантов метода k-ближайших соседей и создания k-мерных деревьев мы отложим до курса традиционных алгоритмов ML.

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

Функция fast_knn()

Вначале подготовим данные.

Установим библиотеку impyute.

Теперь воспользуемся фунцией fast_knn() для заполнения пропусков.

результат заполнения пропусков с помощью fast_knn

Остается взглянуть на распределение.

распределение Age после заполнения с помощью fast_knn()

DataWig

DataWig (буквально — «парик для данных») — библиотека, которая использует нейронную сеть для заполнения пропусков. Она была разработана специалистами подразделения Amazon Science⧉.

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

Откроем ноутбук с библиотекой Datawig

Установка и импорт библиотеки

Зачем нужен отдельный ноутбук? Дело в том, что эта библиотека требует установки более ранних версий основных библиотек, что нарушает работу остального кода. Например, на момент написания статьи в Google Colab стояла версия Pandas 1.3.5.

Установим библиотеку Datawig.

После установки Google Colab попросит перезапустить среду выполнения.

перезапуск среды выполнения после установки библиотеки Datawig

Datawig установил более раннюю версию Pandas 0.25.3 (и так со многими другими библиотеками).

Если вы захотите вернуть ноутбук Google Colab к исходным настройкам, нажмите Среда выполненияОтключиться от среды выполнения и удалить ее.

Импорт и подготовка данных

Подгрузим и импортируем датасет «Титаник», выберем нужные признаки, закодируем Sex и масштабируем данные.

результат заполнения пропусков с помощью Datawig

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

обучающая выборка без пропусков

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

тестовые данные с пропусками

Обучение модели и прогноз

Создадим объект модели и обучим ее через метод .fit().

Обратите внимание, с сессионном хранилище в Google Colab появилась папка imputer_model из прописанного выше параметра output_path. В этой папке хранятся файлы обученной модели.

файлы обученной модели в сессионном хранилище

Мы подробнее рассмотрим файлы json и pickle в дополнительных материалах.

Теперь воспользуемся методом .predict() для заполнения пропусков (по сути, создания столбца Age в тестовом датасете).

заполненные пропуски

В столбец Age_imputed модель поместила заполненные значения.

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

Восстановление датафрейма

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

результат заполнения пропусков с помощью Datawig

Еще раз убедимся в отсутствии пропусков и проверим размеры датафрейма.

Посмотрим на распределение возраста после заполнения с помощью нейросети.

распределение Age после заполнения с помощью Datawig

Сохранение результата

Остается сохранить результат в сессионное хранилище в формате .csv.

Скачать на жесткий диск можно либо через интерфейс сессионного хранилища, либо исполнив код ниже.

Теперь вернемся в основной ноутбук, подгрузим файл datawig.csv и импортием его.

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

Если по какой-либо причине у вас не получилось исполнить код с библиотекой Datawig, ниже вы можете скачать файл datawig.csv с уже заполненными пропусками и подгрузить его в сессионное хранилище.

Сравнение методов

Пришло время оценить качество алгоритмов заполнения пропусков. Для сравнения рассмотренных выше методов сделаем следующее:

  1. Возьмем получившиеся в результате применения каждого из методов одинаковые датасеты (различаются только значения, которыми были заполнены пропуски)
  2. Используем модель логистической регрессии для построения прогноза выживания пассажиров. Тот метод заполнения пропусков, с которым модель логистической регрессии покажется наилучший результат и будет считаться победителем

Вначале создадим два списка:

  • в первый поместим все датасеты с заполненным столбцом Age
  • во второй, соответствующие названия методов

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

Импортируем класс LogisticRegression и функцию accuracy_score() для оценки качества модели.

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

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

Отдельно обратите внимание на хорошие результаты заполнения пропусков внутригрупповой медианой (binned median) и наоборот невысокую точность алгоритма стохастической линейной регрессии. Во втором случае, снижение точности объясняется как раз тем, что мы чаще не угадывали куда должна двигаться вариативность, нежели оказывались правы.


Уточнение. Хотя в классификации выше метод заполнения внутригрупповым показателем отнесен к одномерным методам, де факто это метод многомерный, поскольку среднее или медиана столбца с пропусками рассчитываются на основе группировки по другим столбцам датасета.

Дополнение. Еще одним способом работы с пропусками является создание переменной-индикатора (indicator method), которая принимает значение 1, если пропуск присутствует, и 0, если отсутствует.


На отдельной странице приведены дополнительные материалы к этому занятию.

Подведем итог

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

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

Вопросы для закрепления

Как связаны три группы данных (пропущенные, наблюдаемые или отсутствующие значения) с тремя видами пропусков (MCAR, MAR, MNAR)?

Посмотреть правильный ответ

Вопрос. Чем многомерные методы заполнения пропусков отличаются от одномерных?

Посмотреть правильный ответ

Рассмотрим способы заполнения пропусков во временных рядах.


Ответы на вопросы

Вопрос. Что такое hot-deck imputation?

Ответ. Hot-deck imputation или метод горячей колоды предполагает, что мы используем данные того же датасета для заполнения пропусков. Внутри hot-deck imputation есть несколько различных способов заполнения пропусков:

  • в простейшем варианте мы случайным образом берем непропущенное наблюдение и используем его для заполнения пропуска
  • кроме этого, мы можем отсортировать данные по имеющимся признакам и заполнить пропуски предыдущим значением (last observation carried forward, LCOF). Считается, что таким образом мы заполняем пропуски наиболее близкими значениями.
  • подход LCOF можно модифицировать и предварительно разбить данные на подгруппы.

Помимо этого существует и cold-deck imputation или метод холодной колоды, когда данные для заполнения пропусков берутся из другого датасета.

Терминологически «колодой» (deck of cards) называли стопкуперфокарт (punched cards), на которые записывались данные или программа. При этом горячей называлась используемая сейчас «колода», холодной — та, которая была отложена.


Вопрос. Что делать, если пропуски заполнены каким-либо символом, а не NaN? Например, знаком вопроса.

Ответ. Вначале нужно превратить этот или эти символы в NaN, а дальше работать как со стандартными пропусками.

пропуски в виде знаков вопроса
преобразование вопросительных знаков в NAN

Вопрос. Чем метод .isnull() отличается от метода .isna()?

Ответ. Это одно и то же.


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

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

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


Вопрос. Какие библиотеки для заполнения пропусков существуют на R?

Ответ. Посмотрите imputeTS⧉.