提问人:colebrookson 提问时间:3/10/2023 最后编辑:Mikael Jagancolebrookson 更新时间:3/11/2023 访问量:139
在 R 中矢量化替换子子列表元素
Vectorize replacement of sub-sub list elements in R
问:
我有一个有点复杂的数据结构(嵌套列表),定义为: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]]
i
j
y
y
x
for (i in seq_along(y)) {
for (j in 1:3) {
y[[i]][[j]][[5]] <- df[[i, j]]
}
}
这可行,但对于我更大的数据集来说,它真的很慢。所以我正在尝试对嵌套循环进行矢量化。我一直在尝试:for
Map()
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
答:
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)
df
map2()
y
map2()
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
好的,哇,这太棒了,非常感谢!我根本不会想到这种方法。只是出于我自己的好奇心,这个问题中是否有什么具体的东西让你想到将 / 作为一个函数来做?我不会猜到这么好奇你的思维过程是什么unlist0
split0
2赞
Mikael Jagan
3/10/2023
问题在于,默认情况下 () 它会连接树的所有叶子,如果这些叶子具有不同的类型,则会导致强制。调用两次可确保我们只删除两层嵌套,留下一个保留其元素类型的列表。问题在于它假设我们确实执行了递归取消列表。因此,我们使用两次(具有不同的分组)来逐个反转调用,而无需引入名称,这本身就是如此。unlist
recursive = TRUE
unlist0
relist
split0
unlist0
split
1赞
Mikael Jagan
3/10/2023
定义函数只是一种避免在想做不止一次的事情时重复自己的方法。DRY等(但你可能知道。
评论