R: Vektorisasi loop untuk membuat matriks berpasangan

Saya ingin mempercepat fungsi untuk membuat matriks berpasangan yang menjelaskan berapa kali suatu objek dipilih sebelum dan sesudah semua objek lainnya, dalam sekumpulan lokasi.

Ini contohnya df:

  df <- data.frame(Shop = c("A","A","A","B","B","C","C","D","D","D","E","E","E"),
                   Fruit = c("apple", "orange", "pear",
                             "orange", "pear",
                             "pear", "apple",
                             "pear", "apple", "orange",
                             "pear", "apple", "orange"),
                   Order = c(1, 2, 3,
                            1, 2,
                            1, 2, 
                            1, 2, 3,
                            1, 1, 1))

Di setiap Shop, Fruit dipilih oleh pelanggan di Order tertentu.

Fungsi berikut membuat matriks berpasangan m x n:

loop.function <- function(df){
  
  fruits <- unique(df$Fruit)
  nt <- length(fruits)
  mat <- array(dim=c(nt,nt))
  
  for(m in 1:nt){
    
    for(n in 1:nt){
      
      ## filter df for each pair of fruit
      xm <- df[df$Fruit == fruits[m],]
      xn <- df[df$Fruit == fruits[n],]
      
      ## index instances when a pair of fruit are picked in same shop
      mm <- match(xm$Shop, xn$Shop)
      
      ## filter xm and xn based on mm
      xm <- xm[! is.na(mm),]
      xn <- xn[mm[! is.na(mm)],]
      
      ## assign number of times fruit[m] is picked after fruit[n] to mat[m,n]
      mat[m,n] <- sum(xn$Order < xm$Order)
    }
  }
  
  row.names(mat) <- fruits
  colnames(mat) <- fruits
  
  return(mat)
}

Dimana mat[m,n] adalah berapa kali fruits[m] diambil setelah fruits[n]. Dan mat[n,m] adalah berapa kali fruits[m] dipilih sebelum fruits[n]. Tidak dicatat jika sepasang buah dipetik pada waktu yang sama (misalnya pada Shop E).

Lihat keluaran yang diharapkan:

>loop.function(df)
       apple orange pear
apple      0      0    2
orange     2      0    1
pear       1      2    0

Anda dapat melihat di sini bahwa pear dipilih dua kali sebelum apple (di Shop C dan D), dan apple dipilih satu kali sebelum pear (di Shop A).

Saya mencoba untuk meningkatkan pengetahuan saya tentang vektorisasi, terutama di tempat loop, jadi saya ingin tahu bagaimana loop ini dapat divektorisasi.

(Saya merasa mungkin ada solusi menggunakan outer(), namun pengetahuan saya tentang fungsi vektorisasi masih sangat terbatas.)

Perbarui

Lihat pembandingan dengan data nyata times = 10000 untuk loop.function(), tidyverse.function(), loop.function2(), datatable.function() dan loop.function.TMS():

Unit: milliseconds
                    expr            min        lq       mean    median         uq      max     neval   cld
      loop.function(dat)     186.588600 202.78350 225.724249 215.56575 234.035750 999.8234    10000     e
     tidyverse.function(dat)  21.523400  22.93695  26.795815  23.67290  26.862700 295.7456    10000   c 
     loop.function2(dat)     119.695400 126.48825 142.568758 135.23555 148.876100 929.0066    10000    d
 datatable.function(dat)       8.517600   9.28085  10.644163   9.97835  10.766749 215.3245    10000  b 
  loop.function.TMS(dat)       4.482001   5.08030   5.916408   5.38215   5.833699  77.1935    10000 a 

Mungkin hasil yang paling menarik bagi saya adalah performa tidyverse.function() pada data sebenarnya. Saya harus mencoba menambahkan solusi Rccp di kemudian hari - Saya mengalami kesulitan membuatnya berfungsi pada data sebenarnya.

Saya mengapresiasi segala ketertarikan dan jawaban yang diberikan pada postingan ini - niat saya adalah untuk belajar dan meningkatkan kinerja, dan tentunya banyak pembelajaran dari semua komentar dan solusi yang diberikan. Terima kasih!


