Быстро удалить переменные нулевой дисперсии из data.frame

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

Здесь функция, которую я сейчас использую для удаления таких переменных из data.frame. В настоящее время он основан на apply, и мне было интересно, есть ли какие-либо очевидные способы ускорить эту функцию, чтобы она работала быстро на очень больших наборах данных с большим числом (400 или 500) переменных?

set.seed(1)
dat <- data.frame(
    A=factor(rep("X",10),levels=c('X','Y')),
    B=round(runif(10)*10),
    C=rep(10,10),
    D=c(rep(10,9),1),
    E=factor(rep("A",10)),
    F=factor(rep(c("I","J"),5)),
    G=c(rep(10,9),NA)
)
zeroVar <- function(data, useNA = 'ifany') {
    out <- apply(data, 2, function(x) {length(table(x, useNA = useNA))})
    which(out==1)
}

И вот результат процесса:

> dat
   A B  C  D E F  G
1  X 3 10 10 A I 10
2  X 4 10 10 A J 10
3  X 6 10 10 A I 10
4  X 9 10 10 A J 10
5  X 2 10 10 A I 10
6  X 9 10 10 A J 10
7  X 9 10 10 A I 10
8  X 7 10 10 A J 10
9  X 6 10 10 A I 10
10 X 1 10  1 A J NA

> dat[,-zeroVar(dat)]
   B  D F  G
1  3 10 I 10
2  4 10 J 10
3  6 10 I 10
4  9 10 J 10
5  2 10 I 10
6  9 10 J 10
7  9 10 I 10
8  7 10 J 10
9  6 10 I 10
10 1  1 J NA

> dat[,-zeroVar(dat, useNA = 'no')]
   B  D F
1  3 10 I
2  4 10 J
3  6 10 I
4  9 10 J
5  2 10 I
6  9 10 J
7  9 10 I
8  7 10 J
9  6 10 I
10 1  1 J

Ответы

Ответ 1

Не используйте table() - очень медленно для таких вещей. Один из вариантов: length(unique(x)):

foo <- function(dat) {
    out <- lapply(dat, function(x) length(unique(x)))
    want <- which(!out > 1)
    unlist(want)
}

system.time(replicate(1000, zeroVar(dat)))
system.time(replicate(1000, foo(dat)))

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

> system.time(replicate(1000, zeroVar(dat)))
   user  system elapsed 
  3.334   0.000   3.335 
> system.time(replicate(1000, foo(dat)))
   user  system elapsed 
  0.324   0.000   0.324

Решение Simon в этом примере аналогично быстро:

> system.time(replicate(1000, which(!unlist(lapply(dat, 
+             function(x) 0 == var(if (is.factor(x)) as.integer(x) else x))))))
   user  system elapsed 
  0.392   0.000   0.395

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

Ответ 2

Вы также можете просмотреть функцию nearZeroVar() в пакете каретки.

Если у вас есть одно событие из 1000, может быть хорошей идеей отказаться от этих данных (но это зависит от модели). nearZeroVar() может это сделать.

Ответ 3

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

var0 <- unlist(lapply(df, function(x) 0 == var(if (is.factor(x)) as.integer(x) else x)))

Это будет TRUE для 0-дисперсии, NA для столбцов с NA и FALSE для ненулевой дисперсии

Ответ 4

Хорошо, спасите себе время кодирования:

Rgames: foo
      [,1]  [,2] [,3]
 [1,]    1 1e+00    1
 [2,]    1 2e+00    1
 [3,]    1 3e+00    1
 [4,]    1 4e+00    1
 [5,]    1 5e+00    1
 [6,]    1 6e+00    2
 [7,]    1 7e+00    3
 [8,]    1 8e+00    1
 [9,]    1 9e+00    1
 [10,]    1 1e+01    1
Rgames: sd(foo)
[1] 0.000000e+00 3.027650e+00 6.749486e-01
Warning message:
sd(<matrix>) is deprecated.
 Use apply(*, 2, sd) instead.   

Чтобы избежать неприятных округлений с плавающей запятой, возьмите этот выходной вектор, который я назову "bar", и сделайте что-то вроде bar[bar< 2*.Machine$double.eps] <- 0, а затем, наконец, ваш фрейм данных dat[,as.logical(bar)] должен сделать трюк.

Ответ 5

Как насчет использования factor для подсчета количества уникальных элементов и цикла с помощью sapply:

dat[sapply(dat, function(x) length(levels(factor(x)))>1)]
   B  D F
1  3 10 I
2  4 10 J
3  6 10 I
4  9 10 J
5  2 10 I
6  9 10 J
7  9 10 I
8  7 10 J
9  6 10 I
10 1  1 J

NA исключаются по умолчанию, но это можно изменить с помощью параметра exclude factor:

dat[sapply(dat, function(x) length(levels(factor(x,exclude=NULL)))>1)]
   B  D F  G
1  3 10 I 10
2  4 10 J 10
3  6 10 I 10
4  9 10 J 10
5  2 10 I 10
6  9 10 J 10
7  9 10 I 10
8  7 10 J 10
9  6 10 I 10
10 1  1 J NA

Ответ 6

Используйте Caret Пакет и функцию nearZeroVar

require(caret)
NZV<- nearZeroVar(dataset, saveMetrics = TRUE)
NZV[NZV[,"zeroVar"] > 0, ] 
NZV[NZV[,"zeroVar"] + NZV[,"nzv"] > 0, ]

Ответ 7

Я думаю, что нулевая дисперсия эквивалентна постоянной, и можно обойтись без каких-либо арифметических операций вообще. Я бы ожидал, что range() превосходит var(), но я не подтвердил это:

removeConstantColumns <- function(a_dataframe, verbose=FALSE) {
  notConstant <- function(x) {
    if (is.factor(x)) x <- as.integer(x)
    return (0 != diff(range(x, na.rm=TRUE)))
  }
  bkeep <- sapply(a_dataframe, notConstant)
  if (verbose) {
    cat('removeConstantColumns: '
      , ifelse(all(bkeep)
        , 'nothing'
        , paste(names(a_dataframe)[!bkeep], collapse=',')
      , ' removed',  '\n')
  }
  return (a_dataframe[, bkeep])
}