Какова более эффективная альтернатива циклам for для подстановки данных по идентификатору группы? - PullRequest
7 голосов
/ 27 марта 2010

Парадигма повторяющегося анализа, с которой я сталкиваюсь в своем исследовании, заключается в необходимости поднабора на основе всех различных значений идентификатора группы, выполнения статистического анализа каждой группы по очереди и помещения результатов в выходную матрицу для дальнейшей обработки / суммирования.

Как я обычно делаю это в R, примерно так:

data.mat <- read.csv("...")  
groupids <- unique(data.mat$ID)  #Assume there are then 100 unique groups

results <- matrix(rep("NA",300),ncol=3,nrow=100)  

for(i in 1:100) {  
  tempmat <- subset(data.mat,ID==groupids[i])  

  # Run various stats on tempmat (correlations, regressions, etc), checking to  
  # make sure this specific group doesn't have NAs in the variables I'm using  
  # and assign results to x, y, and z, for example.  

  results[i,1] <- x  
  results[i,2] <- y  
  results[i,3] <- z  
}

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

Помимо перехода к параллельной обработке, есть ли какой-нибудь "прием" для ускорения выполнения чего-то подобного? Например, преобразование циклов во что-то другое (что-то вроде применения с функцией, содержащей статистику, которую я хочу запустить внутри цикла), или устранение необходимости фактически назначать подмножество данных переменной?

Edit:

Может быть, это просто общеизвестно (или ошибка выборки), но я попытался заключить в некоторые части моего кода подстановки в скобки, а не использовать команду подмножества, и это, казалось, дало небольшой прирост производительности, что меня удивило. У меня есть некоторый код, который я использовал и выводил ниже, используя те же имена объектов, что и выше:

system.time(for(i in 1:1000){data.mat[data.mat$ID==groupids[i],]})  
   user  system elapsed  
 361.41   92.62  458.32
system.time(for(i in 1:1000){subset(data.mat,ID==groupids[i])})  
   user  system elapsed   
 378.44  102.03  485.94

Обновление:

В одном из ответов jorgusch предложил использовать пакет data.table для ускорения поднабора. Итак, я применил это к проблеме, с которой столкнулся ранее на этой неделе. В наборе данных с более чем 1 500 000 строк и 4 столбцами (ID, Var1, Var2, Var3) я хотел вычислить две корреляции в каждой группе (индексируемой переменной «ID»). Есть чуть более 50000 групп. Ниже мой исходный код (который очень похож на выше):

data.mat <- read.csv("//home....")  
groupids <- unique(data.mat$ID)

results <- matrix(rep("NA",(length(groupids) * 3)),ncol=3,nrow=length(groupids))  

for(i in 1:length(groupids)) {  
  tempmat <- data.mat[data.mat$ID==groupids[i],] 

  results[i,1] <- groupids[i]  
  results[i,2] <- cor(tempmat$Var1,tempmat$Var2,use="pairwise.complete.obs")  
  results[i,3] <- cor(tempmat$Var1,tempmat$Var3,use="pairwise.complete.obs")    

}  

Я повторяю это прямо сейчас, чтобы точно определить, сколько времени это заняло, но из того, что я помню, я запустил его, когда пришел в офис утром, и он закончился где-то в середине дня. Рисунок 5-7 часов.

Перестройка моего кода для использования data.table ....

data.mat <- read.csv("//home....")  
data.mat <- data.table(data.mat)  

testfunc <- function(x,y,z) {  
  temp1 <- cor(x,y,use="pairwise.complete.obs")  
  temp2 <- cor(x,z,use="pairwise.complete.obs")  
  res <- list(temp1,temp2)  
  res  
}  

system.time(test <- data.mat[,testfunc(Var1,Var2,Var3),by="ID"])  
 user  system  elapsed  
16.41    0.05    17.44  

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

Обновление 2:

Запуск кода с использованием подмножеств наконец завершился снова:

   user     system   elapsed  
17575.79  4247.41   23477.00

Обновление 3:

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

Использование тех же переменных и настроек, что и раньше ...

data.mat <- read.csv("//home....")  
system.time(hmm <- ddply(data.mat,"ID",function(df)c(cor(df$Var1,df$Var2,  use="pairwise.complete.obs"),cor(df$Var1,df$Var3,use="pairwise.complete.obs"))))  
  user  system elapsed  
250.25    7.35  272.09  

