【问题标题】:Optimization: splitting dataframe into a list of dataframes, transforming data per row优化:将数据框拆分为数据框列表,每行转换数据
【发布时间】:2013-04-02 18:53:12
【问题描述】:

预备题:这个问题主要具有教育价值,手头的实际任务已经完成,即使方法不是完全最优的。我的问题是下面的代码是否可以优化速度和/或更优雅地实现。也许使用其他软件包,例如 plyr 或 reshape。在实际数据上运行大约需要 140 秒,远高于模拟数据,因为一些原始行只包含 NA,并且必须进行额外的检查。为了比较,模拟数据的处理时间约为 30 秒。

条件:数据集包含 360 个变量,是 12 个变量的 30 倍。我们将它们命名为 V1_1、V1_2...(第一组)、V2_1、V2_2...(第二组)和等等。每组 12 个变量包含二分法(是/否)响应,在实践中对应于职业状态。例如:工作(是/否)、学习(是/否)等,共12种状态,重复30次。

任务:手头的任务是将每组 12 个二分变量重新编码为具有 12 个响应类别(例如工作、学习...)的单个变量。最终我们应该得到 30 个变量,每个变量都有 12 个响应类别。

数据:我无法发布实际数据集,但这是一个很好的模拟近似值:

randomRow <- function() {
  # make a row with a single 1 and some NA's
  sample(x=c(rep(0,9),1,NA,NA),size=12,replace=F) 
}

# create a data frame with 12 variables and 1500 cases
makeDf <- function() {
  data <- matrix(NA,ncol=12,nrow=1500)
  for (i in 1:1500) {
    data[i,] <- randomRow()
  }
  return(data)
}

mydata <- NULL

# combine 30 of these dataframes horizontally
for (i in 1:30) {
  mydata <- cbind(mydata,makeDf())
}
mydata <- as.data.frame(mydata) # example data ready

我的解决方案

# Divide the dataset into a list with 30 dataframes, each with 12 variables
S1 <- lapply(1:30,function(i) {
  Z <- rep(1:30,each=12) # define selection vector
  mydata[Z==i]           # use selection vector to get groups of variables (x12)
})

recodeDf <- function(df) {
  result <- as.numeric(apply(df,1,function(x) {
    if (any(!is.na(df))) which(x == 1) else NA # return the position of "1" per row
  }))                                          # the if/else check is for the real data
  return(result)
}
# Combine individual position vectors into a dataframe
final.df <- as.data.frame(do.call(cbind,lapply(S1,recodeDf)))

总而言之,有一个双 *apply 函数,一个跨列表,另一个跨数据框行。这使它有点慢。有什么建议?提前致谢。

【问题讨论】:

  • (+1) 框架很好的问题。

标签: r optimization apply


【解决方案1】:

IIUC,每 12 列只有一个 1。剩下的都是 0 或 NA。如果是这样,通过这个想法可以更快地执行操作。

想法:您可以使用维度为1500 * 12 的矩阵,其中每行只是1:12,而不是遍历每一行并询问1 的位置。那就是:

mul.mat <- matrix(rep(1:12, nrow(DT)), ncol = 12, byrow=TRUE)

现在,您可以将此矩阵与您的每个子集的data.frame(相同尺寸,此处为 1500*12)相乘,然后将它们的“rowSums”(矢量化)与na.rm = TRUE 相乘。这只会直接给出你有 1 的行(因为 1 将乘以 1 到 12 之间的对应值)。


data.table 实现:在这里,我将使用data.table 来说明这个想法。由于它通过引用创建列,我希望在 data.frame 上使用的相同想法会慢一点,尽管它应该会大大加快您当前的代码。

require(data.table)
DT <- data.table(mydata)
ids <- seq(1, ncol(DT), by=12)

# for multiplying with each subset and taking rowSums to get position of 1
mul.mat <- matrix(rep(1:12, nrow(DT)), ncol = 12, byrow=TRUE)

for (i in ids) {
    sdcols <- i:(i+12-1)
    # keep appending the new columns by reference to the original data
    DT[, paste0("R", i %/% 12 + 1) := rowSums(.SD * mul.mat, 
                     na.rm = TRUE), .SDcols = sdcols]
}
# delete all original 360 columns by reference from the original data
DT[, grep("V", names(DT), value=TRUE) := NULL]

现在,您将剩下 30 列对应于 1 的位置。在我的系统上,这大约需要 0.4 秒。

all(unlist(final.df) == unlist(DT)) # not a fan of `identical`
# [1] TRUE

【讨论】:

  • 谢谢,阿伦。矩阵乘法是一个绝妙的主意,我什至没有朝那个方向思考。直觉上,我期望 plyr 或 reshape 有某种巧妙的技巧,但您对使用 data.table 的建议确实也是一个非常受欢迎的发现。
【解决方案2】:

我真的很喜欢@Arun 的矩阵乘法思想。有趣的是,如果你针对一些 OpenBLAS 库编译 R,你可以让它并行运行。

但是,我想为您提供另一种可能比矩阵乘法慢的解决方案,它使用您的原始模式,但比您的实现快得多:

