R'de döngü işlemini hızlandırın


193

R'de büyük bir performans sorunum var data.frame. Bir nesne üzerinde yinelenen bir fonksiyon yazdım . Sadece a'ya yeni bir sütun ekler ve bir data.frameşeyler biriktirir. (basit kullanım). data.frameKabaca 850K satır var. Bilgisayarım hala çalışıyor (yaklaşık 10 saat) ve çalışma zamanı hakkında hiçbir fikrim yok.

dayloop2 <- function(temp){
    for (i in 1:nrow(temp)){    
        temp[i,10] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                temp[i,10] <- temp[i,9] + temp[i-1,10]                    
            } else {
                temp[i,10] <- temp[i,9]                                    
            }
        } else {
            temp[i,10] <- temp[i,9]
        }
    }
    names(temp)[names(temp) == "V10"] <- "Kumm."
    return(temp)
}

Bu operasyonu nasıl hızlandıracağına dair bir fikrin var mı?

Yanıtlar:


435

En büyük sorun ve verimsizliğin kökü data.frame'i endekslemektir, yani kullandığınız tüm bu satırlar temp[,].
Bundan mümkün olduğunca kaçınmaya çalışın. İşlevinizi aldım, indekslemeyi değiştirdim ve burada version_A

dayloop2_A <- function(temp){
    res <- numeric(nrow(temp))
    for (i in 1:nrow(temp)){    
        res[i] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                res[i] <- temp[i,9] + res[i-1]                   
            } else {
                res[i] <- temp[i,9]                                    
            }
        } else {
            res[i] <- temp[i,9]
        }
    }
    temp$`Kumm.` <- res
    return(temp)
}

Gördüğünüz gibi ressonuçları toplayan vektör oluşturuyorum. Sonunda ekliyorum data.frameve isimleri karıştırmam gerekmiyor. Peki ne kadar iyi?

Her işlevi 1.000 ila 10.000 data.frameile nrow1.000 için çalıştırıyorum vesystem.time

X <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
system.time(dayloop2(X))

Sonuç:

verim

Sürümünüzün katlanarak bağlı olduğunu görebilirsiniz nrow(X). Modifiye edilmiş versiyonun doğrusal bir ilişkisi vardır ve basit lmmodel 850.000 satırlık hesaplamanın 6 dakika 10 saniye sürdüğünü tahmin eder.

Vektörleştirmenin gücü

Shane ve Calimo'nun cevaplarında belirttiği gibi, vektörleşme daha iyi performans için bir anahtardır. Kodunuzdan döngü dışında hareket edebilirsiniz:

  • şartlandırma
  • sonuçların başlatılması (ki bunlar temp[i,9])

Bu, bu koda götürür

dayloop2_B <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in 1:nrow(temp)) {
        if (cond[i]) res[i] <- temp[i,9] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

Bu işlevlerin sonuçlarını karşılaştırın, bu sefer nrow10.000'den 100.000'e 10.000'e kadar.

verim

Ayarlanan ayarlama

Başka bir çimdik bir döngü indeksleme değişen olduğu temp[i,9]için res[i](tam i-inci döngünün aynı olan). Yine bir vektörü indekslemek ve a indekslemek arasındaki farktır data.frame.
İkinci şey: döngüye baktığınızda, her şeyin üzerinde döngü yapmanın gerekmediğini görebilirsiniz i, ancak sadece koşula uyanlar için.
İşte başlıyoruz

dayloop2_D <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in (1:nrow(temp))[cond]) {
        res[i] <- res[i] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

Büyük ölçüde kazandığınız performans bir veri yapısına bağlıdır. Kesinlikle - koşuldaki TRUEdeğerlerin yüzdesi üzerinde . Simüle edilmiş verilerim için bir saniyenin altındaki 850.000 satır için hesaplama süresi gerekir.

verim

Daha ileri gidebilmeni istiyorum, yapılabilecek en az iki şey görüyorum:

  • Ckoşullu cumsum yapmak için bir kod yazın
  • veri maks dizinizde büyük olmadığını biliyorsanız, döngüyü vectorized olarak değiştirebilirsiniz.

    while (any(cond)) {
        indx <- c(FALSE, cond[-1] & !cond[-n])
        res[indx] <- res[indx] + res[which(indx)-1]
        cond[indx] <- FALSE
    }

Simülasyonlar ve şekiller için kullanılan kod GitHub'da mevcuttur .


2
Marek'e özel olarak sormanın bir yolunu bulamadığım için, bu grafikler nasıl oluşturuldu?
carbontwelve

@carbontwelve Veri mi yoksa grafik mi soruyorsunuz? Arsalar kafes paketi ile yapılmıştır. Zamanım varsa kodu web'de bir yere koyar ve size haber veririm.
Marek

@carbontwelve Ooops, yanılmışım :) Bu standart araziler (R tabanından).
Marek

