在 R 中有效地将大 data.frames 传递给类似应用的函数

Passing Large data.frames to Apply-like Functions Efficiently in R

这是一个关于将大型数据集传递给类似应用的函数时的资源和效率的问题。

例子

[编辑:更改示例和描述以说明多个表的使用和每个@UWE 评论的计算步骤]

library(dplyr)
#> Warning: package 'dplyr' was built under R version 4.0.5 ...[snip]...

set.seed(10)

# Objective: Add period-cost per portion of person's selected fruit 

df.A <- data.frame(
  period = rep(1:3, times = 1, each = 4),
  Name = rep(c("John", "Paul","Ringo", "George"), times = 3),
  Fruit = sample(c("Apple", "Pear", "Banana", "Apple"), size = 12, replace = TRUE)
) # extend to many people, many periods, many fruit

df.B <- data.frame(
  Fruit = c("Pear", "Apple", "Banana"), 
  id = c(1, 2, 3),
  pound.per.portion = c(0.396832,0.440925,0.299829)
) # one entry per fruit

df.C <- data.frame(
  id = rep(1:3, times=3),
  period = rep(1:3, times = 1, each = 3),
  price.pound = c(2.33, 0.99, 2.15, 2.38, 1.01, 2.20, 2.42, 1.04, 2.25)
) # one entry per fruit per period

df.A
#>    period   Name  Fruit
#> 1       1   John Banana
#> 2       1   Paul  Apple
#> 3       1  Ringo   Pear
#> 4       1 George  Apple
#> 5       2   John  Apple
#> 6       2   Paul Banana
#> 7       2  Ringo  Apple
#> 8       2 George   Pear
#> 9       3   John Banana
#> 10      3   Paul Banana
#> 11      3  Ringo Banana
#> 12      3 George  Apple
df.B
#>    Fruit id pound.per.portion
#> 1   Pear  1          0.396832
#> 2  Apple  2          0.440925
#> 3 Banana  3          0.299829
df.C
#>   id period price.pound
#> 1  1      1        2.33
#> 2  2      1        0.99
#> 3  3      1        2.15
#> 4  1      2        2.38
#> 5  2      2        1.01
#> 6  3      2        2.20
#> 7  1      3        2.42
#> 8  2      3        1.04
#> 9  3      3        2.25

df.A$portion.price <- apply(df.A, MARGIN = 1, 
                           FUN = function(x, legend, prices){
                             # please ignore efficiency of this function
                             # the internal function is not the focus of the question
                             fruit.info <- df.B[df.B$Fruit == x[["Fruit"]],]

                             cost <- df.C %>% 
                               filter(period == x[["period"]],
                                      id == fruit.info[["id"]]) %>%
                               select(price.pound) %>%
                               `*`(fruit.info$pound.per.portion)
                             cost[[1]]
                           }, 
                           legend = df.B, prices = df.C) 
                            # Question relates to passing of legend and prices
                            # if `apply` passes df.B and df.C many times
                            # and df.B, df.C are large - is this inefficient, is there a better way

head(df.A, 5)  
#>   period   Name  Fruit portion.price
#> 1      1   John Banana     0.6446323
#> 2      1   Paul  Apple     0.4365157
#> 3      1  Ringo   Pear     0.9246186
#> 4      1 George  Apple     0.4365157
#> 5      2   John  Apple     0.4453343

reprex package (v2.0.1)

于 2022-05-20 创建

本例中的objective是在df.A中增加一列,显示特定时期特定人选择的水果的份量成本。

存在三组数据,但仅其中 none 组就具有计算所需的所有信息。

df.A包含人物、时期和他们选择的水果名称。每个时期的每个人都有一个条目。

df.C 有水果按时期的价格信息,但价格表示为每磅价格,而不是部分价格,并且数据集不识别水果名称(仅识别编号)。每个时期的每个水果都有一个条目。

df.B 提供缺失的信息。首先,它为 df.A$Name 定义了 df.C$id,它提供了一个将每磅成本转换为每份成本的系数。每个水果只需要一个条目。