person jayb    schedule 08.07.2020    source sumber
comment
hai, berapa dimensi kumpulan data Anda yang sebenarnya dan berapa kali Anda akan memanggil fungsi ini?   -  person chinsoon12    schedule 09.07.2020
comment
dan juga haruskah Pesanan menjadi 1,2,3 bukannya 1,1,1 untuk Toko E?   -  person chinsoon12    schedule 09.07.2020
comment
Berdasarkan ukuran: biasanya, df mungkin berisi ~15 buah yang dipesan di ~100 toko. Ini disebut ~1K kali dalam sekali proses, namun dengan bootstrapping ada 10k kali proses. Di Toko E: tidak, ini bukan kesalahan, saya ingin contoh menyertakan kasus di mana semua buah dipetik secara bersamaan, karena fungsi tersebut harus mengabaikan kasus ini.   -  person jayb    schedule 09.07.2020
comment
@ chinsoon12 Ada beberapa kesamaan dengan pertanyaan ini, tetapi pengurutan dalam masalah saya menambah lapisan kompleksitas tambahan: ‹stackoverflow.com/questions/19891278/  -  person jayb    schedule 09.07.2020
comment
Apakah aman untuk berasumsi bahwa toko selalu tersortir? Jika tidak, apakah aman untuk menyortirnya?   -  person Cole    schedule 11.07.2020
comment
@jayb Terima kasih telah memposting kumpulan data mainan kecil agar orang-orang dapat mencoba kodenya. Namun, karena pertanyaan Anda adalah tentang kecepatan dan kinerja, bisakah Anda juga memberikan kumpulan data dengan ukuran dan kompleksitas yang relevan untuk pembandingan dalam pertanyaan Anda. Tanpa data tersebut akan sulit/tidak mungkin untuk mengevaluasi jawaban. Jelaskan juga peningkatan yang Anda harapkan. Terima kasih.   -  person Henrik    schedule 11.07.2020
comment
Karena loop berkinerja baik, saya menambahkan solusi Rcpp: kinerja yang sangat bagus   -  person Waldi    schedule 12.07.2020
comment
Akankah urutannya misalnya c(1, 1, 2, 3) atau akan selalu c(1, 1, 1) atau berurutan?   -  person Andrew    schedule 14.07.2020
comment
@jayb, pertanyaan Anda menghasilkan banyak jawaban, dapatkah Anda memperbarui benchmarking pada kumpulan data sebenarnya? Terima kasih atas tanggapan Anda   -  person Waldi    schedule 16.07.2020
comment
@jayb Saya sedang mengerjakan masalah yang mengingatkan pertanyaan Anda: Saya pikir paket arulesSequence mungkin relevan bagi Anda. Lihat mis. tutorial ini: Penambangan Pola Berurutan di R   -  person Henrik    schedule 20.08.2020


Jawaban (4)


Tampaknya tidak mungkin untuk melakukan vektorisasi pada bingkai data asli df. Namun jika Anda mengubahnya menggunakan reshape2::dcast(), untuk memiliki satu baris per setiap toko:

require(reshape2)

df$Fruit <- as.character(df$Fruit)

by_shop <- dcast(df, Shop ~ Fruit, value.var = "Order")

#   Shop apple orange pear
# 1    A     1      2    3
# 2    B    NA      1    2
# 3    C     2     NA    1
# 4    D     2      3    1
# 5    E     1      1    1

..., maka Anda dapat dengan mudah melakukan vektorisasi setidaknya untuk setiap kombinasi [m, n]:

fruits <- unique(df$Fruit)
outer(fruits, fruits, 
    Vectorize(
        function (m, n, by_shop) sum(by_shop[,m] > by_shop[,n], na.rm = TRUE), 
        c("m", "n")
    ), 
    by_shop)
#      [,1] [,2] [,3]
# [1,]    0    0    2
# [2,]    2    0    1
# [3,]    1    2    0

Ini mungkin solusi yang ingin Anda lakukan dengan outer. Solusi yang jauh lebih cepat adalah vektorisasi sebenarnya pada semua kombinasi buah [m, n], tapi saya sudah memikirkannya dan saya tidak melihat cara apa pun untuk melakukannya. Jadi saya harus menggunakan fungsi Vectorize yang tentu saja jauh lebih lambat daripada vektorisasi sebenarnya.

Perbandingan tolok ukur dengan fungsi asli Anda:

Unit: milliseconds
                  expr      min       lq     mean   median       uq      max neval
     loop.function(df) 3.788794 3.926851 4.157606 4.002502 4.090898 9.529923   100
 loop.function.TMS(df) 1.582858 1.625566 1.804140 1.670095 1.756671 8.569813   100

Fungsi & kode benchmark (juga menambahkan pelestarian nama dim):

