Ускорение функции: проверка количества NA перед вычислением среднего

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

Моя проблема в том, что моя текущая реализация довольно неэффективна. Это занимает в 7 раз больше времени, чем просто запуск mean(vec, na.rm=TRUE)

Я попробовал альтернативный метод с использованием na.omit, но он еще медленнее.

Учитывая размер моих данных, выполнение одного lapply занимает более 40 минут.

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


ОБНОВЛЕНИЕ - RE: решение @thelatemail и комментарий @Arun:

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

Альтернативные образцы данных, чтобы избежать путаницы

# Sample Data 
# ------------
  set.seed(1)
  # slightly different sizes for each group
  N1 <- 5e3
  N2 <- N1 + as.integer(rnorm(1, 0, 100))

  # One group has only a moderate amount of NA's
  SAMP1 <- rnorm(N1)
  SAMP1[sample(N1, .25 * N1, FALSE)] <- NA  # add in NA's

  # Another group has many NA's
  SAMP2 <- rnorm(N2)
  SAMP2[sample(N2, .95 * N2, FALSE)] <- NA  # add in large number of NA's

  # put them all in a list
  SAMP.NEW <- list(SAMP1, SAMP2)

  # keep it clean
  rm(SAMP1, SAMP2)

# Execute 
# -------    
  lapply(SAMP.NEW, meanIfThresh)

Исходные образцы данных, функции и т. д.

# Sample Data 
# ------------
  set.seed(1)
  rows <- 20000  # actual data has more than 7M rows
  cols <-  1000  

  SAMP <- replicate(cols, rnorm(rows))
  SAMP[sample(length(SAMP), .25 * length(SAMP), FALSE)] <- NA  # add in NA's

  # Select 5 random rows, and have them be 90% NA
  tooSparse <- sample(rows, 5)
  for (r in tooSparse)
    SAMP[r, sample(cols, cols * .9, FALSE)] <- NA

# Function 
# ------------
    meanIfThresh <- function(vec, thresh=12/15) { 
      # Calculates the mean of vec, however, 
      #   if the number of non-NA values of vec is less than thresh, returns NA 

      # thresh : represents how much data must be PRSENT. 
      #          ie, if thresh is 80%, then there must be at least 


      len <- length(vec)

      if( (sum(is.na(vec)) / len) > thresh)
        return(NA_real_)
      # if the proportion of NA's is greater than the threshold, return NA
      # example:  if I'm looking at 14 days, and I have 12 NA's,
      #            my proportion is 85.7 % = (12 / 14)
      #            default thesh is  80.0 % = (12 / 15)
      #            Thus, 12 NAs in a group of 14 would be rejected


    # else, calculate the mean, removing NA's
    return(mean(vec, na.rm=TRUE))       
  }


  # Execute
  # -----------------
  apply(SAMP, 1, meanIfThresh)

  # Compare with `mean`
  #----------------
  plain    <- apply(SAMP, 1, mean, na.rm=TRUE)
  modified <- apply(SAMP, 1, meanIfThresh)

  # obviously different
  identical(plain, modified)
  plain[tooSparse]
  modified[tooSparse]


  microbenchmark( "meanIfThresh"   = apply(SAMP, 1, meanIfThresh)
                , "mean (regular)" = apply(SAMP, 1, mean, na.rm=TRUE)
                , times = 15L)

 #  With the actual data, the penalty is sevenfold
 #  Unit: seconds
 #           expr      min       lq   median       uq      max neval
 #   meanIfThresh 1.658600 1.677472 1.690460 1.751913 2.110871    15
 # mean (regular) 1.422478 1.485320 1.503468 1.532175 1.547450    15

person Ricardo Saporta    schedule 13.06.2013    source источник
comment
Эта строка SAMP[sample(SAMP, .25 * length(SAMP), FALSE)] <- NA выдает мне ошибку в R3.0.1 only 0's may be mixed with negative subscripts   -  person thelatemail    schedule 13.06.2013
comment
Может, вместо этого SAMP[sample(seq_along(SAMP), .25 * length(SAMP))] <- NA?   -  person thelatemail    schedule 13.06.2013
comment
спасибо, это должно было быть SAMP[sample(length(SAMP), .25 * length(SAMP), FALSE)] <- NA (исправлено в редактировании)   -  person Ricardo Saporta    schedule 13.06.2013


Ответы (2)


Не могли бы вы просто заменить средние значения строк с высоким значением NA после этого?:

# changed `result <- apply(SAMP,1,mean,na.rm=TRUE)`
result <- rowMeans(SAMP, na.rm=TRUE)
NArows <- rowSums(is.na(SAMP))/ncol(SAMP) > 0.8
result[NArows] <- NA

Некоторый бенчмаркинг:

