基准测试与处理时间结果不一致

4

我一直在尝试测试数据框中最有效的替换NA值的方法。

我首先比较了针对1百万行、12列数据集的NA值和0值替换解决方案的效率。将所有管道可用的方案放入microbenchmark函数中,我得到了如下结果。

问题1:有没有办法在benchmark函数内测试子集左赋值语句(例如:df1[is.na(df1)] <- 0)?

library(dplyr)
library(tidyr)
library(microbenchmark)

set.seed(24)
df1 <- as.data.frame(matrix(sample(c(NA, 1:5), 1e6 *12, replace=TRUE),
                            dimnames = list(NULL, paste0("var", 1:12)), ncol=12))

op <- microbenchmark(
    mut_all_ifelse   = df1 %>% mutate_all(funs(ifelse(is.na(.), 0, .))),
    mut_at_ifelse    = df1 %>% mutate_at(funs(ifelse(is.na(.), 0, .)), .cols = c(1:12)),
    # df1[is.na(df1)] <- 0 would sit here, but I can't make it work inside this function
    replace          = df1 %>% replace(., is.na(.), 0),
    mut_all_replace  = df1 %>% mutate_all(funs(replace(., is.na(.), 0))),
    mut_at_replace   = df1 %>% mutate_at(funs(replace(., is.na(.), 0)), .cols = c(1:12)),
    replace_na       = df1 %>% replace_na(list(var1 = 0, var2 = 0, var3 = 0, var4 = 0, var5 = 0, var6 = 0, var7 = 0, var8 = 0, var9 = 0, var10 = 0, var11 = 0, var12 = 0)),
    times = 1000L
)

print(op) #standard data frame of the output
    Unit: milliseconds
            expr       min       lq     mean   median       uq       max neval
  mut_all_ifelse 769.87848 844.5565 871.2476 856.0941 895.4545 1274.5610  1000
   mut_at_ifelse 713.48399 847.0322 875.9433 861.3224 899.7102 1006.6767  1000
         replace 258.85697 311.9708 334.2291 317.3889 360.6112  455.7596  1000
 mut_all_replace  96.81479 164.1745 160.6151 167.5426 170.5497  219.5013  1000
  mut_at_replace  96.23975 166.0804 161.9302 169.3984 172.7442  219.0359  1000
      replace_na 103.04600 161.2746 156.7804 165.1649 168.3683  210.9531  1000
boxplot(op) #boxplot of output

Boxplot of Microbenchmark Base R, dplyr and tidyr Replaces

library(ggplot2) #nice log plot of the output
qplot(y=time, data=op, colour=expr) + scale_y_log10()

Microbenchmark Base R,dplyr和tidyr替换的彩色logY Time DotPlot

为了测试子集赋值运算符,我最初运行了这些测试。

set.seed(24) 
> Book1 <- as.data.frame(matrix(sample(c(NA, 1:5), 1e8 *12, replace=TRUE),
+ dimnames = list(NULL, paste0("var", 1:12)), ncol=12))
> system.time({ 
+     Book1 %>% mutate_all(funs(ifelse(is.na(.), 0, .))) })
   user  system elapsed 
  52.79   24.66   77.45 
> 
> system.time({ 
+     Book1 %>% mutate_at(funs(ifelse(is.na(.), 0, .)), .cols = c(1:12)) })
   user  system elapsed 
  52.74   25.16   77.91 
> 
> system.time({ 
+     Book1[is.na(Book1)] <- 0 })
   user  system elapsed 
  16.65    7.86   24.51 
> 
> system.time({ 
+     Book1 %>% replace_na(list(var1 = 0, var2 = 0, var3 = 0, var4 = 0, var5 = 0, var6 = 0, var7 = 0, var8 = 0, var9 = 0,var10 = 0, var11 = 0, var12 = 0)) })
   user  system elapsed 
   3.54    2.13    5.68 
> 
> system.time({ 
+     Book1 %>% mutate_at(funs(replace(., is.na(.), 0)), .cols = c(1:12)) })
   user  system elapsed 
   3.37    2.26    5.63 
> 
> system.time({ 
+     Book1 %>% mutate_all(funs(replace(., is.na(.), 0))) })
   user  system elapsed 
   3.33    2.26    5.58 
> 
> system.time({ 
+     Book1 %>% replace(., is.na(.), 0) })
   user  system elapsed 
   3.42    1.09    4.51 