# Match is usually faster than which, because it only returns the first match 
# (and therefore won't fail on multiple matches)
# It also neatly handles your *all NA* case
recodeDf2 <- function(df) apply(df,1,match,x=1) 
# You can split your data.frame by column with split.default
# (Using split on data.frame will split-by-row)
S2<-split.default(mydata,rep(1:30,each=12))
final.df2<-lapply(S2,recodeDf2)

如果您有一个非常大的数据帧和许多处理器,您可以考虑将此操作并行化:

library(parallel)
final.df2<-mclapply(S2,recodeDf2,mc.cores=numcores) 
# Where numcores is your number of processors.

阅读了@Arun 和@mnel,我学到了很多关于如何改进此功能的知识,方法是避免对数组的强制,通过按列而不是按行处理data.frame。我并不是要在这里“窃取”答案。 OP 应该考虑将复选框切换到@mnel 的答案。

不过,我想分享一个不使用data.table 并避免使用for 的解决方案。然而,它仍然比@mnel 的解决方案慢,尽管速度稍慢。

nograpes2<-function(mydata) {
  test<-function(df) {
    l<-lapply(df,function(x) which(x==1))
    lens<-lapply(l,length)
    rep.int(seq.int(l),times=lens)[order(unlist(l))]
  }
  S2<-split.default(mydata,rep(1:30,each=12))
  data.frame(lapply(S2,test))
}

我还想补充一点 @Aaron 的方法,如果 mydata 最初是 matrix,而不是 data.frame,则使用 whicharr.ind=TRUE 也会非常快速和优雅。强制到 matrix 比函数的其余部分慢。如果速度是一个问题,那么首先考虑将数据作为矩阵读取是值得考虑的。

【讨论】:

  • nograpes,(+1)谢谢。根据我对并行作业的经验,除非您要并行化的任务是“繁重的”,否则在完成后创建作业和合并结果的开销要高得多,结果会变慢。在一个处理器和一组处理器上进行基准测试会很有趣。我认为这里的实际操作并不“重”。如果我能挤出一些时间,我会尽力去做。
  • 谢谢。我也喜欢@Arun 关于矩阵乘法的建议。不过,我发现您的代码对于真实数据应用程序更健壮。乘法方法取决于数据的清洁度,否则行和将不正确。我尽我所能消除违规行为,但人们永远无法知道。该代码在速度方面做得很好,0.25 秒。很棒的建议。
  • 在 data.frame 上使用 apply 会强制转换为矩阵,效率不高。
  • @mnel,我刚刚意识到rowSums 也强制转换为矩阵。非常好的使用列操作。您的解决方案是迄今为止最快的。
【解决方案3】:

这是一种基本上是即时的方法。 (system.time = 0.1 秒)

se set。 columnMatch 组件将取决于您的数据,但如果它是每 12 列,那么以下将起作用。

MYD <- data.table(mydata)
# a new data.table (changed to numeric : Arun)
newDT <- as.data.table(replicate(30, numeric(nrow(MYD)),simplify = FALSE))
# for each column, which values equal 1
whiches <- lapply(MYD, function(x) which(x == 1))
# create a list of column matches (those you wish to aggregate)
columnMatch <- split(names(mydata), rep(1:30,each = 12))
setattr(columnMatch, 'names', names(newDT))

# cycle through all new columns
# and assign the the rows in the new data.table
## Arun: had to generate numeric indices for 
## cycling through 1:12, 13:24 in whiches[[.]]. That was the problem.
for(jj in seq_along(columnMatch)) {
 for(ii in seq_along(columnMatch[[jj]])) {
  set(newDT, j = jj, i = whiches[[ii + 12 * (jj-1)]], value = ii)
 }
}

这同样适用于通过引用原始添加列。

注意set 也适用于data.frames....

【讨论】:

  • 我不确定出了什么问题,但是这段代码没有给我结果。相反,我得到一个包含变量名而不是值的 data.table (newDT)。我想这些与我寻求的价值观相对应,例如V1_8指的是8。还是用“set”提点宝贵意见,谢谢。
  • @mnel,出色的答案。我做了一些更正。对whiches[[.]] 的访问不正确。对于每个 jj,它都经历了相同的 1:12,而对于 ex:对于 jj = 2ii 必须是 13:24。希望你不介意编辑。如果您不相信,请随时编辑/回滚。马克西姆,你现在应该得到想要的结果。是的,它很快!
【解决方案4】:

使用基数 R 可以完成的另一种方法是简单地获取要放入新矩阵中的值并直接使用矩阵索引填充它们。

idx <- which(mydata==1, arr.ind=TRUE)   # get indices of 1's
i <- idx[,2] %% 12                      # get column that was 1
idx[,2] <- ((idx[,2] - 1) %/% 12) + 1   # get "group" and put in "col" of idx
out <- array(NA, dim=c(1500,30))        # make empty matrix
out[idx] <- i                           # and fill it in!

【讨论】:

  • 一个非常有趣的方法,谢谢。不幸的是,它不适用于原始数据,很可能是因为某些行仅包含 NA。它确实与模拟数据配合得很好,当然实际数据可以调整。
  • ADDENDUM:它确实适用于原始数据,不确定第一次出了什么问题。再次感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-15
  • 1970-01-01
  • 2022-12-22
  • 1970-01-01
  • 2020-07-14
  • 1970-01-01
相关资源
最近更新 更多