不使用apply函数对data.table的每一行进行操作的方法

Reg*_*ser 5 r data.table

我写了一个简单的函数如下:

mcs <- function(v) { ifelse(sum((diff(sort(v)) > 6) > 0), NA, sd(v)) }

它应该采用一个向量,对其进行排序,然后检查每个连续差异中是否存在大于 6 的差异。如果差值大于 6,则返回 NA;如果差值不大于 6,则返回标准差。

我想将此函数应用于数据表的所有行(仅选择某些列),然后将每行的返回值作为新的列条目附加到数据表中。

例如,给定一个像这样的数据表

> dat <- data.table(A=c(1,2,3,4,5), B=c(2,3,4,10,6), C=c(3,4,10,6,8),   
D=c(3,3,3,3,3))  
> dat  
   A  B  C D  
1: 1  2  3 3  
2: 2  3  4 3  
3: 3  4 10 3  
4: 4 10  6 3  
5: 5  6  8 3  
Run Code Online (Sandbox Code Playgroud)

我想生成下面的输出。(我在每行的第 2、3 和 4 列上应用了函数。)

> dat
   A  B  C D        sd
1: 1  2  3 3 0.5773503
2: 2  3  4 3 0.5773503
3: 3  4 10 3 3.7859389
4: 4 10  6 3 3.5118846
5: 5  6  8 3 2.5166115
Run Code Online (Sandbox Code Playgroud)

我了解到可以使用以下方法对数据表进行按行操作:

> dat[, sd:=apply(.SD, 1, mcs), .SDcols=(c(2,3,4))]
Run Code Online (Sandbox Code Playgroud)

而且这个方法确实有效,只是速度太慢了。我必须对几个大型数据表执行此操作,并且我编写了一个脚本来执行此操作。但是,它仅适用于较小的数据表。对于大约 300,000 行的表,它会在几秒钟内完成,但是当我尝试对大约 8 亿行的表执行此操作时,我的程序无法完成。我尝试等待两个小时,我认为 R 坏了或者其他什么原因,因为控制台死机了。我已经尝试运行该脚本多次,它总是正确完成前几个较小的表(我让程序将表写入文件进行检查),但是当它到达大型数据表时,它永远不会完成。我在计算集群上运行它,所以我绝对不认为这是硬件限制。可能是糟糕的代码。

我假设瓶颈是在 apply 中完成的循环,但我不知道如何使其更快。我对 R 还很陌生,所以我不知道如何优化我的代码。我在互联网上看到了很多关于矢量化的帖子,我在想如果我可以同时将我的函数应用到每一行,它会快得多,但我不知道该怎么做。请帮忙。

编辑
抱歉,我在复制mcs函数时犯了一个错误。我已经更新了。

编辑2
对于那些感兴趣的人,我最终将表分成两半,并分别对每一半进行操作,这对我有用。

duc*_*ayr 6

如果您确实需要速度,一如既往,最好使用 Rcpp 迁移到 C++,这为我们提供了速度快 100 倍以上的解决方案。

数据

我确实制作了一些不同的示例数据来测试它,其中有 1000 行而不是 5 行:

set.seed(123)
dat <- data.table(A = rnorm(1e3, sd=4), B = rnorm(1e3, sd=4), C = rnorm(1e3, sd=4),
                  D = rnorm(1e3, sd=4), E = rnorm(1e3, sd=4))
Run Code Online (Sandbox Code Playgroud)

解决方案

我使用以下 C++ 代码执行与您的函数相同的操作,但现在循环是在 C++ 中完成的,而不是通过 apply 在 R 中完成,这节省了大量时间:

#include <Rcpp.h>

using namespace Rcpp;

// [[Rcpp::export]]
NumericVector mcs2(DataFrame x) {
    int n = x.nrows();
    int m = x.size();
    NumericMatrix mat(n, m);
    for ( int j = 0; j < m; ++j ) {
        mat(_, j) = NumericVector(x[j]);
    }
    NumericVector result(n);
    for ( int i = 0; i < n; ++i ) {
        NumericVector tmp = mat(i, _);
        std::sort(tmp.begin(), tmp.end());
        bool do_sd = true;
        for ( int j = 1; j < m; ++j ) {
            if ( tmp[j] - tmp[j-1] > 6.0 ) {
                result[i] = NA_REAL;
                do_sd = false;
                break;
            }
        }
        if ( do_sd ) {
            result[i] = sd(tmp);
        }
        do_sd = true;
    }
    return result;
}
Run Code Online (Sandbox Code Playgroud)

我们可以确保它返回相同的值:

all.equal(apply(dat[, 2:4], 1, mcs1), mcs2(dat[,2:4]))

[1] TRUE
Run Code Online (Sandbox Code Playgroud)

现在让我们进行基准测试:

benchmark(mcs1 = dat[, sd:=apply(.SD, 1, mcs1), .SDcols=(c(2,3,4))],
          mcs2 = dat[, sd:=mcs2(.SD), .SDcols=(c(2,3,4))],
          order = 'relative',
          columns = c('test', 'elapsed', 'relative', 'user.self'))


  test elapsed relative user.self
2 mcs2    0.19    1.000     0.183
1 mcs1   21.34  112.316    20.044
Run Code Online (Sandbox Code Playgroud)

如何编译这段代码

作为通过 Rcpp 使用 C++ 代码的介绍,我建议您阅读Hadley Wickham 的 Advanced R 的这一章。如果您打算使用 Rcpp 做进一步的事情,我强烈建议您还阅读官方文档和小插图,但 Wickham 的书可能是作为起点更适合初学者使用。出于您的目的,您只需启动并运行 Rcpp,以便可以编译上面的代码。

为了使此代码适合您,您需要 Rcpp 包(如果您还没有)。您可以通过运行来获取该包

install.packages(Rcpp)
Run Code Online (Sandbox Code Playgroud)

来自 R。请注意,您还需要一个编译器;如果您使用的是基于 Debian 的 Linux 系统(例如 Ubuntu),您可以运行

sudo apt install r-base-dev
Run Code Online (Sandbox Code Playgroud)

从航站楼。如果您使用的是 Mac 或 Windows,请在此处查看有关进行此设置的一些说明,或者查看上面链接的 Wickham 章节。

安装 Rcpp 后,将上面的 C++ 代码保存到文件中。假设我们的示例中的文件名为“SOanswer.cpp”。然后,您可以mcs2()通过在 R 脚本中添加以下两行来使其函数在 R 中可用:

library(Rcpp)
sourceCpp("SOanswer.cpp") # assuming the file is in your working directory
Run Code Online (Sandbox Code Playgroud)

就是这样!mcs2()现在,您的 R 脚本可以更快地调用和运行。如果您想了解有关 Rcpp 的更多信息,除了上面的 Wickham 章节之外,我还可以查看此处提供的参考手册和小插图页面来自 RStudio(也有大量链接,其中一些链接到此处),您还可以在Rcpp 画廊中找到一些真正有用的东西。