【问题标题】:Speeding up linear model fitting on complete pairwise observations in large sparse matrix in R在R中的大稀疏矩阵中加速线性模型拟合完整的成对观察
【发布时间】:2020-01-21 17:30:13
【问题描述】:

我有一个数字 data.frame df 有 134946 行 x 1938 列。
99.82% 的数据是NA
对于每一对(不同的)列"P1""P2",我需要找出哪些行的值都不是NA,然后对这些行进行一些操作(线性模型)。

我写了一个脚本来做这个,但是看起来很慢。

This post 似乎在讨论一项相关任务,但我无法立即看到它是否或如何适应我的情况。

借用那篇文章中的例子:

set.seed(54321)
nr = 1000;
nc = 900;
dat = matrix(runif(nr*nc), nrow=nr)
rownames(dat) = paste(1:nr)
colnames(dat) = paste("time", 1:nc)
dat[sample(nr*nc, nr*nc*0.9)] = NA

df <- as.data.frame(dat)
df_ps <- names(df)
N_ps <- length(df_ps)

我的脚本是:

tic = proc.time()

out <- do.call(rbind,sapply(1:(N_ps-1), function(i) {
  if (i/10 == floor(i/10)) {
    cat("\ni = ",i,"\n")
    toc = proc.time();
    show(toc-tic);
  }
  do.call(rbind,sapply((i+1):N_ps, function(j) {
    w <- which(complete.cases(df[,i],df[,j]))
    N <- length(w)
    if (N >= 5) {
      xw <- df[w,i]
      yw <- df[w,j]
      if ((diff(range(xw)) != 0) & (diff(range(yw)) != 0)) {
        s <- summary(lm(yw~xw))
        o <- c(i,j,N,s$adj.r.squared,s$coefficients[2],s$coefficients[4],s$coefficients[8],s$coefficients[1],s$coefficients[3],s$coefficients[7])} else {
          o <- c(i,j,N,rep(NA,7))
        }
    } else {o <- NULL}
    return(o)
  },simplify=F))

}
,simplify=F))

toc = proc.time();
show(toc-tic);

这在我的机器上大约需要 10 分钟。
您可以想象当我需要处理更大(尽管更稀疏)的数据矩阵时会发生什么。我从来没有完成计算。

问题:您认为这可以更有效地完成吗?

问题是我不知道哪些操作需要更多时间(df 的子集,在这种情况下我会删除重复项?附加矩阵数据,在这种情况下我会创建一个平面向量然后转换它到最后的矩阵?...)。

谢谢!


编辑跟进我的帖子

如 minem 所示,此计算的速度很大程度上取决于计算线性回归参数的方式。因此,更改该部分是最重要的事情。
我自己的进一步试验表明:1) 必须将sapplydo.call(rbind 结合使用,而不是任何平面向量来存储数据(我仍然不确定为什么 - 我可能会就此单独发表一篇文章); 2) 在我正在处理的原始矩阵上,使用每个i 迭代开始时可用的x 向量信息,比本示例中的矩阵更稀疏且nrows/ncolumns 比率更大在每次 j 迭代开始时减少 y 向量,速度提高了几个数量级,即使与 minem 的原始脚本相比,这已经比我上面的要好得多。
我认为优势来自于先验过滤掉许多行,从而避免了对非常长的向量进行昂贵的 xna &amp; yna 操作。

修改后的脚本如下:

set.seed(54321)
nr = 1000;
nc = 900;
dat = matrix(runif(nr*nc), nrow = nr)
rownames(dat) = paste(1:nr)
colnames(dat) = paste("time", 1:nc)
dat[sample(nr*nc, nr*nc*0.90)] = NA

df <- as.data.frame(dat)
df_ps <- names(df)
N_ps <- length(df_ps)

tic = proc.time()

naIds <- lapply(df, function(x) !is.na(x))
dl <- as.list(df)

rl <- sapply(1:(N_ps - 1), function(i) {
  if ((i-1)/10 == floor((i-1)/10)) {
    cat("\ni = ",i,"\n")
    toc = proc.time();
    show(toc-tic);
  }
  x <- dl[[i]]
  xna <- which(naIds[[i]])
  rl2 <- sapply((i + 1):N_ps, function(j) {
    y <- dl[[j]][xna]
    yna <- which(naIds[[j]][xna])
    w <- xna[yna]
    N <- length(w)
    if (N >= 5) {
      xw <- x[w]
      yw <- y[yna]

      if ((min(xw) != max(xw)) && (min(yw) != max(yw))) {

        # extracts from lm/lm.fit/summary.lm  functions
        X <- cbind(1L, xw)
        m <- .lm.fit(X, yw)

        # calculate adj.r.squared
        fitted <- yw - m$residuals
        rss <- sum(m$residuals^2)
        mss <- sum((fitted - mean(fitted))^2)
        n <- length(m$residuals)
        rdf <- n - m$rank
        # rdf <- df.residual
        r.squared <- mss/(mss + rss)
        adj.r.squared <- 1 - (1 - r.squared) * ((n - 1L)/rdf)

        # calculate se & pvals
        p1 <- 1L:m$rank
        Qr <- m$qr
        R <- chol2inv(Qr[p1, p1, drop = FALSE])
        resvar <- rss/rdf
        se <- sqrt(diag(R) * resvar)
        est <- m$coefficients[m$pivot[p1]]
        tval <- est/se
        pvals <- 2 * pt(abs(tval), rdf, lower.tail = FALSE)
        res <- c(m$coefficients[2], se[2], pvals[2],
                 m$coefficients[1], se[1], pvals[1])
        o <- c(i, j, N, adj.r.squared, res)
      } else {
        o <- c(i,j,N,rep(NA,7))
      }
    } else {o <- NULL}
    return(o)
  }, simplify = F)
  do.call(rbind, rl2)
}, simplify = F)
out2 <- do.call(rbind, rl)