@Gregor Maalesef değil. Kümülatiftir, böylece vektörleştiremezsiniz. Basit bir örnek: res = c(1,2,3,4)ve condtüm TRUEardından nihai sonuç olmalıdır: 1, 3(neden 1+2), 6(neden ikinci şimdi 3, ve üçüncü 3de), 10( 6+4). Elindeki basit toplamı yaparak 1, 3, 5, 7.
Marek

Ah, daha dikkatli düşünmeliydim. Bana hatayı gösterdiğin için teşekkürler.
Gregor Thomas

132

R kodunu hızlandırmak için genel stratejiler

İlk olarak, yavaş parçanın gerçekte nerede olduğunu bulun. Yavaş çalışmayan kodu optimize etmeye gerek yoktur. Küçük miktarlarda kod için, sadece düşünmek işe yarayabilir. Bu başarısız olursa, RProf ve benzeri profil oluşturma araçları yardımcı olabilir.

Darboğazı çözdükten sonra, istediğiniz şeyi yapmak için daha verimli algoritmalar düşünün . Hesaplamalar mümkünse yalnızca bir kez yapılmalıdır, bu nedenle:

  • Sonuçları saklayın ve tekrar tekrar hesaplama yapmak yerine sonuçlara erişin
  • Döngü bağımlı olmayan hesaplamaları döngülerden çıkarın
  • Gerekli olmayan hesaplamalardan kaçının (örneğin , sabit aramalarda normal ifadeler kullanmayın )

Daha verimli işlevlerin kullanılması orta veya büyük hız kazanımları sağlayabilir. Örneğin, paste0küçük bir verimlilik kazancı üretir , ancak .colSums()akrabaları bir şekilde daha belirgin kazanımlar üretir. meanbir özellikle yavaş .

O zaman özellikle bazı yaygın sorunlardan kaçınabilirsiniz :

  • cbind seni çok çabuk yavaşlatacak.
  • Veri yapılarınızı başlatın, ardından her seferinde genişletmek yerine doldurun .
  • Ön ayırmayla bile, değer bazında bir yaklaşımdan ziyade bir referans yoluyla yaklaşıma geçebilirsiniz, ancak bu güçlüklere değmeyebilir.
  • Kaçınılması gereken daha fazla tuzak için R Inferno'ya bir göz atın .

Sıklıkla ancak her zaman yardımcı olmayan daha iyi vektörizasyon deneyin . Bu bağlamda, doğal olarak vectorized komutlar gibi ifelse, diffve benzeri daha bir iyileşme sağlayacaktır apply(iyi yazılmış döngü üzerinde hiçbir hız artışı çok az sağlamak) komutların ailesi.

Ayrıca R işlevlerine daha fazla bilgi sağlamayı deneyebilirsiniz . Örneğin, vapplyyerinesapply kullanın ve colClassesmetin tabanlı verilerde okurken belirtin . Hız kazançları, ne kadar tahminde bulunduğunuza bağlı olarak değişecektir.

Daha sonra, optimize edilmiş paketleri düşünün : data.tablePaket, kullanımının mümkün olduğu yerlerde, veri manipülasyonunda ve büyük miktarda veri okurken ( fread) büyük devir kazançları üretebilir .

Daha sonra, R'yi daha verimli bir şekilde aramanın hız kazanımlarını deneyin :

  • R betiğinizi derleyin. Ya da Rave jitpaketlerini tam zamanında derleme için birlikte kullanın (Dirk bu sunumda bir örneğe sahiptir ).
  • Optimize edilmiş bir BLAS kullandığınızdan emin olun. Bunlar, genel hız artışları sağlar. Dürüst olmak gerekirse, R'nin yükleme sırasında en verimli kütüphaneyi otomatik olarak kullanmaması utanç verici. İnşallah Devrim R, burada yaptıkları işe tüm topluma katkıda bulunacaktır.
  • Radford Neal, bazıları R Core'a ve bazıları da pqR'ye yerleştirilmiş olan bir dizi optimizasyon yaptı .