在这些测试中,基本的replace()首先出现。在基准测试中,replace()排名较低,而tidyrreplace_na()微弱获胜。 重复运行单个测试,并在不同形状和大小的数据框上运行,始终发现基本的replace()领先。
问题2:为什么它的基准性能是唯一一个与简单测试结果如此不符的结果?
更加令人困惑的是 -
问题3:为什么所有的mutate_all/_at(replace())都比简单的replace()运行得快呢? 许多人报告了这个问题:http://datascience.la/dplyr-and-a-very-basic-benchmark/(以及那篇文章中的所有链接),但我还没有找到一个解释为什么会这样的原因,除了 hashing 和 C++ 的使用)
特别感谢 Tyler Rinker:https://www.r-bloggers.com/microbenchmarking-with-r/ 和 akrun:https://dev59.com/n57ha4cB1Zd3GeqPcwAp#41530071

1
尝试用 {} 包装它 -- { df1[is.na(df1)] <- 0 }。顺便提一下,注意 df1Book1 都是“整数”,在所有情况下你都在强制转换为“数字”。将 0 替换为 0L 应该可以提高速度。此外,在基准测试时,请注意 Book1[is.na(Book1)] <- 0 将实际的 Book1 替换为从“整数”到“数字”的强制转换的 Book1,而所有后续情况都具有不需要强制转换的优势。为了避免强制转换原始数据,请使用函数或 local 进行包装。最后,我认为一个有效的方法是 for(j in 1:ncol(df1)) df1[[j]][is.na(df1[[j]])] = 0L - alexis_laz
@alexis_laz:确实!这回答了大部分问题,帮助我开始看到可变对象在R中的工作方式和位置。您是否愿意将其放入答案中,以便我可以选择它?此外,您是否可以解释一下为什么您的for循环(即使没有简化子集)比其他选项快得多? - leerssej
1
我添加了一个更详细的答案。for循环是最快的替代方案之一,因为它只执行必须完成的最小操作来替换向量中的值。在for循环中发生的所有子集都只使用原始函数而不是“data.frame”方法进行 [[<-,这包括了显著的开销。唯一能够“击败”(但并不显著)循环内的这种一系列操作的事情就是就地修改;这是基本R不支持的。 - alexis_laz
谢谢!这是一组非常有趣的本地和函数封装介绍,清晰说明了尽可能使用整数的可观好处,以及简单循环如何真正提高速度。此外,写作也非常信息丰富和精心制作! - leerssej
1
不客气,很高兴你发现它有用。顺便提一下,“循环”实际上只包含“向量化”的操作。 “data.frame”是向量列表(字面上和作为R对象)。上述循环本质上等同于df1 [[1]] [is.na(df1 [[1]])] = 0L; df1 [[2]] [is.na(df1 [[2]])] = 0L; etc ...但包装为方便且合理的代码在“for”循环中。无论是用户代码、R函数还是内部代码,都必须对“data.frame”列向量进行迭代选择。 - alexis_laz
显示剩余2条评论
1个回答

4
您可以通过使用大括号 {} 将复杂/多语句包含在 microbenchmark 中,这基本上将其转换为单个表达式。
microbenchmark(expr1 = { df1[is.na(df1)] = 0 }, 
               exp2 = { tmp = 1:10; tmp[3] = 0L; tmp2 = tmp + 12L; tmp2 ^ 2 }, 
               times = 10)
#Unit: microseconds
#  expr        min         lq       mean     median         uq        max neval cld
# expr1 124953.716 137244.114 158576.030 142405.685 156744.076 284779.353    10   b
#  exp2      2.784      3.132     17.748     23.142     24.012     38.976    10  a 

值得注意的是,这可能会带来一些副作用:
tmp
#[1]  1  2  0  4  5  6  7  8  9 10

与之相比,例如:
rm(tmp)
microbenchmark(expr1 = { df1[is.na(df1)] = 0 },  
               exp2 = local({ tmp = 1:10; tmp[3] = 0L; tmp2 = tmp + 12L; tmp2 ^ 2 }), 
               times = 10)
#Unit: microseconds
#  expr       min         lq        mean     median         uq        max neval cld
# expr1 127250.18 132935.149 165296.3030 154509.553 169917.705 314820.306    10   b
#  exp2     10.44     12.181     42.5956     54.636     57.072     97.789    10  a 
tmp
#Error: object 'tmp' not found

注意到基准测试的副作用,我们可以看到第一个操作移除NA值,为后续的替代方案留下了一个相当轻松的任务:

# re-assign because we changed it before
set.seed(24)
df1 = as.data.frame(matrix(sample(c(NA, 1:5), 1e6 * 12, TRUE), 
                           dimnames = list(NULL, paste0("var", 1:12)), ncol = 12))
unique(sapply(df1, typeof))
#[1] "integer"
any(sapply(df1, anyNA))
#[1] TRUE
system.time({ df1[is.na(df1)] <- 0 })
# user  system elapsed 
# 0.39    0.14    0.53 

前面的基准测试留下了以下结论:
unique(sapply(df1, typeof))
#[1] "double"
any(sapply(df1, anyNA))
#[1] FALSE

当没有任何内容需要替换时,应该考虑不对输入进行任何操作。

除此之外,请注意在所有的备选方案中,您都将“double”(typeof(0))分配给“integer”列向量(sapply(df1, typeof))。虽然我不认为在上述备选方案中有任何情况下会在原地修改df1(因为在创建“data.frame”后,存储了复制其向量列的信息以便在修改时使用),但仍然存在一些小的、可避免的开销,即强制转换为“double”并将其存储为“double”。R在替换“integer”向量中的元素之前将分配和复制(在“integer”替换的情况下)或分配和强制转换(在“double”替换的情况下)。此外,在第一次强制转换后(R在一个基准测试的副作用中注意到),R将在“double”上操作,这包含比在“integer”上慢的操作。我找不到一个简单的R方法来研究这种差异,但简而言之(可能不是完全准确的),我们可以通过以下方式模拟这些操作:

# simulate R's copying of int to int
# allocate a new int and copy
int2int = inline::cfunction(sig = c(x = "integer"), body = '
    SEXP ans = PROTECT(allocVector(INTSXP, LENGTH(x)));
    memcpy(INTEGER(ans), INTEGER(x), LENGTH(x) * sizeof(int));
    UNPROTECT(1);
    return(ans);
')
# R's coercing of int to double
# 'coerceVector', internally, allocates a double and coerces to populate it
int2dbl = inline::cfunction(sig = c(x = "integer"), body = '
    SEXP ans = PROTECT(coerceVector(x, REALSXP));
    UNPROTECT(1);
    return(ans);
')
# simulate R's copying form double to double
dbl2dbl = inline::cfunction(sig = c(x = "double"), body = '
    SEXP ans = PROTECT(allocVector(REALSXP, LENGTH(x)));
    memcpy(REAL(ans), REAL(x), LENGTH(x) * sizeof(double));
    UNPROTECT(1);
    return(ans);
')

在基准测试中:

x.int = 1:1e7; x.dbl = as.numeric(x.int)
microbenchmark(int2int(x.int), int2dbl(x.int), dbl2dbl(x.dbl), times = 50)
#Unit: milliseconds
#           expr      min       lq     mean   median       uq      max neval cld
# int2int(x.int) 16.42710 16.91048 21.93023 17.42709 19.38547 54.36562    50  a 
# int2dbl(x.int) 35.94064 36.61367 47.15685 37.40329 63.61169 78.70038    50   b
# dbl2dbl(x.dbl) 33.51193 34.18427 45.30098 35.33685 63.45788 75.46987    50   b

总之,将0替换为0L可以节省一些时间...

最后,为了更公平地复制基准测试,我们可以使用:

library(dplyr)
library(tidyr)
library(microbenchmark) 
set.seed(24)
df1 = as.data.frame(matrix(sample(c(NA, 1:5), 1e6 * 12, TRUE), 
                            dimnames = list(NULL, paste0("var", 1:12)), ncol = 12))

将代码封装到函数中:

stopifnot(ncol(df1) == 12)  #some of the alternatives are hardcoded to 12 columns
mut_all_ifelse = function(x, val) x %>% mutate_all(funs(ifelse(is.na(.), val, .)))
mut_at_ifelse = function(x, val) x %>% mutate_at(funs(ifelse(is.na(.), val, .)), .cols = c(1:12))
baseAssign = function(x, val) { x[is.na(x)] <- val; x }
baseFor = function(x, val) { for(j in 1:ncol(x)) x[[j]][is.na(x[[j]])] = val; x }
base_replace = function(x, val) x %>% replace(., is.na(.), val)
mut_all_replace = function(x, val) x %>% mutate_all(funs(replace(., is.na(.), val)))
mut_at_replace = function(x, val) x %>% mutate_at(funs(replace(., is.na(.), val)), .cols = c(1:12))
myreplace_na = function(x, val) x %>% replace_na(list(var1 = val, var2 = val, var3 = val, var4 = val, var5 = val, var6 = val, var7 = val, var8 = val, var9 = val, var10 = val, var11 = val, var12 = val))

在性能基准测试之前测试结果的相等性:

identical(mut_all_ifelse(df1, 0), mut_at_ifelse(df1, 0))
#[1] TRUE
identical(mut_at_ifelse(df1, 0), baseAssign(df1, 0))
#[1] TRUE
identical(baseAssign(df1, 0), baseFor(df1, 0))
#[1] TRUE
identical(baseFor(df1, 0), base_replace(df1, 0))
#[1] TRUE
identical(base_replace(df1, 0), mut_all_replace(df1, 0))
#[1] TRUE
identical(mut_all_replace(df1, 0), mut_at_replace(df1, 0))
#[1] TRUE
identical(mut_at_replace(df1, 0), myreplace_na(df1, 0))
#[1] TRUE

测试强制转换为“double”:

benchnum = microbenchmark(mut_all_ifelse(df1, 0), 
                          mut_at_ifelse(df1, 0), 
                          baseAssign(df1, 0), 
                          baseFor(df1, 0),
                          base_replace(df1, 0), 
                          mut_all_replace(df1, 0),
                          mut_at_replace(df1, 0), 
                          myreplace_na(df1, 0),
                          times = 10)
benchnum
#Unit: milliseconds
#                    expr       min        lq      mean    median        uq       max neval cld
#  mut_all_ifelse(df1, 0) 1368.5091 1441.9939 1497.5236 1509.2233 1550.1416 1629.6959    10   c
#   mut_at_ifelse(df1, 0) 1366.1674 1389.2256 1458.1723 1464.5962 1503.4337 1553.7110    10   c
#      baseAssign(df1, 0)  532.4975  548.9444  586.8198  564.3940  655.8083  667.8634    10  b 
#         baseFor(df1, 0)  169.6048  175.9395  206.7038  189.5428  197.6472  308.6965    10 a  
#    base_replace(df1, 0)  518.7733  547.8381  597.8842  601.1544  643.4970  666.6872    10  b 
# mut_all_replace(df1, 0)  169.1970  183.5514  227.1978  194.0903  291.6625  346.4649    10 a  
#  mut_at_replace(df1, 0)  176.7904  186.4471  227.3599  202.9000  303.4643  309.2279    10 a  
#    myreplace_na(df1, 0)  172.4926  177.8518  199.1469  186.3645  192.1728  297.0419    10 a

不强制转换为“双精度”的测试:

benchint = microbenchmark(mut_all_ifelse(df1, 0L), 
                          mut_at_ifelse(df1, 0L), 
                          baseAssign(df1, 0L), 
                          baseFor(df1, 0L),
                          base_replace(df1, 0L), 
                          mut_all_replace(df1, 0L),
                          mut_at_replace(df1, 0L),
                          myreplace_na(df1, 0L),
                          times = 10)
benchint
#Unit: milliseconds
#                     expr        min        lq      mean    median        uq       max neval cld
#  mut_all_ifelse(df1, 0L) 1291.17494 1313.1910 1377.9265 1353.2812 1417.4389 1554.6110    10   c
#   mut_at_ifelse(df1, 0L) 1295.34053 1315.0308 1372.0728 1353.0445 1431.3687 1478.8613    10   c
#      baseAssign(df1, 0L)  451.13038  461.9731  477.3161  471.0833  484.9318  528.4976    10  b 
#         baseFor(df1, 0L)   98.15092  102.4996  115.7392  107.9778  136.2227  139.7473    10 a  
#    base_replace(df1, 0L)  428.54747  451.3924  471.5011  470.0568  497.7088  516.1852    10  b 
# mut_all_replace(df1, 0L)  101.66505  102.2316  137.8128  130.5731  161.2096  243.7495    10 a  
#  mut_at_replace(df1, 0L)  103.79796  107.2533  119.1180  112.1164  127.7959  166.9113    10 a  
#    myreplace_na(df1, 0L)  100.03431  101.6999  120.4402  121.5248  137.1710  141.3913    10 a

并且一个简单的可视化方式:

boxplot(benchnum, ylim = range(min(summary(benchint)$min, summary(benchnum)$min),
                               max(summary(benchint)$max, summary(benchnum)$max)))
boxplot(benchint, add = TRUE, border = "red", axes = FALSE) 
legend("topright", c("coerce", "not coerce"), fill = c("black", "red"))                       

请注意,所有操作完成后 df1 保持不变(str(df1))。

enter image description here


请注意,你的学生(即 OP)已经尝试在你的代码基础上进行了编写,但仍然对默默的强制转换以及 [[ 的作用感到困惑:https://dev59.com/D2sz5IYBdhLWcg3wBzbi#41585689/ 我已经厌倦了向他们解释所以情况,只是提醒您如有兴趣可以帮忙解答。 - Frank

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接