NA означает «неизвестно» — и оно расползается
В реальных наборах данных есть дыры: незаполненный вопрос анкеты, датчик, пропустивший показание. R обозначает каждую дыру как NA — not available. Ключевая мысленная модель: NA — это не ноль, не пустая строка и не особое число. Это означает «здесь есть значение, но мы не знаем какое».
Отнеситесь к этому серьёзно — и поведение R станет логичным. Чему равно неизвестное число плюс один? Неизвестно. Каково среднее от 4, 8 и чего-то неизвестного? Неизвестно — пропущенное значение может быть любым, значит и среднее может быть любым:
Все три печатают NA. Эта заразительность — свойство, а не баг: R отказывается тихо делать вид, будто знает ответ, которого не знает. Электронная таблица, молча пропускающая пустые ячейки, может скрыть тот факт, что половина столбца отсутствует; R заставляет вас заметить это и решить, что пропущенные значения должны означать для вашего анализа. Остальная часть статьи — о том, как принять это решение осознанно.
Проверка на NA: is.na(), а не ==
Вот первая ловушка, в которую попадают все. Вы хотите найти пропущенные значения, поэтому пишете сравнение — и оно не работает:
x == NA возвращает NA NA NA — ни одного TRUE. Почему? Проследите логику «неизвестного»: равно ли 4 некоторому неизвестному значению? Сказать нельзя — неизвестное может быть четвёркой. Даже слот с NA сравнивается как NA: равно ли одно неизвестное другому? Неизвестно. Сравнение с NA никогда не может вернуть TRUE или FALSE, поэтому как проверка оно бесполезно — и хуже того, маска сплошь из NA внутри [ ] выберет не то, что вы ожидаете.
Правильный инструмент — is.na(), созданная отвечать ровно на этот вопрос и возвращающая честные логические значения: FALSE TRUE FALSE. Отсюда две идиомы, которыми вы будете пользоваться постоянно:
sum(is.na(x)) считает пропуски (TRUE суммируется как 1) — запускайте это по каждому столбцу нового набора данных прежде всего остального. which(is.na(x)) находит их. А x[!is.na(x)] оставляет только наблюдённые значения — ручное удаление через логическую маску, тот же шаблон фильтрации векторов, что и всегда.
Пропуск NA в сводках: na.rm = TRUE
Обычно удалять NA вручную не требуется, потому что у сводных функций R есть встроенный запасной выход — аргумент na.rm (NA remove):
С na.rm = TRUE функция отбрасывает пропущенные значения и считает по оставшимся: среднее от 4 и 8 равно 6. sum(), sd(), median(), min(), max(), var() — всё семейство описательных статистик его принимает.
Обратите внимание, что по умолчанию стоит FALSE. Это R занимает принципиальную позицию в вашу пользу: он хочет, чтобы игнорирование пропущенных данных было явным выбором, который вы записали, а не молчаливым умолчанием. Когда вы набираете na.rm = TRUE, вы утверждаете: «пропущенные значения здесь можно безопасно игнорировать» — что верно, когда датчик случайно пропустил несколько показаний, и опасно неверно, когда, скажем, вопрос о доходе пропустили именно самые малообеспеченные. Этот аргумент заставляет вас взять решение на себя.
Удаление неполных строк: na.omit() и complete.cases()
В датафрейме пропуски живут в ячейках, но анализ часто идёт по строкам — поэтому частая операция это удаление строк, где есть хоть один NA:
na.omit(df) возвращает датафрейм без каждой строки, содержащей хотя бы один NA, — здесь выживает только Rosa. complete.cases(df) возвращает логическую маску строк (TRUE FALSE FALSE), стоящую за той же идеей, а индексация ею даёт идентичный результат с двумя преимуществами: можно сначала посчитать, что вы собираетесь потерять (sum(!complete.cases(df))), и можно ограничить, какие столбцы важны, — df[complete.cases(df[, "age"]), ] отбрасывает только строки с пропущенным age, сохраняя Mia, хотя её балл неизвестен.
Эта форма с ограничением по столбцам важнее, чем кажется: na.omit() на широком датафрейме может молча выбросить большую часть ваших строк из-за NA в столбцах, которые вы вообще не собирались анализировать. Знайте, сколько строк вы удаляете и почему, прежде чем удалять.
Замена NA
Иногда правильный ход — заполнить дыры, а не удалять строки. Идиома сочетает is.na() с присваиванием:
Читается это так: «в слоты, где visits пропущен, поставить 0». Такое законно ровно тогда, когда NA кодирует известное значение: у клиента без записей о визитах действительно было ноль визитов.
Но будьте честны насчёт того, когда это верно. Если NA означает «мы не смогли измерить», подстановка 0 фабрикует данные: замена пропущенных результатов теста нулями тянет среднее вниз, как будто те студенты получили ноль, тогда как на самом деле вы не знаете, сколько они набрали. Сравните среднее выше (2.4) со средним по оригиналу с na.rm (4): те же данные, разные утверждения. Замена средним или медианой искажает меньше, но всё равно занижает изменчивость. Правило: подставляйте значение только тогда, когда можете простыми словами объяснить, почему именно этим значением и была пропущенная запись. Иначе оставьте NA и используйте na.rm — «неизвестно» часто самое правдивое значение в наборе данных.
NA против NULL, NaN и Inf
В R есть четыре похожих специальных значения, означающих действительно разные вещи:
| Значение | Смысл | Длина | Типичный источник |
|---|---|---|---|
NA | Значение есть, но неизвестно | 1 (занимает слот) | Пропущенные данные |
NULL | Объекта нет вообще | 0 (нет слота) | Удалённый элемент списка, пустой результат |
NaN | Математика с неопределённым ответом | 1 | 0 / 0, log(-1) |
Inf | Число за пределами представления | 1 | 1 / 0, переполнение |
Различия, важные на практике: NULL исчезает внутри векторов (c(1, NULL, 3) имеет два элемента — он не может представлять пропущенное наблюдение), тогда как NA держит своё место. NaN считается пропуском (is.na(NaN) даёт TRUE, поэтому na.rm удаляет и его), но обратное неверно — is.nan(NA) даёт FALSE. А Inf не является пропуском — это настоящее сравнимое число (Inf > 1e300 даёт TRUE), поэтому na.rm его не уберёт; используйте is.finite(), чтобы отфильтровать обычные числа.
Для полноты: NA тихо существует в типизированных разновидностях (NA_integer_, NA_real_, NA_character_), чтобы помещаться в любой вектор, не нарушая правило одного типа. Набирать их вам придётся редко, но вы увидите их в коде пакетов и сообщениях об ошибках dplyr.
Что вы уносите с собой
NAозначает «неизвестно», а неизвестное заразительно: любое вычисление, коснувшееся NA, возвращает NA — так и задумано.- Проверяйте через
is.na(), никогда через== NA; считайте дыры черезsum(is.na(x)). na.rm = TRUEзаставляет сводные функции пропускать NA — явное решение на каждый вызов о том, что пропусками можно пренебречь.na.omit()оптом отбрасывает неполные строки;complete.cases()даёт маску, которую можно посчитать и ограничить нужными столбцами.- Заменяйте NA (
x[is.na(x)] <- value) только когда можете обосновать, чем на самом деле было пропущенное значение. - NA ≠ NULL ≠ NaN ≠ Inf: пропущенное значение, отсутствующий объект, неопределённая математика, неограниченное число.
Дальше: функции — упаковка вашей логики в переиспользуемые именованные части.
Часто задаваемые вопросы
Почему mean() возвращает NA в R?
Потому что хотя бы одно значение в векторе равно NA, а NA заразителен: если какой-то вход неизвестен, R говорит, что и ответ неизвестен. Передайте na.rm = TRUE — mean(x, na.rm = TRUE), — чтобы посчитать среднее по имеющимся значениям. Большинство сводных функций (sum, sd, median, min, max) принимают тот же аргумент.
Как проверить наличие NA в R?
С помощью is.na(x), которая возвращает TRUE везде, где значение пропущено. Никогда не проверяйте через x == NA — сравнение чего угодно с неизвестным даёт NA, а не TRUE или FALSE, поэтому такая проверка молча не работает. sum(is.na(x)) считает пропуски; which(is.na(x)) находит их позиции.
Как удалить строки с NA из датафрейма в R?
na.omit(df) отбрасывает каждую строку, содержащую хотя бы один NA. Для большего контроля complete.cases(df) возвращает логический вектор, помечающий полностью заполненные строки, так что df[complete.cases(df), ] делает то же самое явно — и вы можете применить это только к нужным столбцам, например df[complete.cases(df[, c("age", "score")]), ].
В чём разница между NA и NULL в R?
NA — это пропущенное значение: оно занимает место в векторе и имеет длину 1. NULL — это отсутствие объекта: у него длина 0, и он исчезает при помещении в вектор — в c(1, NULL, 3) всего два элемента. Используйте NA для пропущенного наблюдения; NULL появляется при удалении элементов списка или как пустой возврат.