Ve son olarak, yukarıdakilerin hepsi hala ihtiyacınız olduğu kadar hızlı almazsa , yavaş kod snippet'i için daha hızlı bir dile geçmeniz gerekebilir . Buradaki kombinasyon Rcppve inlinealgoritma C ++ koduyla algoritmanın sadece en yavaş kısmını değiştirmeyi özellikle kolaylaştırır. Örneğin, benim ilk denemem budur ve son derece optimize edilmiş R çözümlerini bile uçurur.

Tüm bunlardan sonra hala sıkıntılar yaşıyorsanız, daha fazla bilgi işlem gücüne ihtiyacınız var. İçine bak paralelleştirme ( http://cran.r-project.org/web/views/HighPerformanceComputing.html ) ve hatta GPU tabanlı çözümler ( gpu-tools).

Diğer rehberliğe bağlantılar


36

forDöngüler kullanıyorsanız , büyük olasılıkla R'yi C veya Java ya da başka bir şeymiş gibi kodlamış olursunuz. Düzgün vektörize edilmiş R kodu son derece hızlıdır.

Örneğin, sırasıyla 10.000 tamsayının bir listesini oluşturmak için bu iki basit kod parçasını ele alalım:

İlk kod örneği, geleneksel bir kodlama paradigmasını kullanarak bir döngüyü nasıl kodlayacağıdır. Tamamlanması 28 saniye sürer

system.time({
    a <- NULL
    for(i in 1:1e5)a[i] <- i
})
   user  system elapsed 
  28.36    0.07   28.61 

Belleği önceden ayırmak gibi basit bir işlemle neredeyse 100 kat iyileşme elde edebilirsiniz:

system.time({
    a <- rep(1, 1e5)
    for(i in 1:1e5)a[i] <- i
})

   user  system elapsed 
   0.30    0.00    0.29 

Ancak, kolon operatörünü kullanarak temel R vektör işlemini kullanarak :bu işlem neredeyse anlıktır:

system.time(a <- 1:1e5)

   user  system elapsed 
      0       0       0 

+1 olsa da, ikinci örneğinizi a[i]değişmediği için ikna edici görmüyorum. Ancak system.time({a <- NULL; for(i in 1:1e5){a[i] <- 2*i} }); system.time({a <- 1:1e5; for(i in 1:1e5){a[i] <- 2*i} }); system.time({a <- NULL; a <- 2*(1:1e5)})benzer bir sonucu var.
Henry

@Henry, adil yorum, ama işaret ettiğiniz gibi, sonuçlar aynı. Ben bir rep(1, 1e5)- zamanlamaları aynı başlatmak için örnek değiştirdik .
Andrie

17

Dizinler veya iç içe ifelse()ifadeler kullanılarak döngüler atlanarak bu çok daha hızlı yapılabilir .

idx <- 1:nrow(temp)
temp[,10] <- idx
idx1 <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
temp[idx1,10] <- temp[idx1,9] + temp[which(idx1)-1,10] 
temp[!idx1,10] <- temp[!idx1,9]    
temp[1,10] <- temp[1,9]
names(temp)[names(temp) == "V10"] <- "Kumm."

Cevap için teşekkürler. İfadelerinizi anlamaya çalışıyorum. 4. satır: "temp [idx1,10] <- temp [idx1,9] + temp [ki (idx1) -1,10]" bir hataya neden oldu, çünkü daha uzun nesnenin uzunluğu, daha kısa nesne. "temp [idx1,9] = num [1: 11496]" ve "temp [ki (idx1) -1,10] = int [1: 11494]" "2 satır eksik.
Kay

Bir veri örneği sağlarsanız (birkaç satırla dput () kullanın) sonra sizin için düzeltirim. Hangi () - 1 bit nedeniyle, dizinler eşit değildir. Ancak buradan nasıl çalıştığını görmelisiniz: herhangi bir döngüye veya uygulamaya gerek yoktur; sadece vectorized fonksiyonları kullanın.
Shane

1
Vaov! Ben sadece bir iç içe if..else işlev bloğu ve mapply, iç içe bir ifelse işlevini değiştirdim ve 200x hızlanma var!
James

Genel tavsiyeniz doğrudur, ancak kodda gerçeği kaçırdınız, bu i-th değeri -th'e bağlıdır, i-1bu nedenle bunu yaptığınız şekilde ayarlanamazlar (kullanarak which()-1).
Marek

8

Kod yazmayı sevmiyorum ... Ayrıca elbette ifelse ve lapply daha iyi seçeneklerdir, ancak bazen bu uyumu zorlaştırır.

Ben sık sık biri gibi listeleri kullanır gibi data.frames kullanın df$var[i]

İşte bir örnek:

nrow=function(x){ ##required as I use nrow at times.
  if(class(x)=='list') {
    length(x[[names(x)[1]]])
  }else{
    base::nrow(x)
  }
}

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
})

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  d=as.list(d) #become a list
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
  d=as.data.frame(d) #revert back to data.frame
})

