r: для циклической работы с вложенными индексами работает очень медленно - PullRequest
15 голосов
/ 30 ноября 2011

У меня есть операция, которую я хотел бы запустить для каждой строки фрейма данных, меняя один столбец. Я человек apply / ddply / sqldf, но я буду использовать циклы, когда они имеют смысл, и я думаю, что это один из тех случаев. Этот случай сложен, потому что столбец для изменений зависит от информации, которая изменяется по строке; в зависимости от информации в одной ячейке, я должен внести изменения только в одну из десяти других ячеек в этой строке. С 75 столбцами и 20000 строками операция занимает 10 минут, тогда как любая другая операция в моем скрипте занимает 0-5 секунд, максимум десять секунд. Я сократил мою проблему до очень простого тестового примера ниже.

n <- 20000
t.df <- data.frame(matrix(1:5000, ncol=10, nrow=n) )
system.time(
 for (i in 1:nrow(t.df)) {
 t.df[i,(t.df[i,1]%%10 + 1)] <- 99
 }
)

Это занимает 70 секунд с десятью столбцами и 360 при ncol = 50. Это безумие. Являются ли петли неправильным подходом? Есть ли лучший, более эффективный способ сделать это?

Я уже пытался инициализировать вложенный термин (t.df [i, 1] %% 10 + 1) в виде списка вне цикла for. Это экономит около 30 секунд (из 10 минут), но делает приведенный выше пример более сложным. Так что это помогает, но это не решение.

Моя лучшая идея пришла при подготовке этого теста. Для меня только 10 столбцов актуальны (а 75-11 столбцов не имеют значения). Поскольку время выполнения в значительной степени зависит от количества столбцов, я могу просто выполнить вышеуказанную операцию для фрейма данных, который исключает ненужные столбцы. Это доведет меня до чуть более минуты. Но является ли «цикл с вложенными индексами» лучшим способом решения моей проблемы?

Ответы [ 5 ]

11 голосов
/ 30 ноября 2011

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

Кстати, отличный вопрос и очень хороший пример.

Вот иллюстрация того, насколько быстрее циклы на матрицах, чем на data.frames:

> n <- 20000
> t.df <- (matrix(1:5000, ncol=10, nrow=n) )
> system.time(
+   for (i in 1:nrow(t.df)) {
+     t.df[i,(t.df[i,1]%%10 + 1)] <- 99
+   }
+ )
   user  system elapsed 
  0.084   0.001   0.084 
> 
> n <- 20000
> t.df <- data.frame(matrix(1:5000, ncol=10, nrow=n) )
> system.time(
+   for (i in 1:nrow(t.df)) {
+     t.df[i,(t.df[i,1]%%10 + 1)] <- 99
+   }
+   )
   user  system elapsed 
 31.543  57.664  89.224 
7 голосов
/ 30 ноября 2011

ОБНОВЛЕНИЕ: добавлена ​​матричная версия решения Томми в упражнение по тестированию.

Вы можете векторизовать его. Вот мое решение и сравнение с циклом

n <- 20000
t.df <- (matrix(1:5000, ncol=10, nrow=n))

f_ramnath <- function(x){
  idx <- x[,1] %% 10 + 1
  x[cbind(1:NROW(x), idx)] <- 99  
  return(x)
}

f_long <- function(t.df){
  for (i in 1:nrow(t.df)) {
    t.df[i,(t.df[i,1]%%10 + 1)] <- 99
  }
  return(t.df)
}

f_joran <- function(t.df){
  t.df[col(t.df) == (row(t.df) %% 10) + 1]  <- 99
  return(t.df)
}

f_tommy <- function(t.df){
  t2.df <- t.df
  # Create a logical matrix with TRUE wherever the replacement should happen
  m <- array(FALSE, dim=dim(t2.df))
  m[cbind(seq_len(nrow(t2.df)), t2.df[,1]%%10L + 1L)] <- TRUE
  t2.df[m] <- 99
  return(t2.df)
}

f_tommy_mat <- function(m){
  m[cbind(seq_len(nrow(m)), m[,1]%%10L + 1L)] <- 99
}

Для сравнения производительности различных подходов мы можем использовать rbenchmark.

library(rbenchmark)
benchmark(f_long(t.df), f_ramnath(t.df), f_joran(t.df), f_tommy(t.df), 
  f_tommy_mat(t.df), replications = 20,  order = 'relative',
  columns = c('test', 'elapsed', 'relative')

               test elapsed  relative
5 f_tommy_mat(t.df)   0.135  1.000000
2   f_ramnath(t.df)   0.172  1.274074
4     f_tommy(t.df)   0.311  2.303704
3     f_joran(t.df)   0.705  5.222222
1      f_long(t.df)   2.411 17.859259
7 голосов
/ 30 ноября 2011

Использование row и col кажется мне менее сложным:

t.df[col(t.df) == (row(t.df) %% 10) + 1]  <- 99

Я думаю, что Томми все еще быстрее, но использование row и col может быть проще для понимания.

7 голосов
/ 30 ноября 2011

@ JD Long прав, что если t.df можно представить в виде матрицы, все будет гораздо быстрее.

... И тогда вы можете фактически векторизовать все это так, чтобы оно было молниеносно:

n <- 20000
t.df <- data.frame(matrix(1:5000, ncol=10, nrow=n) )
system.time({
  m <- as.matrix(t.df)
  m[cbind(seq_len(nrow(m)), m[,1]%%10L + 1L)] <- 99
  t2.df <- as.data.frame(m)
}) # 0.00 secs

К сожалению, матричное индексирование, которое я здесь использую, похоже, не работает на data.frame.

EDIT Вариант, в котором я создаю логическую матрицу для индексации, работает на data.frame и работает почти так же быстро:

n <- 20000
t.df <- data.frame(matrix(1:5000, ncol=10, nrow=n) )
system.time({
  t2.df <- t.df

  # Create a logical matrix with TRUE wherever the replacement should happen
  m <- array(FALSE, dim=dim(t2.df))
  m[cbind(seq_len(nrow(t2.df)), t2.df[,1]%%10L + 1L)] <- TRUE

  t2.df[m] <- 99
}) # 0.01 secs
1 голос
/ 01 декабря 2011

Другой вариант, когда вам нужны смешанные типы столбцов (и поэтому вы не можете использовать matrix), это := в data.table .Пример из ?":=":

require(data.table)
m = matrix(1,nrow=100000,ncol=100)
DF = as.data.frame(m)
DT = as.data.table(m)    
system.time(for (i in 1:1000) DF[i,1] <- i)
    # 591 seconds 
system.time(for (i in 1:1000) DT[i,V1:=i])
    # 1.16 seconds  ( 509 times faster )
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...