require(reshape2)   
loop.function.TMS <- function(df) { 
    df$Fruit <- as.character(df$Fruit)
    by_shop <- dcast(df, Shop ~ Fruit, value.var = "Order")
    fruits <- unique(df$Fruit)
    o <- outer(fruits, fruits, Vectorize(function (m, n, by_shop) sum(by_shop[,m] > by_shop[,n], na.rm = TRUE), c("m", "n")), by_shop)
    colnames(o) <- rownames(o) <- fruits
    o
}

require(microbenchmark)
microbenchmark(loop.function(df), loop.function.TMS(df))
person Tomas    schedule 15.07.2020
comment
Terima kasih untuk ini - ini adalah penggunaan outer() yang sangat menarik - Saya belum pernah melihatnya digunakan dengan Vectorize() dengan cara ini. - person jayb; 16.07.2020

Solusi data.table :

library(data.table)
setDT(df)
setkey(df,Shop)
dcast(df[df,on=.(Shop=Shop),allow.cartesian=T][
           ,.(cnt=sum(i.Order<Order&i.Fruit!=Fruit)),by=.(Fruit,i.Fruit)]
      ,Fruit~i.Fruit,value.var='cnt')

    Fruit apple orange pear
1:  apple     0      0    2
2: orange     2      0    1
3:   pear     1      2    0

Indeks Shop tidak diperlukan untuk contoh ini, namun mungkin akan meningkatkan kinerja pada kumpulan data yang lebih besar.

Karena pertanyaan tersebut menimbulkan banyak komentar tentang kinerja, saya memutuskan untuk memeriksa apa yang dapat dihasilkan Rcpp:

library(Rcpp)
cppFunction('NumericMatrix rcppPair(DataFrame df) {

std::vector<std::string> Shop = Rcpp::as<std::vector<std::string> >(df["Shop"]);
Rcpp::NumericVector Order = df["Order"];
Rcpp::StringVector Fruit = df["Fruit"];
StringVector FruitLevels = sort_unique(Fruit);
IntegerVector FruitInt = match(Fruit, FruitLevels);
int n  = FruitLevels.length();

std::string currentShop = "";
int order, fruit, i, f;

NumericMatrix result(n,n);
NumericVector fruitOrder(n);

for (i=0;i<Fruit.length();i++){
    if (currentShop != Shop[i]) {
       //Init counter for each shop
       currentShop = Shop[i];
       std::fill(fruitOrder.begin(), fruitOrder.end(), 0);
    }
    order = Order[i];
    fruit = FruitInt[i];
    fruitOrder[fruit-1] = order;
    for (f=0;f<n;f++) {
       if (order > fruitOrder[f] & fruitOrder[f]>0 ) { 
         result(fruit-1,f) = result(fruit-1,f)+1; 
    }
  }
}
rownames(result) = FruitLevels;
colnames(result) = FruitLevels;
return(result);
}
')

rcppPair(df)

       apple orange pear
apple      0      0    2
orange     2      0    1
pear       1      2    0

Pada contoh kumpulan data, solusi ini berjalan ›500 kali lebih cepat dibandingkan solusi data.table, mungkin karena solusi ini tidak memiliki masalah produk kartesius. Ini tidak seharusnya berlaku pada input yang salah, dan mengharapkan toko/pesanan berada dalam urutan menaik.

Mengingat beberapa menit yang dihabiskan untuk menemukan 3 baris kode untuk solusi data.table, dibandingkan dengan solusi Rcpp/proses debugging yang jauh lebih lama, saya tidak akan merekomendasikan untuk menggunakan Rcpp di sini kecuali ada hambatan kinerja yang nyata.

Namun menarik untuk diingat bahwa jika kinerja adalah suatu keharusan, Rcpp mungkin sepadan dengan usaha yang dilakukan.

