R: Цикл векторизации для создания попарной матрицы

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

Вот пример 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))

В каждом Shop, Fruit выбирается покупателем в данном Order.

Следующая функция создает 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)
}

Где mat[m,n] - количество раз, когда fruits[m] выбирается после fruits[n]. И mat[n,m] - это количество раз, когда fruits[m] выбирается до fruits[n]. Не регистрируется, если пары фруктов собираются одновременно (например, в Shop E).

См. Ожидаемый результат:

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

Вы можете видеть здесь, что pear выбирается дважды перед appleShop C и D), а apple выбирается один раз перед pearShop A).

Я пытаюсь улучшить свои знания о векторизации, особенно о циклах, поэтому я хочу знать, как этот цикл можно векторизовать.

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

Обновить

См. Сравнительный анализ с реальными данными times = 10000 для loop.function(), tidyverse.function(), loop.function2(), datatable.function() и 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 

Наверное, самый интересный для меня результат - это производительность tidyverse.function() на реальных данных. Позже мне придется попробовать добавить Rccp решений - мне не удается заставить их работать с реальными данными.

Я ценю весь интерес и ответы, проявленные к этому сообщению - моим намерением было изучить и улучшить производительность, и, безусловно, есть чему поучиться из всех предоставленных комментариев и решений. Спасибо!


person jayb    schedule 08.07.2020    source источник
comment
привет, каковы размеры вашего фактического набора данных и сколько раз вы будете вызывать эту функцию?   -  person chinsoon12    schedule 09.07.2020
comment
а также должен ли Order быть 1,2,3 вместо 1,1,1 для Shop E?   -  person chinsoon12    schedule 09.07.2020
comment
По размеру: обычно df может содержать ~ 15 фруктов, заказанных в ~ 100 магазинах. Он вызывается ~ 1 КБ раз за один запуск, однако при начальной загрузке выполняется 10 КБ запусков. В магазине E: нет, это не было ошибкой, я хотел, чтобы пример включал случай, когда все фрукты были собраны одновременно, так как важно, чтобы функция игнорировала эти случаи   -  person jayb    schedule 09.07.2020
comment
@ chinsoon12 В этом вопросе есть некоторое сходство, но порядок в моей проблеме добавляет дополнительный уровень сложности: ‹stackoverflow.com/questions/19891278/  -  person jayb    schedule 09.07.2020
comment
Можно ли предположить, что магазины всегда отсортированы? Если нет, безопасно ли их сортировать?   -  person Cole    schedule 11.07.2020
comment
@jayb Спасибо, что разместили небольшой набор данных о игрушках, чтобы люди могли опробовать свой код. Однако, поскольку ваш вопрос касается скорости и производительности, не могли бы вы также предоставить в своем вопросе набор (ы) данных о размере и сложности, релевантных для сравнительного анализа. Без таких данных будет сложно / невозможно оценить ответы. Также опишите ожидаемые улучшения. Спасибо.   -  person Henrik    schedule 11.07.2020
comment
Поскольку циклы работают хорошо, я добавил решение Rcpp: очень хорошая производительность   -  person Waldi    schedule 12.07.2020
comment
Будет ли заказ когда-либо, например, c(1, 1, 2, 3) или всегда будет c(1, 1, 1) или последовательно?   -  person Andrew    schedule 14.07.2020
comment
@jayb, на ваш вопрос было получено много ответов, не могли бы вы обновить сравнительный анализ на реальном наборе данных? Спасибо за ваш отзыв   -  person Waldi    schedule 16.07.2020
comment
@jayb Я работал над проблемой, которая напомнила о вашем вопросе: я думаю, что пакет arulesSequence может быть вам интересен. См., Например, это руководство: Последовательный анализ шаблонов в R   -  person Henrik    schedule 20.08.2020


Ответы (4)


Кажется, что невозможно векторизовать исходный фрейм данных df. Но если вы трансформируете его с помощью reshape2::dcast(), чтобы на каждый магазин была одна строчка:

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

