Что быстрее/лучше: зацикливайтесь на каждой строке фрейма данных или разбивайте его на список длиной «nrow», R

#r #dataframe #parallel-processing #future

Вопрос:

Мне просто интересно, является ли это серьезным компромиссом, который следует учитывать. Допустим, у вас есть фрейм данных в R и вы хотите выполнить операцию над каждым наблюдением (строкой). Я знаю, что это уже деликатный вопрос для перебора строк, поэтому мне просто было интересно, какой из трех вариантов:

  • Нормально для цикла над каждой строкой
  • Разделите фрейм данных на список nrow элементов, примените операцию к каждому элементу и свяжите результат вместе
  • Сделайте то же самое, что и выше, параллельно

Без какого-либо сравнительного анализа или около того, это в основном то, о чем я спрашиваю в псевдокоде:

 
library(future.apply)

n = 1000000
x = 1:n
y = x   rnorm(n, mean=50, sd=50)

df = data.frame(
  x = x,
  y = y
)

# 1)
# iterating over each row with normal for loop
for(r in 1:nrow(df)){
  row = df[r, ]
  r = f(row)
  df[r, ] = row
}

# 2)
# create a list of length nrow(df) and apply do something to each list element
# and rowbind it together
res = df %>% split(., .$x) %>% lapply(., function(x){
  r = f(x)
})

bind_rows(res, .id="x")  

# 3)
# create a list of length nrow(df) and apply do something to each list element in parallel
# and rowbind it together

res = df %>% split(., .$x) %>% future_lapply(., function(x){
  r = f(x)
})
bind_rows(res, .id="x")  

 

Вероятно, ни один из вышеперечисленных вариантов не является лучшим, поэтому я был бы рад высказать любые мысли по этому поводу. Извините, если это очень наивный вопрос. Я только начинаю с Р.

Комментарии:

1. split это почти наверняка неправильный подход. Исходя из вашего желания делать по одной строке за раз в a future , подумайте future.apply::future_Map о столбцах, которые вам нужно передать в свой f(.) .

2. Вам не нужно использовать split() , просто зацикливайтесь на индексах. Или, если у вас есть функция, которая фактически извлекает значения из столбцов, вы можете использовать furrr::future_pmap_dfr() .

Ответ №1:

Я очень часто использую эту схему tibble %>% nest %>% mutate(map) %>% unnest . Взгляните на приведенный ниже пример.

 library(tidyverse)
n = 10000

f = function(data) sqrt(data$x^2 data$y^2 data$z^2)
tibble(
  x = 1:n,
  y = x   rnorm(n, mean=50, sd=50),
  z = x   y   rnorm(n, mean=50, sd=50)
) %>% nest(data = c(x:z)) %>% 
  mutate(l = map(data, f)) %>% 
  unnest(c(data, l))
 

выход

 # A tibble: 10,000 x 4
       x     y     z     l
   <int> <dbl> <dbl> <dbl>
 1     1  67.1 136.  151. 
 2     2  75.4 127.  148. 
 3     3 -11.1  38.9  40.6
 4     4  58.1 106.  121. 
 5     5  23.5 126.  128. 
 6     6  73.4 179.  193. 
 7     7  44.5 121.  129. 
 8     8 106.  131.  169. 
 9     9  32.5 140.  144. 
10    10 -27.7  82.7  87.8
# ... with 9,990 more rows
 

Лично для меня это очень понятно и элегантно. Но вы можете с этим не согласиться.

Обновление 1

Честно говоря, ваш вопрос также заинтриговал меня с точки зрения производительности. Поэтому я решил проверить это. Вот код:

 library(tidyverse)
library(microbenchmark)

n = 1000
df = tibble(
  x = 1:n,
  y = x   rnorm(n, mean=50, sd=50),
  z = x   y   rnorm(n, mean=50, sd=50)
)

f = function(data) sqrt(data$x^2 data$y^2 data$z^2)

f1 = function(df){
  df %>% nest(data = c(x:z)) %>% 
    mutate(l = map(data, f)) %>% 
    unnest(c(data, l))
}
f1(df)

f2 = function(df){
  df = df %>% mutate(l=NA)
  for(r in 1:nrow(df)){
    row = df[r, ]
    df$l[r] = f(row)
  }
  df
}
f2(df)


f3 = function(df){
  res = df %>% 
    split(., .$x) %>% 
    lapply(., f)
  df %>% bind_cols(l = unlist(res))
}
f3(df)


ggplot2::autoplot(microbenchmark(f1(df), f2(df), f3(df), times=100))
 

Вот результат:
введите описание изображения здесь
Должен ли я добавить что-нибудь еще и объяснить, почему схема tibble%>% nest%>% mutate (map)%>% unnest такая классная?

Комментарии:

1. Большое спасибо:) Это выглядит действительно хорошо, мне просто нужно некоторое время, чтобы пройти через это и посмотреть, как я могу применить это к своей проблеме. Но почему это происходит намного быстрее?

2.Почему это решение работает быстрее? Я не знаю наверняка, но, похоже, это связано с внутренней оптимизацией dplyr пакета. Но я знаю, что почти всегда for цикл в R-плохая идея. И правда в том, что я нахожу все меньше и меньше причин использовать его. С другой стороны, схема tibble%>% nest%>% mutate (map)%>% unnest мне очень ясна. Здесь я мог бы сослаться на множество примеров на этом сайте, где я показал другим, как его применять. Однако лучше проконсультироваться с настоящим гуру. Я предлагаю вам прочитать главу 25 R для науки о данных r4ds.had.co.nz/many-models.html осторожно

3. большое вам спасибо!:) Высоко ценится

4. В случае переполнения стека благодарность выражается, когда вы принимаете ответ.