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

Пропущенные значения

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

В реальных данных встречаются не только ошибки, но и пропущенные значения (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 для заполнения пропусков.

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

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

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

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

Более продвинутый подход — многомерные методы (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) и наоборот невысокую точность алгоритма стохастической линейной регрессии. Во втором случае, снижение точности объясняется как раз тем, что мы чаще не угадывали куда должна двигаться вариативность, нежели оказывались правы.


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


Дополнительные материалы

Тест Литтла для выявления MCAR

Для того чтобы количественно определить полностью случайным ли образом сформированы пропущенные значения (MCAR), существует тест Литтла (Little’s test). Этот критерий был предложен⧉ в 1988 году Родериком Литтлом в качестве единого критерия оценки случайности пропущенных значений в многомерных количественных (!) данных.

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

Так как функция для проведения этого теста есть только на языке R, создадим новый ноутбук на R в Google Colab.

Откроем ноутбук с кодом на R

Датасет airquality

Вначале попрактикуемся на встроенном в R датасете airquality.

Посмотрим на первые строки датафрейма с помощью функции head().

датафрейм airquality в R

Оценим размерность.

Посмотрим на общие статистические показатели с помощью функции summary().

функция summary() на R

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

Посмотрим на абсолютное количество и процент пропусков в каждом из столбцов.

абсолютное количество и процент пропусков в каждом из столбцов

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

тест Литтла с помощью функции mcar_test()

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

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

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

Датасет «Титаник»

Подгрузите файл train.csv в сессионное хранилище ноутбука на R. Импортируем его с помощью функции read.csv().

датасет "Титаник" на R

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

Посмотрим на количество и процент пропусков в каждом столбце.

пропуски с помощью функции miss_var_summary()

Проведем тест Литтла.

тест Литтла на датасете "Титаник"

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

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

Временная сложность алгоритма

Сравнение алгоритмов

Важность сравнения эффективности алгоритмов очевидна. Менее очевидным является способ их сравнения.

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

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

Линейный и бинарный поиск

Вернёмся к рассмотрению алгоритмов линейного и бинарного поиска. Вначале приведем соответствующие функции.

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

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

Найдем в этих массивах индекс чисел 28 и 42 соответственно. Алгоритм линейного поиска ожидаемо справится за восемь и шестнадцать операций.

Заметим, что это худший случай из возможных (worst case scenario), искомые числа оказались на самом неудачном месте.

Алгоритму же бинарного поиска потребуется всего лишь четыре и пять операций соответственно.

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

Количество операций (k) линейного поиска растет пропорционально количеству данных (n), т.е. $k = n$, это линейная зависимость, в бинарном же поиске зависимость логарифмическая $k = \log(n)$.

Сравним рост количества операций по мере роста объема данных.

Выведем результат на графике.

зависимость количества операций поиска от длины массива

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

Логарифмическая зависимость бинарного поиска

На каждом этапе мы делим данные пополам и выполняем операцию сравнения. Например, массив из 16-ти чисел мы разделим четыре раза.

$$ \frac{n}{2}, \frac{n}{4}, \frac{n}{8}, \frac{n}{16}, \text{т.е.} \frac{n}{2^{k}} $$

В нашем случае,

$$ \frac{16}{2^k} $$

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

$$ \frac{n}{2^k} = 1, 2^k = n $$

Тогда k (количество операций) будет равно

$$ k = \log_2 (n) $$

В нашем случае,

$$ 4 = \log_2 (16) $$

Нотация «О» большого

Такая оценка называется временной сложностью (time complexity) алгоритма и обычно выражается в O-символике или нотации «О» большого (big O notation). Для приведенных выше алгоритмов запись будет выглядеть так:

  • линейный поиск: $O(n)$
  • бинарный поиск: $O(\log(n))$

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

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

Основание логарифма обычно не приводится поскольку

$$ \log_a b = \frac{\log_c b}{\log_c a} $$

Асимптотическое время

Нотация «О» большого отражает так называемое асимптотическое время работы алгоритма.

Асимптотический анализ (asymptotic analysis) изучает поведение функции при стремлении аргумента к определенному значению. В нашем случае мы смотрим на количество операций (поведение функции) при значительном увеличении объема данных (аргумент).

Скорость изменения функции ещё называют порядком изменения, от англ. order или нем. Ordnung, отсюда буква «O» в нотации.

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

Существует также константное время (когда сложность алгоритма не зависит от объема данных, $O(1)$, квадратичное время $O(n^2)$ или, например, факториальное $O(n!)$. В последнем случае, количество операций растет наиболее стремительно.

Про константы

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

Мы взяли массивы из 8-ми и 16-ти чисел и по формуле, должны были уложиться в $ \log_2(8) = 3 $ и $ \log_2(16) = 4 $ операции. Фактически же мы затратили четыре и пять соответственно.

Другими словами сложность алгоритма бинарного поиска в худшем случае составляет $ O(\log(n) + 1) $, однако так как нас интересует порядок (примерное понимание) роста функции при росте аргумента, а не точное значение, константы принято опускать.

Аналогично $O (2\log(n)) $ сводится к $ O (\log(n)) $.

Временная сложность в ML

Как применять вычислительную сложность алгоритма на практике? Рассмотрим два примера.

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

  • Во втором случае (brute force) временная сложность равна $O(n, m)$, где $n$ — количество векторов запроса, а $m$ — количество векторов сравнения.
  • В первом (kdtree) — $O(n\log(m))$, правда без учета операций для создания k-мерного дерева.

Кроме того, если мы знаем из-за какого компонента данных количество необходимых операций растет наиболее быстро, именно с этого компонента мы и начнем оптимизацию модели.

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

$$O(knp^3\min(n,p))$$

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

Еще одно сравнение методов заполнения пропусков

Способ сравнения

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

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

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

Создание данных с пропусками

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

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

Обратите внимание на один нюанс. Функция random.sample(), что удобно, выбирает элемент без возвращения, то есть один раз выбрав наблюдение с индексом «один», второй раз она это наблюдение не выберет.

Мы могли бы использовать функции np.random.randint() или random.choice(), однако в этом случае из-за повторов процент пропусков был бы чуть ниже желаемого, например не 20, а 18.

Применим объявленную функцию к датафрейму и создадим 20 процентов пропусков в первом столбце.

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

Перейдем к заполнению пропусков.

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

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

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

Заполнение линейной регрессией

Напишем функцию для заполнения пропусков линейной регрессией.

Заполним пропуски.

Заполнение с помощью MICE
Заполнение с помощью KNNImputer

В данном случае используем только один из методов k-ближайших соседей, а именно класс KNNImputer библиотеки sklearn.

Заполнение с помощью Datawig

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

Перейдем в ноутбук с библиотекой Datawig⧉. Подгрузим только что сформированный файл.

Подготовим данные.

Обучим модель.

Сделаем прогноз.

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

Теперь, прежде чем «склеивать» train и test, нам нужно переименовать столбец mean radius_imputed и перенести его на первое место. Сделать это можно, соединив два метода библиотеки Pandas, метод .insert() и метод .pop().

применение методов .insert() и .pop() библиотеки Pandas

Соединим части датасета и обновим индекс.

Сформируем файл для переноса в основной ноутбук.

Также вы можете скачать уже готовый файл.

Вернемся обратно в основной ноутбук. Подгрузим и импортируем файл datawig_xnan.csv.

Остается оценить качество созданных моделей.

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

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

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

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

Обратите внимание, результат линейной регрессии совпал с результатом алгоритма MICE (как в случае с датасетом «Титаник», так и в текущем сравнении методов). Это логично, MICE, как и линейная регрессия использовали один и тот же алгоритм (класс LinearRegression) и одни и те же признаки без пропусков (и MICE не пришлось заполнять их средним значением).

Сериализация и десериализация данных

Ранее мы обратили внимание на то, что библиотека Datawig создала файлы модели, в частности, в форматах JSON и Pickle. Зачем это нужно?

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

Процесс преобразования объекта (в частности, модели) в формат, пригодный для хранения и передачи данных, называется сериализацией (serialization). Процесс восстановления из этого формата называется соответственно десериализацией (deserialization).

сериализация и десериализация данных

Вначале рассмотрим в целом процесс серилизации в JSON и Pickle, а затем перейдем к работе с моделями.

Формат JSON

Формат JSON расшифровывается как JavaScript Object Notation, но, несмотря на наличие слова JavaScript в своем названии, не зависит от языка программирования. Кроме того, он позволяет сериализовать данные в человекочитаемый формат.

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

По сети мы получили объект типа bytes, то есть последовательность байтов, и десериализовали его с помощью метода .loads() в формат питоновского словаря.

Убедиться в человекочитаемости этого формата можно просто перейдя по ссылке в браузере: https://random-data-api.com/api/v2/banks⧉.

Вложенный словарь и список словарей

JSON-объект можно создать из вложенных питоновских словарей или списка словарей.

Методы .dumps() и .loads()

Вначале применим метод .dumps() для создания строкового JSON-объекта.

Обратите внимание, что хотя объект похож на питоновский словарь, тем не менее это строка. Восстановим словарь с помощью метода .loads().

Методы .dump() и .load()

Метод .dump() создает последовательность байтов и используется для записи JSON-объекта в файл. Этот метод принимает два основных параметра: источник данных и файл, в который следует записать JSON-объект. Передадим ему этот файл с помощью конструкции with open().

Восстановим список словарей из файла.

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

Обратите внимание, результат десериализации — это новый объект.

JSON и Pandas

Библиотека Pandas позволяет сохранить датафрейм в файл формата JSON, а также импортировать такой файл и соответственно восстановить датафрейм.

создание датафрейма из JSON-файла
Скриншот демонстрирует только часть датафрейма

Pickle

Если JSON-объект можно создать на Питоне, а восстановить на Java, то объект Pickle сериализуется и десериализуется только с помощью Питона. За это отвечает одноименная библиотека.

Методы .dumps() и .loads()

Методы .dumps() и .loads() преобразуют объект в байты и восстанавливают исходный тип данных соответственно.

Методы .dump() и .load()

Методы .dump() и .load() сериализуют объект в файл и соответственно десериализуют объект из файла.

При создании файла в формате pickle можно использовать расширения .p, .pkl или .pickle.

Собственные объекты

В отличие JSON, который позволяет сериализовать только ограниченный набор объектов, Pickle может сериализовать любой питоновский объект, например собственную функцию или класс.

Начнем с функций.

То же самое можно сделать с классом.

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

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

Сохраняемость модели ML

Чаще всего для этого используется формат Pickle, потому что он способен сериализовать и десериализовать сложные объекты, которыми являются модели.

В машинном обучении говорят, что процесс сериализации и десериализации выполняется ради обеспечения сохраняемости модели (model persistance).

При этом, при использовании модуля Pickle, на этапах сериализации и десериализации важно работать с одинаковыми версиями Питона и используемых библиотек (например, sklearn). В противном случае результат может быть непредсказуемым. Это заставило некоторых специалистов предложить⧉ серилизацию модели ML с помощью JSON.

Рассмотрим сериализацию модели на практике. Вначале обучим модель логистической регрессии.

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

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

Теперь выполним сериализацию и десериализацию модели.

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

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

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

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

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

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

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

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

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

  • если ни наблюдаемые, ни отсутствующие значения не влияют на пропуски, то это 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⧉.