Ответы [ 4 ]

6 голосов
/ 27 марта 2010

Это почти то же, что и пакет plyr, разработанный для облегчения работы.Однако маловероятно, что это сделает все намного быстрее - большую часть времени, вероятно, тратят на статистику.

3 голосов
/ 27 марта 2010

Помимо plyr, вы можете попробовать использовать пакет foreach, чтобы исключить явный счетчик цикла, но я не знаю, даст ли он какие-либо преимущества в производительности.

Foreach, тем не менее, дает вам довольно простой интерфейс для параллельной обработки чанка, если у вас многоядерная рабочая станция (с пакетами doMC / multicore) (проверьте Начало работы с doMC и foreach для детали), если исключить параллельную обработку только потому, что ее не очень легко понять студентам. Если это не единственная причина, plyr очень хорошее решение ИМХО.

2 голосов
/ 27 марта 2010

Лично я считаю, что plyr не очень легко понять. Я предпочитаю data.table, который также быстрее. Например, вы хотите сделать стандартное отклонение столбца my_column для каждого идентификатора.

dt <- datab.table[df] # one time operation...changing format of df to table
result.sd <- dt[,sd(my_column),by="ID"] # result with each ID and SD in second column 

Три утверждения такого рода и cbind в конце - это все, что вам нужно. Вы также можете использовать dt do some action только для одного идентификатора без команды подмножества в новом синтаксисе:

result.sd.oneiD<- dt[ID="oneID",sd(my_column)]  

Первый комментарий относится к строкам (i), второй к столбцам (j).

Если вам легче читать, то проигрыватель и он более гибкий, так как вы также можете создавать поддомен в пределах «подмножества» ... Документация описывает, что он использует SQL-подобные методы. Например, by ​​в значительной степени «сгруппирован» в SQL. Хорошо, если вы знаете SQL, вы, вероятно, можете сделать гораздо больше, но использовать пакет не обязательно. Наконец, это чрезвычайно быстро, так как каждая операция не только параллельная, но и data.table собирает данные, необходимые для расчета. Подмножество, однако, поддерживает уровни всей матрицы и перетаскивает ее через память.

2 голосов
/ 27 марта 2010

Вы уже предложили векторизовать и избегать создания ненужных копий промежуточных результатов, поэтому вы, безусловно, на правильном пути. Позвольте мне предостеречь вас не делать то, что я сделал, и просто предположить , что векторизация всегда даст вам повышение производительности (как это происходит в других языках, например Python + NumPy, MATLAB) .

Пример:

# small function to time the results:
time_this = function(...) {
  start.time = Sys.time(); eval(..., sys.frame(sys.parent(sys.parent()))); 
  end.time = Sys.time(); print(end.time - start.time)
}

# data for testing: a 10000 x 1000 matrix of random doubles
a = matrix(rnorm(1e7, mean=5, sd=2), nrow=10000)

# two versions doing the same thing: calculating the mean for each row
# in the matrix
x = time_this( for (i in 1:nrow(a)){ mean( a[i,] ) } )
y = time_this( apply(X=a, MARGIN=1, FUN=mean) )

print(x)    # returns => 0.5312099
print(y)    # returns => 0.661242

Версия 'apply' на самом деле медленнее , чем версия 'for'. (По словам автора Inferno , если вы делаете это, вы не векторизуете, вы «скрываете петли».)

Но вы можете повысить производительность, используя встроенные модули . Ниже я рассчитал ту же операцию, что и две выше, просто используя встроенную функцию 'rowMeans':

z = time_this(rowMeans(a))
print(z)    # returns => 0.03679609

Улучшение на порядок по сравнению с циклом 'for' (и векторизованной версией).

Другие члены семейства заявок не являются просто обертками над нативным циклом for.

a = abs(floor(10*rnorm(1e6)))

time_this(sapply(a, sqrt))
# returns => 6.64 secs

time_this(for (i in 1:length(a)){ sqrt(a[i])})
# returns => 1.33 secs

'sapply' примерно на 5x медленнее по сравнению с циклом 'for'.

Наконец, в / в / т векторизовано по сравнению с циклами 'for', я не думаю, что когда-либо использую цикл, если я могу использовать векторизованную функцию - последний обычно меньше нажатий клавиш, и это более естественный способ ( для меня), чтобы написать код, который является другим видом повышения производительности, я полагаю.

...