person Waldi    schedule 10.07.2020
comment
Kode-golf (yang pada akhirnya disamakan dengan kecepatan): Saya tidak tahu bahwa &i.Fruit!=Fruit mengubah apa pun dengan data sampel ini. Apakah Anda yakin itu perlu secara logis? - person r2evans; 11.07.2020
comment
@ r2evans, saya pikir ini akan berguna untuk tidak menghitung misalnya Apple diambil pertama dan ketiga, dengan jeruk di antaranya. Dari uraian tersebut saya memahami bahwa kami hanya ingin menghitung buah-buahan yang berbeda. - person Waldi; 11.07.2020
comment
Saya juga memikirkan hal itu, dan itu jelas merupakan pendekatan yang lebih aman. Jika OP menyatakan bahwa input sudah unik per-Shop maka saya rasa saran saya berlaku. (Tidak sulit untuk memastikannya sebelum matrixification. - person r2evans; 11.07.2020
comment
FYI, di mesin saya, solusi Anda (difungsikan) hanya membutuhkan separuh waktu loop.function (karena OP ingin mempercepat suatu fungsi). - person r2evans; 11.07.2020
comment
@ r2evans, terima kasih atas benchmarknya. Menarik untuk melihat hasilnya dengan dataset sebenarnya (100 toko / 15 buah) - person Waldi; 11.07.2020
comment
Mungkin menambahkan kondisi non-ekuitas pada gabungan pertama, untuk hanya menyertakan kombinasi 'Pesanan' yang relevan (dan menghindari ledakan allow.cartesian). Kemudian perhitungan pada langkah kedua dapat disederhanakan menjadi menghitung baris. d = df[df, on = .(Shop, Order < Order), .(before = Fruit, after = i.Fruit)]; dcast(d[!is.na(before), .N, by = .(before, after)], after ~ before, value.var = "N", fill = 0). - person Henrik; 11.07.2020
comment
@Henrik, terima kasih atas saran Anda. Saya mengerti maksud Anda dan juga membuat saya memikirkannya. Saya sampai pada kesimpulan bahwa ledakan kartesius akan dibatasi oleh gabungan Toko. Dalam hal microbenchmarking, misalnya kumpulan data, solusi Anda sedikit lebih lambat, namun hal ini tentunya harus diuji pada kumpulan data nyata yang kelebihannya harus lebih terlihat. - person Waldi; 11.07.2020
comment
Dengan saran @Henriks, Anda bisa mendapatkan hasil yang sama dengan table(i.Fruit, Fruit) seperti j untuk pendekatan non-equi. Jadi df[df, on = .(Shop, Order < Order), table(i.Fruit, Fruit), allow.cartesian = T, nomatch = 0L]. Saya juga sangat tertarik dengan kinerja jawaban Anda di dunia nyata - pembandingan dalam milidetik jarang berhasil. - person Cole; 11.07.2020
comment
@Waldi Terima kasih banyak atas solusi ini - Saya tidak paham dengan data.table, tapi mungkin sebaiknya begitu, mengingat kinerjanya. Satu masalah dalam menggunakan solusi ini dalam algoritma saya yang sebenarnya adalah bahwa outputnya berupa daftar, yang tidak dapat saya indeks dengan cara yang sama seperti matriks - person jayb; 16.07.2020
comment
@jayb, jika Anda mencari kinerja, data.table patut dicoba; ). konversi ke matriks sesederhana : as.matrix(dt) - person Waldi; 16.07.2020

Berikut adalah pendekatan yang membuat modifikasi sederhana agar 5x lebih cepat.

loop.function2 <- function(df){

    spl_df = split(df[, c(1L, 3L)], df[[2L]])
    
    mat <- array(0L,
                 dim=c(length(spl_df), length(spl_df)),
                 dimnames = list(names(spl_df), names(spl_df)))
    
    for (m in 1:(length(spl_df) - 1L)) {
        xm = spl_df[[m]]
        mShop = xm$Shop
        for (n in ((1+m):length(spl_df))) {
            xn = spl_df[[n]]
            mm = match(mShop, xn$Shop)
            inds = which(!is.na(mm))
            mOrder = xm[inds, "Order"]
            nOrder = xn[mm[inds], "Order"]

            mat[m, n] <- sum(nOrder < mOrder)
            mat[n, m] <- sum(mOrder < nOrder)
        }
    }
    mat
}

Ada 3 konsep utama:

  1. Baris df[df$Fruits == fruits[m], ] asli tidak efisien karena Anda akan membuat perbandingan yang sama sebanyak length(Fruits)^2 kali. Sebagai gantinya, kita bisa menggunakan split() yang berarti kita hanya memindai Buah satu kali.
  2. Ada banyak penggunaan df$var yang akan mengekstrak vektor selama setiap loop. Di sini, kami menempatkan penugasan xm di luar loop dalam dan kami mencoba meminimalkan apa yang perlu kami subset/ekstrak.
  3. Saya mengubahnya menjadi lebih dekat ke combn karena kita dapat menggunakan kembali kondisi match() dengan melakukan kedua sum(xmOrder > xnOrder) dan kemudian mengalihkannya ke sum(xmOrder < xnOrder).

Pertunjukan:

bench::mark(loop.function(df), loop.function2(df))

# A tibble: 2 x 13
##  expression              min median
##  <bch:expr>         <bch:tm> <bch:>
##1 loop.function(df)    3.57ms 4.34ms
##2 loop.function2(df)  677.2us 858.6us

