让\xe2\x80\x99s 制作一个简单的数据框并为其赋予属性\xe2\x80\x9cfoo\xe2\x80\x9d:
\norig <- data.frame(x1 = 1, x2 = 2)\nattr(orig, "foo") <- TRUE\nRun Code Online (Sandbox Code Playgroud)\n\xe2\x80\x9cfoo\xe2\x80\x9d 在那里:
\nattributes(orig)\n#> $names\n#> [1] "x1" "x2"\n#> \n#> $class\n#> [1] "data.frame"\n#> \n#> $row.names\n#> [1] 1\n#> \n#> $foo\n#> [1] TRUE\nRun Code Online (Sandbox Code Playgroud)\n但是如果我重新排序列, \xe2\x80\x9cfoo\xe2\x80\x9d 就会消失
\nnew <- orig[, c(2, 1)]\nattributes(new)\n#> $names\n#> [1] "x2" "x1"\n#> \n#> $class\n#> [1] "data.frame"\n#> \n#> $row.names\n#> [1] 1\nRun Code Online (Sandbox Code Playgroud)\n我可以用以下命令将其添加回来:
\nattributes(new) <- utils::modifyList(attributes(orig), attributes(new))\nattributes(new)\n#> $names\n#> [1] "x2" "x1"\n#> \n#> $class\n#> [1] "data.frame"\n#> \n#> $row.names\n#> [1] 1\n#> \n#> $foo\n#> [1] TRUE\nRun Code Online (Sandbox Code Playgroud)\n但这个操作很耗时。不是在这种情况下,因为它\xe2\x80\x99是一个\none-row数据帧,但考虑这种情况有10,000,000行:
\norig <- data.frame(x1 = rep(1, 1e7), x2 = rep(2, 1e7))\nattr(orig, "foo") <- TRUE\nnew <- orig[, c(2, 1)]\n\nbench::mark(\n test = {\n attributes(new) <- utils::modifyList(attributes(orig), attributes(new))\n }\n)\n#> # A tibble: 1 \xc3\x97 6\n#> expression min median `itr/sec` mem_alloc `gc/sec`\n#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>\n#> 1 test 43.2ms 46.6ms 21.6 38.1MB 14.4\nRun Code Online (Sandbox Code Playgroud)\n当然,完成此操作并不需要太多时间,但它比第一种情况要长得多(只需要几微秒)。对我来说,向数据帧添加单个属性所需的时间随着数据帧的大小而增加,这似乎很奇怪。我错过了什么吗?是否有更有效的方法将“简单”属性列表添加到大型数据框中?
\n编辑:寻找仅包含基本 R 的解决方案
\nSam*_*amR 11
我已经广泛更新了这个答案,因为我意识到我使用tracemem()考虑对象的内存位置而不考虑对象大小得出了错误的结论。相反,我使用辅助函数来创建输出的简化树表示,lobstr::sxp(dat)以显示对象在内存中的表示方式。结论是,没有必要预先分配row.names,并且只需避免复制它们,如 Joris C. 的出色答案中所述就足够了。
当您创建行数据框n而未显式声明行名称时,行名称将存储为长度为 2 的整数向量,格式为c(NA, -n).
如果将行名称属性从一个数据帧复制到另一个数据帧,R 会计算该向量以便复制它。绝对不应该这样做。
\n或者,您可以使用data.table或tidyverse,两者在创建副本时都会保留属性,从而避免复制任何内容。
让我们创建一个包含 10 行的数据框。
\nnum_rows <- 10\nset.seed(0)\ndat <- data.frame(\n x_char = sample(letters, num_rows),\n x_int = sample(1:10, num_rows)\n)\nRun Code Online (Sandbox Code Playgroud)\n让我们看看它在内存中是如何出现的:
\nlibrary(lobstr)\ndat_sxp <- sxp(dat)\nget_dat_obj_tree(dat_sxp)\n1 dat VECSXP length: 2 mem_addr:0x7\n2 \xc2\xa6--x_char STRSXP length: 10 mem_addr:0x1\n3 \xc2\xa6--x_int INTSXP length: 10 mem_addr:0x2\n4 \xc2\xb0--_attrib LISTSXP length: 3 mem_addr:0x3\n5 \xc2\xa6--names STRSXP length: 2 mem_addr:0x4\n6 \xc2\xa6--class STRSXP length: 1 mem_addr:0x5\n7 \xc2\xb0--row.names INTSXP length: 2 mem_addr:0x6\nRun Code Online (Sandbox Code Playgroud)\n该函数用唯一的整数替换内存地址(即每次查找实际地址时mem_addr:0x1都会保留地址,除非内存位置实际发生变化)。x_charx_char
我们期望数据的长度为 10。但是为什么row.names长度只有 2?让我们打印它们:
rownames(dat) # "1" "2" "3" "4" "5" "6" "7" "8" "9" "10"\nattr(dat, "row.names") # 1 2 3 4 5 6 7 8 9 10\nRun Code Online (Sandbox Code Playgroud)\n显然,这些是长度为 10 的向量。您可能会注意到,一个是字符向量,一个是整数向量。这让我陷入了很多死胡同,直到我在 R 源代码中发现了这样的注释:
\n## As from R 2.4.0, row.names can be either character or integer.\n## row.names() will always return character.\n## attr(, "row.names") will return either character or integer.\n##\n## Do not assume that the internal representation is either, since\n## 1L:n is stored as the integer vector c(NA, n) to save space (and\n## the C-level code to get/set the attribute makes the appropriate\n## translations.\nRun Code Online (Sandbox Code Playgroud)\n这让我想起了您在可重现示例中经常看到的东西:
\ndput(dat)\n# structure(list(x_char = c("e", "i", "n", "z", "w", "b", "j",\n# "l", "o", "a"), x_int = c(4L, 3L, 6L, 2L, 7L, 10L, 5L, 8L, 9L,\n# 1L)), class = "data.frame", row.names = c(NA, -10L))\nRun Code Online (Sandbox Code Playgroud)\n我们看到行名称确实表示为长度为 2 的向量row.names = c(NA, -10L)。这是理解如何避免昂贵的复制操作的关键。
事实并非如此。它只是创建了一种您更有可能复制行名称的情况,因为不会在每次操作后复制属性。R 内部声明:
\n\n\n子集化(除了空索引之外)通常会删除除名称、暗淡和暗淡名称之外的所有属性,这些属性会根据需要进行重置。
\n
让我们创建一个新属性 ,foo看看内存中发生了什么:
attr(dat, "foo") <- TRUE\nRun Code Online (Sandbox Code Playgroud)\n我们看一下内部表示:
\ndat_foo_sxp <- sxp(dat)\nget_dat_obj_tree(dat_foo_sxp)\n1 dat VECSXP length: 2 mem_addr:0x7\n2 \xc2\xa6--x_char STRSXP length: 10 mem_addr:0x1\n3 \xc2\xa6--x_int INTSXP length: 10 mem_addr:0x2\n4 \xc2\xb0--_attrib LISTSXP length: 4 mem_addr:0x3\n5 \xc2\xa6--names STRSXP length: 2 mem_addr:0x4\n6 \xc2\xa6--class STRSXP length: 1 mem_addr:0x5\n7 \xc2\xa6--row.names INTSXP length: 2 mem_addr:0x6\n8 \xc2\xb0--foo LGLSXP length: 1 mem_addr:0x8\nRun Code Online (Sandbox Code Playgroud)\n内存中没有任何真正的改变 - 属性类只是有一个类型为 的新节点LGLSXP,即逻辑向量。
让我们重新排序列。
\nnew <- dat[, c(2,1)]\nRun Code Online (Sandbox Code Playgroud)\n尽管我们选择了所有列,但我们本质上是按索引对数据进行子集化。我们看一下该对象在内存中的节点:
\nnew_sxp <- sxp(new)\nget_dat_obj_tree(new_sxp, "new")\n1 new VECSXP length: 2 mem_addr:0x12\n2 \xc2\xa6--x_int INTSXP length: 10 mem_addr:0x2\n3 \xc2\xa6--x_char STRSXP length: 10 mem_addr:0x1\n4 \xc2\xb0--_attrib LISTSXP length: 3 mem_addr:0x9\n5 \xc2\xa6--names STRSXP length: 2 mem_addr:0x10\n6 \xc2\xa6--class STRSXP length: 1 mem_addr:0x5\n7 \xc2\xb0--row.names INTSXP length: 2 mem_addr:0x11\nRun Code Online (Sandbox Code Playgroud)\n这基本上是我们对延迟评估副本的期望,除了row.names,它没有改变,但有一个新的内存地址:
integer是相同的。character是相同的。names有一个新的新位置(因为它已重新排序)。class相同的地址。row.names一个新的内存地址。也许 R 可以将其保留row.names在同一内存位置。毕竟,我们只是对列进行子集化,因此行的数量和顺序不会改变。
然而,这就是为什么我之前预分配行名称的建议是错误的,事实上,有新的行名称row.names并不会显着影响执行时间。R 正在创建一个长度为 2 的新整数向量,无论数据大小如何。这几乎不需要时间。为了避免如此微小的操作,可能不值得向 R 源添加逻辑来确定行是否相同。
在您的示例以及 Joris C. 的回答中值得注意的是,如果操作attr(new, "row.names") <- attr(dat, "row.names")单独或作为较大函数调用(例如 )的一部分包含 ,则需要更长的时间utils::modifyList(attributes(dat), attributes(new))。让我们尝试一下简单的方法:
attr(new, "row.names") <- attr(dat, "row.names")\nget_dat_obj_tree(sxp(new))\n1 dat VECSXP length: 2 mem_addr:0x15\n2 \xc2\xa6--x_int INTSXP length: 10 mem_addr:0x2\n3 \xc2\xa6--x_char STRSXP length: 10 mem_addr:0x1\n4 \xc2\xb0--_attrib LISTSXP length: 3 mem_addr:0x13\n5 \xc2\xa6--names STRSXP length: 2 mem_addr:0x10\n6 \xc2\xa6--class STRSXP length: 1 mem_addr:0x5\n7 \xc2\xb0--row.names INTSXP length: 2 mem_addr:0x14\nRun Code Online (Sandbox Code Playgroud)\n有一个新的内存地址。但row.names的属性new仍然是一个长度为 2 的整数向量。如果我们运行,dput(new)我们会看到row.names = c(NA, -10L)。
因此,如果我们将长度为 2 的整数向量从一个地方复制到另一个地方,无论数据大小如何,为什么对于较大的数据帧会花费更长的时间?答案是当你运行时会发生什么:
\nattr(new, "row.names") <- attr(dat, "row.names")\nRun Code Online (Sandbox Code Playgroud)\n这是语法糖:
\nnew <- `attr<-`(new, "row.names", attr(dat, "row.names"))\nRun Code Online (Sandbox Code Playgroud)\n首先,这意味着我们正在评估row.namesfor dat。其次,正如R 内部人士指出的,有一个类似的例子,a <- `dim<-`(a, c(7, 2)):
\n\n原则上两份
\na原则上在计算期间存在
所以这可能会发生两次。
\n理解这一点的一种更简单的方法是打印该函数调用的右侧。
\n`attr<-`(new, "row.names", attr(dat, "row.names")) \n# <truncated>\n# attr(,"row.names")\n# [1] 1 2 3 4 5 6 7 8 9 10\nRun Code Online (Sandbox Code Playgroud)\n当它们row.names存储在attrib.cnew中的 R 源代码中时,它足够聪明,可以将其恢复为c(NA, n)以下形式:
INTEGER(val)[0] = NA_INTEGER;\nINTEGER(val)[1] = n; // +n: compacted *and* automatic row names\nRun Code Online (Sandbox Code Playgroud)\n然而,损害已经造成,简短的形式c(NA, -10)行名称已被完全评估,正如您所期望的(并且已经证明的),对于较长的行名称向量需要更多的时间。
data.table在基础 R 以及和包中可以避免这个问题tidyverse。
要点是 - 不要将行名称从一个数据框复制到另一个数据框。Joris C. 建议的函数复制子集操作未复制的任何属性,而不是复制所有属性,是一个很好的基础 R 解决方案。
\n另一种方法是将数据框转换为 adata.table并使用data.table::setattr()通过引用设置属性:
library(data.table)\norig <- data.frame(x1 = 1, x2 = 2)\nsetDT(orig)\n\nmem_location <- tracemem(orig)\n\nsetattr(orig, "foo", TRUE)\n\ntracemem(orig) == mem_location # TRUE\n\nattr(orig, "foo") # TRUE\nRun Code Online (Sandbox Code Playgroud)\n此外,data.table您可以通过引用更改列顺序,这样在对列重新排序时就不会丢失属性:
setcolorder(orig, c(2,1))\nattr(orig, "foo") # TRUE\n\norig\n# x2 x1\n# 1: 2 1\nRun Code Online (Sandbox Code Playgroud)\n类似地,当您对列进行子集化时,atibble()会保留其row.names属性:
library(tibble)\n\nset.seed(0)\nnum_rows <- 10\ndat <- tibble(\n x_char = sample(letters, num_rows),\n x_int = sample(1:10, num_rows)\n) \n\nattr(dat, "foo") <- TRUE\n\nnew <- dat[,c(2,1)]\n\nattr(new, "foo") # TRUE\nRun Code Online (Sandbox Code Playgroud)\n我在这个问题上陷入了几个死胡同,并发布了两个不太正确的答案,然后我才明白幕后到底发生了什么。但在这个过程中我学到了很多关于 R 的知识。感谢您提出如此有趣的问题。
\ndata.frame复制所有属性的计算时间随着 的大小而变化的原因data.frame似乎主要是由于row.names属性。
我们可以检查复制row.names属性占用了大部分计算时间:
orig <- data.frame(x1 = rep(1, 1e7), x2 = rep(2, 1e7))
attr(orig, "foo") <- TRUE
new <- orig[, c(2, 1)]
microbenchmark::microbenchmark(
all_attrs = { attributes(new) <- attributes(orig) },
rownames = { attr(new, "row.names") <- attr(orig, "row.names") },
foo = { attr(new, "foo") <- attr(orig, "foo") },
times = 10,
unit = "ms"
)
#> Unit: milliseconds
#> expr min lq mean median uq max neval
#> all_attrs 60.477554 61.18414 64.3562408 61.9978505 67.117645 72.827139 10
#> rownames 59.831147 61.21029 69.6012781 64.2950890 68.880676 106.280348 10
#> foo 0.001043 0.00206 0.0072771 0.0087225 0.011206 0.015295 10
Run Code Online (Sandbox Code Playgroud)
foo如果我们将其与在较小的情况下复制属性进行比较data.frame,则时间(大致)具有相同的顺序:
orig <- data.frame(x1 = 1, x2 = 2)
attr(orig, "foo") <- TRUE
new <- orig[, c(2, 1)]
microbenchmark::microbenchmark(
foo = { attr(new, "foo") <- attr(orig, "foo") },
unit = "ms"
)
#> Unit: milliseconds
#> expr min lq mean median uq max neval
#> foo 0.00115 0.00118 0.00146262 0.0012055 0.0012725 0.022368 100
Run Code Online (Sandbox Code Playgroud)
为了提高效率,您可以选择仅复制任何自定义定义的属性(而不是所有data.frame属性)。例如:
## replace only custom attributes
replace_attrs <- function(obj, new_attrs) {
for(nm in setdiff(names(new_attrs), names(attributes(data.frame())))) {
attr(obj, which = nm) <- new_attrs[[nm]]
}
return(obj)
}
new <- replace_attrs(new, attributes(orig))
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
284 次 |
| 最近记录: |