data.frame sürümü:

   user  system elapsed 
   0.53    0.00    0.53

liste sürümü:

   user  system elapsed 
   0.04    0.00    0.03 

Bir vektör listesini kullanmak için bir veri çerçevesinden 17 kat daha hızlı.

Dahili veri.frames neden bu konuda bu kadar yavaş hakkında herhangi bir yorum? Bunların liste gibi çalıştığını düşünürdüm ...

Daha hızlı kod için bunu class(d)='list'yerine d=as.list(d)veclass(d)='data.frame'

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  class(d)='list'
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
  class(d)='data.frame'
})
head(d)

1
Muhtemelen ek yükü sayesinde [<-.data.frame, bir şekilde yaptığınızda denir d$foo[i] = markve muhtemelen tüm <-değişikliklerin vektörünün yeni bir kopyasını oluşturabilir. Her modifikasyonda çerçeve . SO hakkında ilginç bir soru olurdu.
Frank

2
@Frank (i) değiştirilmiş nesnenin hala geçerli bir veri çerçevesi olduğundan emin olmalıdır. (İi) afaik en az bir kopya, muhtemelen birden fazla kopya oluşturur. Veri çerçevesi alt atamasının yavaş olduğu bilinir ve uzun kaynak koduna bakarsanız gerçekten şaşırtıcı değildir.
Roland

@Frank, @Roland: df$var[i]Gösterim aynı [<-.data.frameişlevden mi geçiyor ? Gerçekten çok uzun olduğunu fark ettim. Değilse, hangi işlevi kullanır?
Chris

@Chris d$foo[i]=markKabaca çevrildiğine inanıyorum d <- `$<-`(d, 'foo', `[<-`(d$foo, i, mark)), ancak bazı geçici değişkenler kullanıyorum.
Tim Goodman

7

Ari'nin cevabının sonunda belirttiği gibi, Rcppve inlinepaketleri işleri hızlı hale getirmeyi inanılmaz derecede kolaylaştırıyor. Örnek olarak, bu inlinekodu deneyin (uyarı: test edilmedi):

body <- 'Rcpp::NumericMatrix nm(temp);
         int nrtemp = Rccp::as<int>(nrt);
         for (int i = 0; i < nrtemp; ++i) {
             temp(i, 9) = i
             if (i > 1) {
                 if ((temp(i, 5) == temp(i - 1, 5) && temp(i, 2) == temp(i - 1, 2) {
                     temp(i, 9) = temp(i, 8) + temp(i - 1, 9)
                 } else {
                     temp(i, 9) = temp(i, 8)
                 }
             } else {
                 temp(i, 9) = temp(i, 8)
             }
         return Rcpp::wrap(nm);
        '

settings <- getPlugin("Rcpp")
# settings$env$PKG_CXXFLAGS <- paste("-I", getwd(), sep="") if you want to inc files in wd
dayloop <- cxxfunction(signature(nrt="numeric", temp="numeric"), body-body,
    plugin="Rcpp", settings=settings, cppargs="-I/usr/include")

dayloop2 <- function(temp) {
    # extract a numeric matrix from temp, put it in tmp
    nc <- ncol(temp)
    nm <- dayloop(nc, temp)
    names(temp)[names(temp) == "V10"] <- "Kumm."
    return(temp)
}

#includeBir parametreyi geçirdiğiniz şeyleri ing için benzer bir prosedür vardır.

inc <- '#include <header.h>

cxxfunction işlevine include=inc . Bu konuda gerçekten güzel olan şey, sizin için tüm bağlantı ve derlemeleri yapmasıdır, bu nedenle prototipleme gerçekten hızlıdır.

Yasal Uyarı: tmp sınıfının sayısal olması ve sayısal matris ya da başka bir şey olması gerektiğinden tam olarak emin değilim. Ama çoğunlukla eminim.

Düzenleme: bundan sonra hala daha fazla hıza ihtiyacınız varsa, OpenMP iyi bir paralelleştirme tesisidir C++. Kullanmayı denemedim inline, ama işe yaramalı. Fikir, nçekirdekler söz konusu olduğunda, döngü yinelemesinin kgerçekleştirilmesi olacaktır k % n. Matloff bulunan Uygun bir giriş var R Programlama Sanatı mevcuttur, burada , Bölüm 16'da, C başvurmak .


3

Buradaki cevaplar harika. Kapsanmayan küçük bir husus, " Bilgisayarım hala çalışıyor (yaklaşık 10 saat) ve çalışma zamanı hakkında hiçbir fikrim yok " sorusunun belirtilmesi . Değişikliklerin hızı nasıl etkilediğine dair bir fikir edinmek ve ayrıca ne kadar sürede tamamlanacağını izlemek için gelişirken her zaman aşağıdaki kodu döngülere koydum.

dayloop2 <- function(temp){
  for (i in 1:nrow(temp)){
    cat(round(i/nrow(temp)*100,2),"%    \r") # prints the percentage complete in realtime.
    # do stuff
  }
  return(blah)
}

Lapon ile de çalışır.

dayloop2 <- function(temp){
  temp <- lapply(1:nrow(temp), function(i) {
    cat(round(i/nrow(temp)*100,2),"%    \r")
    #do stuff
  })
  return(temp)
}

Döngü içindeki işlev oldukça hızlı, ancak döngü sayısı büyükse, konsolun kendisine yazdırmanın bir ek yükü olduğu için sadece sık sık yazdırmayı düşünün. Örneğin

dayloop2 <- function(temp){
  for (i in 1:nrow(temp)){
    if(i %% 100 == 0) cat(round(i/nrow(temp)*100,2),"%    \r") # prints every 100 times through the loop
    # do stuff
  }
  return(temp)
}

Benzer bir seçenek, kesir i / n'yi yazdırın. Her zaman böyle bir şey var cat(sprintf("\nNow running... %40s, %s/%s \n", nm[i], i, n))çünkü genellikle adlandırılmış şeyler (isimler ile nm) üzerinde döngü .
Frank

2

R'de, genellikle applyaile işlevlerini kullanarak döngü işlemeyi hızlandırabilirsiniz (sizin durumunuzda olabilir replicate). plyrİlerleme çubukları sağlayan pakete bir göz atın .

Başka bir seçenek, döngülerden tamamen kaçınmak ve bunları vektörize aritmetiklerle değiştirmek. Tam olarak ne yaptığınızdan emin değilim, ancak işlevinizi bir kerede tüm satırlara uygulayabilirsiniz:

temp[1:nrow(temp), 10] <- temp[1:nrow(temp), 9] + temp[0:(nrow(temp)-1), 10]

Bu çok daha hızlı olacak ve daha sonra satırları durumunuza göre filtreleyebilirsiniz:

cond.i <- (temp[i, 6] == temp[i-1, 6]) & (temp[i, 3] == temp[i-1, 3])
temp[cond.i, 10] <- temp[cond.i, 9]

Vektörize aritmetik, daha fazla zaman ve sorun hakkında düşünmeyi gerektirir, ancak daha sonra bazen yürütme zamanında birkaç büyüklük sırası kaydedebilirsiniz.


14
vektör işlevlerinin döngülerden daha hızlı olacağını ya da uygula () yöntemini uygulayacağınızı ancak uygula () yönteminin döngülerden daha hızlı olduğu doğru değildir. Birçok durumda Apply () basitçe döngü kullanıcıdan soyutlama ama yine de döngü. Bu önceki soruya bakın: stackoverflow.com/questions/2275896/…
JD Long

0

İle işlem yapmak data.tableuygun bir seçenektir:

n <- 1000000
df <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
colnames(df) <- paste("col", 1:9, sep = "")

library(data.table)

dayloop2.dt <- function(df) {
  dt <- data.table(df)
  dt[, Kumm. := {
    res <- .I;
    ifelse (res > 1,             
      ifelse ((col6 == shift(col6, fill = 0)) & (col3 == shift(col3, fill = 0)) , 
        res <- col9 + shift(res)                   
      , # else
        res <- col9                                 
      )
     , # else
      res <- col9
    )
  }
  ,]
  res <- data.frame(dt)
  return (res)
}

res <- dayloop2.dt(df)

m <- microbenchmark(dayloop2.dt(df), times = 100)
#Unit: milliseconds
#       expr      min        lq     mean   median       uq      max neval
#dayloop2.dt(df) 436.4467 441.02076 578.7126 503.9874 575.9534 966.1042    10

Koşul filtrelemeden olası kazançları görmezden gelirseniz, çok hızlıdır. Açıkçası, eğer verilerin alt kümesinde hesaplamayı yapabiliyorsanız, yardımcı olur.


2
Data.table kullanma önerisini neden tekrarlıyorsunuz? Daha önceki cevaplarda zaten birçok kez yapılmıştır.
IRTFM
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.