在 R 中矢量化替换子子列表元素

Vectorize replacement of sub-sub list elements in R

提问人:colebrookson 提问时间:3/10/2023 最后编辑:Mikael Jagancolebrookson 更新时间:3/11/2023 访问量:139

问:

我有一个有点复杂的数据结构(嵌套列表),定义为:y

x <- list(
  list(1, "a", 2, "b", 0.1),
  list(3, "c", 4, "d", 0.2),
  list(5, "e", 6, "f", 0.3)
)
y <- rep(list(x), 10)

我还有一个数据框,定义为:df

df <- data.frame(
  x1 = c( 0.33, 1.67, -0.62, -0.56,  0.17, 0.73,  0.59,  0.56, -0.22, 1.49),
  x2 = c(-0.82, 1.22,  0.65,  0.54, -2.26, 1.21, -0.44, -0.92, -0.56, 0.50),
  x3 = c(-0.16, 0.49, -0.82, -0.71,  0.13, 1.22,  1.23, -0.01, -1.11, 0.97)
)

其中列名不重要。

我想用 for all 和 替换 .我的 Python/Julia 大脑在循环中工作得最好,所以我通过循环 ,然后遍历 的元素(每个元素都是 的副本)来实现这一点,如下所示:y[[i]][[j]][[5]]df[[i, j]]ijyyx

for (i in seq_along(y)) {
  for (j in 1:3) {
    y[[i]][[j]][[5]] <- df[[i, j]]
  }
}

这可行,但对于我更大的数据集来说,它真的很慢。所以我正在尝试对嵌套循环进行矢量化。我一直在尝试:forMap()

y_new <- y
for (j in 1:3) {
  y_new <- Map(function(sublist, value) { sublist[[5]] <- value; sublist },
               y_new, df[, j])
}

但上述方法不起作用,因为返回.我想我缺少一个子集的水平。identical(y, y_new)FALSE

我根本没有结婚。我只是在寻找嵌套循环的最快替代方案。Map()for

R 循环矢 量化 嵌套列表

评论


答:

4赞 akrun 3/10/2023 #1

有了,我们可以做Map

y_new <- Map(\(u, v) Map(\(uu, vv) {uu[5] <- vv; uu}, u, v), y, asplit(df, 1))

- 使用 OP 的代码进行测试

> for(i in seq_len(length(y))) {
+   for(j in 1:3) {
+     y[[i]][[j]][[5]] <- df[[i, j]]
+   }
+ }
> 
> all.equal(y_new, y)
[1] TRUE

作为一种矢量化方法,一个选项是 to 和 assignunlist

v1 <- unlist(y)
v1[seq(5, length(v1), by = 5)] <- c(t(df))
y_new <- type.convert(relist(v1, skeleton = y), as.is = TRUE)

-检查

> for(i in seq_len(length(y))) {
+   for(j in 1:3) {
+     y[[i]][[j]][[5]] <- df[[i, j]]
+   }
+ }
> 
> all.equal(y, y_new)
[1] TRUE
3赞 TarJae 3/10/2023 #2

我们可以使用两次:map2

首先,我们用于将数据框转换为向量列表。第一个遍历然后遍历行向量列表,第二个将每个嵌套列表的第 5 个元素替换为 df 中的相应值。asplit(df, 1)dfmap2()ymap2()y

我们使用大括号和分号将多个表达式组合成一个表达式。{};

library(purrr)

map2(y, asplit(df, 1), ~ map2(.x, .y, ~ { .x[[5]] <- .y; .x }))
6赞 Mikael Jagan 3/10/2023 #3

@akrun 取消列表和重新列表的建议是优雅且高度惯用的(“R-like”)。

但我希望在没有强制的情况下做到这一点,尤其是从数字到字符再到字符,这可能会很慢并导致精度损失。这样的东西会更快、更安全:

unlist0 <- function(x) unlist(x, recursive = FALSE, use.names = FALSE)
split0 <- function(x, f) unname(split(x, f))

n <- length(y) # 10
n1 <- length(y[[1L]]) # 3
n11 <- length(y[[1L]][[1L]]) # 5

uy <- unlist0(unlist0(y))
uy[seq.int(n11, n * n1 * n11, n11)] <- as.list(t(df))
suy <- split0(split0(uy, gl(n * n1, n11)), gl(n, n1))

下面是一个基准:

unlist0 <- function(x) unlist(x, recursive = FALSE, use.names = FALSE)
split0 <- function(x, f) unname(split(x, f))

n <- length(y) # 10
n1 <- length(y[[1L]]) # 3
n11 <- length(y[[1L]][[1L]]) # 5

library(purrr)

microbenchmark::microbenchmark(
colebrookson =
    {
        ans <- y
        for (i in seq_len(n))
            for (j in seq_len(n1))
                ans[[i]][[j]][[n11]] <- df[[i, j]]
        ans
    },
TarJae =
    {
        map2(y, asplit(df, 1L), ~ map2(.x, .y, ~ { .x[[n11]] <- .y; .x }))
    },
akrun.1 = 
    {
        Map(function(u, v) Map(function(uu, vv) { uu[5L] <- vv; uu }, u, v), y, asplit(df, 1L))
    },
akrun.2 = 
    {
        uy <- unlist(y)
        uy[seq.int(n11, n * n1 * n11, n11)] <- c(t(df))
        type.convert(relist(uy, y), as.is = TRUE)
    },
`Mikael Jagan` =
    {
        uy <- unlist0(unlist0(y))
        uy[seq.int(n11, n * n1 * n11, n11)] <- as.list(t(df))
        split0(split0(uy, gl(n * n1, n11)), gl(n, n1))
    },
times = 1000L
)
Unit: microseconds
         expr      min        lq       mean    median       uq       max neval
 colebrookson 1116.635 1171.6365 1318.72441 1195.2115 1238.077 16936.567  1000
       TarJae  297.783  314.8390  365.66412  331.7105  352.026  1554.679  1000
      akrun.1   76.096   82.4305   96.82716   87.0840   91.676  2076.117  1000
      akrun.2 1206.343 1231.1685 1345.24661 1244.2270 1261.222  5023.197  1000
 Mikael Jagan   35.465   40.9590   51.61260   45.7765   50.594  1271.984  1000

一些评论:

  • 不要在小例子上阅读基准。
  • 我怀疑只有第四个和第五个答案才能很好地扩展,因为只有那些答案才能使替换矢量化。
  • 只有第四个答案与第一个答案不同,在 的意义上。由于精度损失,它有所不同。identical

评论

0赞 colebrookson 3/10/2023
好的,哇,这太棒了,非常感谢!我根本不会想到这种方法。只是出于我自己的好奇心,这个问题中是否有什么具体的东西让你想到将 / 作为一个函数来做?我不会猜到这么好奇你的思维过程是什么unlist0split0
2赞 Mikael Jagan 3/10/2023
问题在于,默认情况下 () 它会连接树的所有叶子,如果这些叶子具有不同的类型,则会导致强制。调用两次可确保我们只删除两层嵌套,留下一个保留其元素类型的列表。问题在于它假设我们确实执行了递归取消列表。因此,我们使用两次(具有不同的分组)来逐个反转调用,而无需引入名称,这本身就是如此。unlistrecursive = TRUEunlist0relistsplit0unlist0split
1赞 Mikael Jagan 3/10/2023
定义函数只是一种避免在想做不止一次的事情时重复自己的方法。DRY等(但你可能知道。