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

Для тестирования производительности нам понадится пример данных. Я, конечно, мог бы взять уже готовый датасет, но мне всегда интереснее конструировать данные для примеров самому. Ниже приведена функция, которая генерирует таблицу данных, с заданным числом строк (nrow) и столбцов (ncol), а также долей пропущенных значений на один столбец (na.perc). В качестве генератора значений я взял фукнцию runif(), но это абсолютно не принципиально.

gen_data <- function(nrow = 10e3, ncol = 10, na.perc = 0.1) {
    m <- replicate(ncol, runif(nrow))
    for (col in 1:ncol) {
        idx <- sample(1:nrow, size = floor(nrow * na.perc))
        m[idx, col] <- NA
    }
    return(as.data.frame(m))
}

Создадим данные для дальнейшего тестирования.

dataset <- gen_data()

Итак, в R есть следующие способы удаления пропущенных значений из матриц и таблиц: функции na.omit() и complette.cases() из пакеты stats. Также можно использовать решение на основе примитива is.na(), которую я продемонстрирую ниже. Для тестирования производительности я, традиционно, использую пакет microbenchmark.

library(microbenchmark)
microbenchmark(
    na.omit = na.omit(dataset),
    comp.case = dataset[complete.cases(dataset), ],
    comp.case.which = dataset[which(complete.cases(dataset)), ],
    rowsums = dataset[rowSums(is.na(dataset)) == 0, ],
    roswums.which = dataset[which(rowSums(is.na(dataset)) == 0), ]
)
#> Unit: microseconds
#>             expr    min     lq mean median     uq   max neval cld
#>          na.omit 5943.8 6086.8 6755 6608.8 6770.1 32436   100   c
#>        comp.case 2600.4 2620.7 3412 2636.8 3310.2 32470   100  b 
#>  comp.case.which  788.4  800.5 1714  809.9  853.6 27413   100 a  
#>          rowsums 2968.1 2998.7 3617 3106.5 3731.5 28739   100  b 
#>    roswums.which 1149.7 1175.0 1450 1195.1 1869.5  2197   100 a

Как видим из результатов сравнения, наилучшие результаты показывает выриант с использованием функций complate.cases() и which(), а наименее эффективным na.omit(). Резльтутаы вполне предсказуемы, если мы рассмотрим как работают эти функции. na.omit() циклом проходит по всем столбцам и составляет список пропущенных занчений, после чего их исключает. conplate.cases() вызыввает внтреннюю функцию, написанную на C, поэтому работает значительно быстрее. is.na() и rowSums() тажке вызывают внутренние функции, написанные на C. Также ожидаемым ускорение при использовании which(), т.к. вектор и номеров строк обрабатывается быстрее логического вектора, хотя бы потому, что числовой вектор имеет меньший размер.

Немного другая ситуация наблюдается при манипуляции с data.table:

library(data.table)
datset <- setDT(dataset)
microbenchmark(
    na.omit = na.omit(dataset),
    comp.case = dataset[complete.cases(dataset), ],
    comp.case.which = dataset[which(complete.cases(dataset)), ],
    rowsums = dataset[rowSums(is.na(dataset)) == 0, ],
    roswums.which = dataset[which(rowSums(is.na(dataset)) == 0), ]
)
#> Unit: milliseconds
#>             expr   min    lq  mean median    uq    max neval cld
#>          na.omit 2.635 2.658 3.202  2.674 3.366 35.223   100   b
#>        comp.case 1.140 1.170 1.275  1.180 1.199  2.147   100  a 
#>  comp.case.which 1.157 1.170 1.605  1.180 1.194 34.800   100  a 
#>          rowsums 1.565 1.585 2.236  1.606 2.380 34.184   100  ab
#>    roswums.which 1.555 1.567 1.846  1.590 2.350  3.162   100  a

Как видим, во-первых, различия стали не такими значительными и, во-вторых, варианты с использованием which() и без него практически не различаются. Это подтверждает высокую оптимизацию data.table при работе с логическими векторами при фильтрации строк, а также циклами.