..., то вы можете легко векторизовать по крайней мере для каждой комбинации [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

Вероятно, это решение, которое вы хотели использовать с outer. Гораздо более быстрым решением была бы настоящая векторизация всех комбинаций фруктов [m, n], но я думал об этом и не вижу способа сделать это. Поэтому мне пришлось использовать функцию Vectorize, которая, конечно, намного медленнее, чем настоящая векторизация.

Контрольное сравнение с вашей исходной функцией:

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

Код функции и теста (также добавлено сохранение dimnames):

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
Спасибо за это - это действительно интересное использование outer() - я никогда не видел, чтобы оно использовалось с Vectorize() таким образом. - person jayb; 16.07.2020

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

В этом примере индекс Shop не нужен, но он, вероятно, повысит производительность для большего набора данных.

Поскольку вопрос вызвал много комментариев по поводу производительности, я решил проверить, что может принести 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

В примере набора данных это выполняется ›в 500 раз быстрее, чем решение data.table, вероятно, потому, что оно не имеет проблемы с декартовым произведением. Это не должно быть устойчивым при неправильном вводе и предполагает, что магазины / заказ находятся в порядке возрастания.

Учитывая несколько минут, потраченных на поиск 3 строк кода для data.table решения, по сравнению с гораздо более длительным Rcpp процессом решения / отладки, я бы не рекомендовал использовать Rcpp здесь, если нет реального узкого места в производительности.

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

person Waldi    schedule 10.07.2020
comment
Код-гольф (который в конечном итоге приравнивается к скорости): я не знаю, что &i.Fruit!=Fruit что-то меняет с этими образцами данных. Вы уверены, что это необходимо по логике? - person r2evans; 11.07.2020
comment
@ r2evans, я подумал, что было бы полезно не считать, например, Apple, взятую первым, а затем третьим, с апельсином между ними. Из описания я понимаю, что мы хотим только подсчитывать разные фрукты. - person Waldi; 11.07.2020
comment
Я тоже так думал, и это определенно более безопасный подход. Если OP удостоверяет, что ввод уже уникален для-Shop, тогда я думаю, что мое предложение остается в силе. (Нетрудно убедиться, что до matrixification. - person r2evans; 11.07.2020
comment
К вашему сведению, на моей машине ваше решение (функционализированное) занимает чуть более половины времени loop.function (поскольку OP хочет ускорить выполнение функции). - person r2evans; 11.07.2020
comment
@ r2evans, спасибо за тестирование. Будет интересно увидеть результаты с реальным набором данных (100 магазинов / 15 фруктов) - person Waldi; 11.07.2020
comment
Возможно, добавьте к первому соединению условие неэквивалентности, чтобы включить только релевантные комбинации «Порядок» (и избежать взрыва allow.cartesian). Затем вычисления на втором этапе можно упростить до подсчета строк. 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, спасибо за ваше предложение. Я понял вашу точку зрения и заставил себя задуматься об этом. Я пришел к выводу, что декартово взрыв в любом случае будет ограничен присоединением Shop. Что касается микробенчмаркинга, для примера набора данных ваше решение немного медленнее, но это, конечно, необходимо протестировать на реальном наборе данных, где его преимущества должны быть более заметны. - person Waldi; 11.07.2020
comment
С предложением @Henriks вы можете получить тот же результат с table(i.Fruit, Fruit), что и j для неэквивалентного подхода. Итак df[df, on = .(Shop, Order < Order), table(i.Fruit, Fruit), allow.cartesian = T, nomatch = 0L]. Меня тоже очень интересует реальная производительность вашего ответа - бенчмаркинг в миллисекундах редко говорит об этом. - person Cole; 11.07.2020
comment
@Waldi Большое спасибо за это решение - я не знаком с data.table, но, вероятно, мне стоит знать, учитывая производительность. Одна из проблем при использовании этого решения в моем реальном алгоритме заключается в том, что на выходе получается список, который я не могу индексировать так же, как матрицу. - person jayb; 16.07.2020
comment
@jayb, если вы ищете производительность, стоит попробовать data.table; ). преобразование в матрицу очень просто: as.matrix(dt) - person Waldi; 16.07.2020

Вот подход, который вносит простые изменения, чтобы сделать его в 5 раз быстрее.

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
}

Есть 3 основных понятия:

  1. Исходные df[df$Fruits == fruits[m], ] строки были неэффективными, поскольку вы бы проводили такое же сравнение length(Fruits)^2 раз. Вместо этого мы можем использовать split(), что означает, что мы сканируем Фрукты только один раз.
  2. Очень часто использовался df$var, который извлекает вектор во время каждого цикла. Здесь мы размещаем присваивание xm за пределами внутреннего цикла и пытаемся минимизировать то, что нам нужно для подмножества / извлечения.
  3. Я изменил его, чтобы он был ближе к combn, так как мы можем повторно использовать наше match() условие, выполнив оба sum(xmOrder > xnOrder), а затем переключив его на sum(xmOrder < xnOrder).

Представление:

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

Я догадываюсь, что для вашего более крупного набора данных от @ Waldi data.table будет быстрее. Но для небольших наборов данных это должно быть достаточно эффективным.

Наконец, вот еще один подход rcpp, который кажется медленнее, чем @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
Большое спасибо за решения - здесь определенно есть чему поучиться с точки зрения ускорения существующего цикла, особенно по сохранению только абсолютно необходимого кода в каждом цикле. - person jayb; 16.07.2020

Хорошо, вот решение:

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

Если вы не знаете, что происходит во второй части кода (df_compare %>% ...), прочитайте вертикальную черту (%>%) как «затем». Запустите код от df_compare до любого канала, чтобы увидеть промежуточные результаты.

person Georgery    schedule 08.07.2020
comment
Спасибо за предложенное решение. Я должен был пояснить, что важно, чтобы структура (и порядок) вывода сохранялись в соответствии с выводом loop.function (). - person jayb; 08.07.2020
comment
Я внес некоторые изменения в ваш код, чтобы вернуть тот же результат, что и loop.function(). После column_to_rownames("row_fruit") %>% я добавил `select (all_of (unique (df $ Fruit)))%›% `, а после as.matrix() я добавил %>% replace_na(replace = 0). Код теперь возвращает тот же результат, но не улучшает скорость - причина, по которой меня интересует векторизация, связана с производительностью. Я добавил бенчмаркинг на основе вашего кода (с поправками). - person jayb; 08.07.2020
comment
Итак, я отредактировал ответ. Хотел сделать это вчера, но не работает stackoverflow. Я тестировал, и loop.function() работает быстрее - также для больших наборов данных. Однако немного странно так менять вопрос. Вы спросили о векторизации, а не о производительности. На исходный вопрос моим ответом был ответ. - person Georgery; 09.07.2020
comment
Привет, я изменил исходное сообщение только для того, чтобы прояснить некоторые аспекты вопроса, как вы просили, и для добавления тестов для вашего кода. Пост всегда отмечался производительностью, и в первой строке всегда говорилось, что я хочу ускорить работу ... - person jayb; 09.07.2020