Ricardo <- function(vec, thresh=12/15) {
    len <- length(vec)
    if( (sum(is.na(vec)) / len) > thresh)
        return(NA_real_)
    return(mean(vec, na.rm=TRUE))       
}

DanielFischer <- function(vec, thresh=12/15) {

    len <- length(vec)
    nas <- is.na(vec)
    Nna <- sum(nas)
    if( (Nna / len) > thresh)
        return(NA_real_)
    return(sum(vec[!nas])/(len-Nna))
}

thelatemail <- function(mat) {
    result <- rowMeans(mat, na.rm=TRUE)
    NArows <- rowSums(is.na(mat))/ncol(mat) > 0.8
    result[NArows] <- NA
    result
}

require(microbenchmark)
microbenchmark(m1 <- apply(SAMP, 1, Ricardo), 
               m2 <- apply(SAMP, 1, DanielFischer), 
               m3 <- thelatemail(SAMP), times = 5L)

Unit: milliseconds
                                expr       min        lq    median        uq       max neval
       m1 <- apply(SAMP, 1, Ricardo) 2923.7260 2944.2599 3066.8204 3090.8127 3105.4283     5
 m2 <- apply(SAMP, 1, DanielFischer) 2643.4883 2683.1034 2755.7032 2799.5155 3089.6015     5
                m3 <- latemail(SAMP)  337.1862  340.6339  371.6148  376.5517  383.4436     5

all.equal(m1, m2) # TRUE
all.equal(m1, m3) # TRUE
person thelatemail    schedule 13.06.2013
comment
Точно. @RicardoSaporta, ты звонишь sum(is.na(.)) nrow раз. Вместо этого вы можете использовать векторизованный rowSums. Накладные расходы на вызов этих функций будут больше, чем сами вычисления. Вероятно, профилирование Rprof(); modified <- apply(SAMP, 1, meanIfThresh); Rprof(NULL); summaryRprof() могло бы помочь. - person Arun; 13.06.2013
comment
@latemail, вы должны изменить apply(SAMP, 1, mean, na.rm = TRUE) на rowMeans(SAMP, na.rm=TRUE) - person Arun; 13.06.2013
comment
@thelatemail Спасибо за это. Я должен был быть более точным в своем вопросе, поскольку я очень конкретно выполняю эту функцию несколько сотен раз, поскольку у меня есть несколько сотен групп, каждая из которых имеет разный размер. (Возможно, путаница - это моя вина за чрезмерное упрощение выборочных данных.) Однако ... это действительно мотивирует идею плавления данных по группам и применения вашего метода таким образом. - person Ricardo Saporta; 13.06.2013

Это так, что вам нужно дважды пройти через ваш вектор vec в вашей функции? Если вы можете сначала сохранить NA, возможно, это немного ускорит ваши вычисления:

meanIfThresh2 <- function(vec, thresh=12/15) { 

  len <- length(vec)
  nas <- is.na(vec)
  Nna <- sum(nas)
  if( (Nna / len) > thresh)
    return(NA_real_)

  return(sum(vec[!nas])/(len-Nna))
}

РЕДАКТИРОВАТЬ: я выполнил аналогичный бенчмаркинг, чтобы увидеть эффект от этого изменения:

> microbenchmark(  "meanIfThresh"   = apply(SAMP, 1, meanIfThresh)
+                 , "meanIfThresh2"   = apply(SAMP, 1, meanIfThresh2)
+                 , "mean (regular)" = apply(SAMP, 1, mean, na.rm=TRUE)
+                 , times = 15L)
Unit: seconds
           expr      min       lq   median       uq      max neval
   meanIfThresh 2.009858 2.156104 2.158372 2.166092 2.192493    15
  meanIfThresh2 1.825470 1.828273 1.829424 1.834407 1.872028    15
 mean (regular) 1.868568 1.882526 1.889852 1.893564 1.907495    15
person Daniel Fischer    schedule 13.06.2013
comment
Вы должны сделать тот же бенчмаркинг. Я не думаю, что это что-то изменит. @thelatemail правильно понял, имхо. - person Arun; 13.06.2013
comment
Хорошо, я добавил тест в свой пост - и кажется, что сохранение NA увеличивает скорость. По крайней мере, кажется, что она работает так же быстро, как и обычная средняя функция. - person Daniel Fischer; 13.06.2013
comment
Я должен был сказать, что это мало что изменит. Проверьте редактирование в ответе поздней почты. - person Arun; 13.06.2013
comment
Да, ваше предложение использовать rowMeans внесло огромные изменения! Но до этого я думал, что решение должно быть медленнее, так как сначала выполнялось обычное среднее, а затем снова is.na шага. Я не знал, что rowMeans намного быстрее... - person Daniel Fischer; 13.06.2013
comment
@ Даниэль, это, безусловно, помогает! Это уменьшает дополнительные накладные расходы, связанные с вызовом mean. - person Ricardo Saporta; 13.06.2013