对于 df.A 的每一行,apply 将人的水果名称和周期以及两个参考集(df.Bdf.C)传递给函数.该函数从 df.B 中查找必要的信息,它用于引用来自 df.C 的数据,然后用于计算每部分的成本(返回)。

函数本身对这个问题并不重要,只是为了说明使用多个数据集来查找每一行的值。

这个例子很简单(四个人,三个时期,三个水果)并且非常易于管理apply;但是,理论上,这些数据集中的每一个都可以包含数千行。

讨论主题从这里开始

如果我理解正确,r 传递的是值而不是引用。我相信这意味着上面示例中的 apply 函数为 df.A 的每一行创建了 df.Bdf.C 的新副本。假设这是正确的,这感觉效率不高,尤其是在数据集很大的情况下。

在使用大数据集时,对于这种外观 up/processing,有没有比 apply 更好的解决方案?

我知道 rcpp 函数可以使用引用而不是值。是否会构建一个自定义 rccp 函数,其行为类似于 apply 仅使用引用,或者是否存在标准的现成方法?

apply() 函数与 data.frames 一起使用有一个主要缺点,因为 apply() 在继续之前将 data.frame 强制转换为 矩阵 (参见 Patrick Burns' The R Inferno 的第 8.2.38 节)。

由于矩阵的所有元素都必须是同一类型 data.frame 的所有 列都被强制转换为一种通用数据类型。

这可以通过

验证
apply(df.A, MARGIN = 2, str)
 chr [1:12] "1" "1" "1" "1" "2" "2" "2" "2" "3" "3" "3" "3"
 chr [1:12] "John" "Paul" "Ringo" "George" "John" "Paul" "Ringo" "George" "John" "Paul" "Ringo" "George"
 chr [1:12] "Banana" "Apple" "Pear" "Apple" "Apple" "Banana" "Apple" "Pear" "Banana" "Banana" "Banana" ...

在这里,整数列 period 也被强制键入字符。这很昂贵,可能会创建所有数据的副本。


那么我们可以做些什么来实现 OP 的目标:

The objective in this example is to add a column to df.A that shows the portion cost for the fruit selected by a particular person in a particular period.

恕我直言,实现 objective 的最佳方法是加入两次。

首先,创建一个 查找 table lut,其中包含每个 Fruit 和 [=18 的 portion.price =].然后,使用 update join 将列附加到 df.A:

library(data.table)
lut <- setDT(df.B)[df.C, on = .(id)][, portion.price := pound.per.portion * price.pound][]
setDT(df.A)[lut, on = .(Fruit, period), portion.price := i.portion.price][]
    period   Name  Fruit portion.price
 1:      1   John Banana     0.6446323
 2:      1   Paul  Apple     0.4365157
 3:      1  Ringo   Pear     0.9246186
 4:      1 George  Apple     0.4365157
 5:      2   John  Apple     0.4453343
 6:      2   Paul Banana     0.6596238
 7:      2  Ringo  Apple     0.4453343
 8:      2 George   Pear     0.9444602
 9:      3   John Banana     0.6746152
10:      3   Paul Banana     0.6746152
11:      3  Ringo Banana     0.6746152
12:      3 George  Apple     0.4585620

data.table 是为高效处理大型数据集而创建的。


或者,可以使用 SQL:

sqldf::sqldf("
select period, Name, Fruit, `portion.price` from `df.A` 
  left join (
    select Fruit, period, 
      `pound.per.portion` * `price.pound` as `portion.price` from `df.B`  
      join `df.C` using(id) 
       ) using(period, Fruit)
")
   period   Name  Fruit portion.price
1       1   John Banana     0.6446323
2       1   Paul  Apple     0.4365157
3       1  Ringo   Pear     0.9246186
4       1 George  Apple     0.4365157
5       2   John  Apple     0.4453343
6       2   Paul Banana     0.6596238
7       2  Ringo  Apple     0.4453343
8       2 George   Pear     0.9444602
9       3   John Banana     0.6746152
10      3   Paul Banana     0.6746152
11      3  Ringo Banana     0.6746152
12      3 George  Apple     0.4585620

请注意,某些 table 和列名称用反引号括起来,因为句号在 SQL

中具有特殊含义