Firasat saya adalah untuk kumpulan data Anda yang lebih besar, solusi data.table akan lebih cepat. Namun untuk kumpulan data yang lebih kecil, performanya seharusnya cukup baik.

Terakhir, inilah pendekatan rcpp yang tampaknya lebih lambat dari @Waldi:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
IntegerMatrix loop_function_cpp(List x) {
    int x_size = x.size();
    IntegerMatrix ans(x_size, x_size);
    
    for (int m = 0; m < x_size - 1; m++) {
        DataFrame xm = x[m];
        CharacterVector mShop = xm[0];
        IntegerVector mOrder = xm[1];
        int nrows = mShop.size();
        for (int n = m + 1; n < x_size; n++) {
            DataFrame xn = x[n];
            CharacterVector nShop = xn[0];
            IntegerVector nOrder = xn[1];
            for (int i = 0; i < nrows; i++) {
                for (int j = 0; j < nrows; j++) {
                    if (mShop[i] == nShop[j]) {
                        if (mOrder[i] > nOrder[j])
                           ans(m, n)++;
                        else
                            ans(n, m)++;
                        break;
                    }
                }
            }
        }
    }
    return(ans);
}
loop_wrapper = function(df) {
  loop_function_cpp(split(df[, c(1L, 3L)], df[[2L]]))
}
loop_wrapper(df)
``
person Cole    schedule 11.07.2020
comment
Terima kasih banyak atas solusinya - pasti ada banyak hal yang harus dipelajari dalam hal mempercepat loop yang ada di sini, terutama dalam menjaga kode yang benar-benar diperlukan dalam setiap loop. - person jayb; 16.07.2020

Oke, ini solusinya:

library(tidyverse)

# a dataframe with all fruit combinations
df_compare <-  expand.grid(row_fruit = unique(df$Fruit)
                           , column_fruit = unique(df$Fruit)
                           , stringsAsFactors = FALSE)

df_compare %>%
    left_join(df, by = c("row_fruit" = "Fruit")) %>%
    left_join(df, by = c("column_fruit" = "Fruit")) %>%
    filter(Shop.x == Shop.y &
               Order.x < Order.y) %>%
    group_by(row_fruit, column_fruit) %>%
    summarise(obs = n()) %>%
    pivot_wider(names_from = row_fruit, values_from = obs) %>%
    arrange(column_fruit) %>%
    mutate_if(is.numeric, function(x) replace_na(x, 0)) %>%
    column_to_rownames("column_fruit") %>%
    as.matrix()

       apple orange pear
apple      0      0    2
orange     2      0    1
pear       1      2    0

Jika Anda tidak tahu apa yang terjadi di bagian kode kedua (df_compare %>% ...), bacalah pipa (%>%) sebagai 'lalu'. Jalankan kode dari df_compare hingga sebelum pipa mana pun untuk melihat hasil antara.

person Georgery    schedule 08.07.2020
comment
Terima kasih atas solusi yang disarankan. Saya seharusnya mengklarifikasi bahwa penting untuk menjaga struktur (dan urutan) keluaran sesuai dengan keluaran loop.function(). - person jayb; 08.07.2020
comment
Saya membuat beberapa perubahan pada kode Anda untuk mengembalikan keluaran yang sama seperti loop.function(). Mengikuti column_to_rownames("row_fruit") %>% saya menambahkan ` select(all_of(unique(df$Fruit))) %›%` dan mengikuti as.matrix(), saya menambahkan %>% replace_na(replace = 0) . Kode sekarang mengembalikan keluaran yang sama, tetapi tidak meningkatkan kecepatan - alasan saya tertarik pada vektorisasi adalah terkait kinerja. Saya telah menambahkan tolok ukur berdasarkan kode Anda (dengan amandemen). - person jayb; 08.07.2020
comment
Jadi, saya mengedit jawabannya. Ingin melakukannya kemarin, tetapi stackoverflow sedang down. Saya menguji dan loop.function() lebih cepat - juga untuk kumpulan data yang lebih besar. Namun, agak aneh mengubah pertanyaan seperti itu. Anda bertanya tentang vektorisasi, bukan tentang kinerja. Untuk pertanyaan awal, jawaban saya adalah sebuah jawaban. - person Georgery; 09.07.2020
comment
Hai, Saya hanya mengubah postingan asli untuk memperjelas beberapa aspek pertanyaan, seperti yang Anda minta, dan untuk menambahkan tolok ukur untuk kode Anda. Postingan tersebut selalu ditandai dengan kinerja dan baris pertama selalu menyatakan saya ingin mempercepat suatu fungsi... - person jayb; 09.07.2020