toc = proc.time();
show(toc - tic)

例如试试nr=100000; nc=100

我可能应该提到我尝试使用索引,即:

naIds <- lapply(df, function(x) which(!is.na(x)))

然后显然通过交集生成w

w <- intersect(xna,yna)
N <- length(w)

然而,这比上面的要慢。

【问题讨论】:

  • 首先,如果你只有数字数据,我真的建议存储为矩阵,而不是数据框。其次,如果你的数据非常稀疏,为什么不将其存储为稀疏矩阵呢?
  • 好的,这两种更改都是可能的。您认为这将有助于加快进程吗?

标签: r performance


【解决方案1】:

较大的瓶颈是lm 函数,因为有很多检查和额外的计算,你不一定需要。所以我只提取了需要的部分。 我让它在 +/- 18 秒内运行。

set.seed(54321)
nr = 1000;
nc = 900;
dat = matrix(runif(nr*nc), nrow = nr)
rownames(dat) = paste(1:nr)
colnames(dat) = paste("time", 1:nc)
dat[sample(nr*nc, nr*nc*0.9)] = NA

df <- as.data.frame(dat)
df_ps <- names(df)
N_ps <- length(df_ps)


tic = proc.time()

naIds <- lapply(df, function(x) !is.na(x)) # outside loop
dl <- as.list(df) # sub-setting list elements is faster that columns

rl <- sapply(1:(N_ps - 1), function(i) {
  x <- dl[[i]]
  xna <- naIds[[i]] # relevant logical vector if not empty elements
  rl2 <- sapply((i + 1):N_ps, function(j) {
    y <- dl[[j]]
    yna <- naIds[[j]]
    w <- xna & yna
    N <- sum(w)
    if (N >= 5) {
      xw <- x[w]
      yw <- y[w]

      if ((min(xw) != max(xw)) && (min(xw) != max(xw))) { # faster

        # extracts from lm/lm.fit/summary.lm  functions
        X <- cbind(1L, xw)
        m <- .lm.fit(X, yw)

        # calculate adj.r.squared
        fitted <- yw - m$residuals
        rss <- sum(m$residuals^2)
        mss <- sum((fitted - mean(fitted))^2)
        n <- length(m$residuals)
        rdf <- n - m$rank
        # rdf <- df.residual
        r.squared <- mss/(mss + rss)
        adj.r.squared <- 1 - (1 - r.squared) * ((n - 1L)/rdf)

        # calculate se & pvals
        p1 <- 1L:m$rank
        Qr <- m$qr
        R <- chol2inv(Qr[p1, p1, drop = FALSE])
        resvar <- rss/rdf
        se <- sqrt(diag(R) * resvar)
        est <- m$coefficients[m$pivot[p1]]
        tval <- est/se
        pvals <- 2 * pt(abs(tval), rdf, lower.tail = FALSE)
        res <- c(m$coefficients[2], se[2], pvals[2],
                 m$coefficients[1], se[1], pvals[1])
        o <- c(i, j, N, adj.r.squared, res)
        } else {
          o <- c(i,j,N,rep(NA,6))
          }
    } else {o <- NULL}
    return(o)
  }, simplify = F)
  do.call(rbind, rl2)
}, simplify = F)
out2 <- do.call(rbind, rl)

toc = proc.time();
show(toc - tic);
#  user  system elapsed 
# 17.94    0.11   18.44

【讨论】:

  • 太好了,谢谢@minem!事实上,我后来意识到我的代码中有错误(rep(NA,6) 应该是rep(NA,7))。通过其他试验,我发现通过收集x数据的非NA记录的索引并使用这些索引只查看y数据的相关记录,可以获得另一个很大的性能提升。我稍后会发布编辑。
  • 实际上,更正:按我说的做只会改进我更大的集合的计算,而不是这个例子。我想这是因为我的矩阵更加稀疏。另一方面,你是绝对正确的,lm 部分是杀死它的原因。作为测试,我尝试更改脚本中的一些内容,我可以报告以下内容。原始 = 22 秒。递归子集 = 22 秒。两个嵌套的 for 循环而不是 sapply = 20 s 用于单独的计算,但 非常 存储数据的速度较慢 (???) = ~10 分钟。 lm 与最初一样,但 sapply = ~10 分钟。
  • 再次感谢您,这看起来很有用!我知道一个事实,如果我将每次迭代生成的o 的值附加到外部变量Results,这会大大减慢进程。以这种方式收集数据而不是让do.call(rbind,sapply 的输出上完成工作之间肯定存在根本的不同。我认为这可能值得单独发布。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-11-28
  • 1970-01-01
  • 1970-01-01
  • 2019-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多