将多个函数应用于数据框中的每一行

20

每次我认为我已经理解了如何处理向量,但看起来简单的问题总是让我头痛不已。阅读大量的资料和尝试不同的例子在这个问题上并没有帮助。请在这里给我详细指导...

我想对数据框中的每一行应用两个自定义函数,并将结果作为两个新列添加。以下是我的示例代码:

# Required packages:
library(plyr)

FindMFE <- function(x) {
    MFE <- max(x, na.rm = TRUE) 
    MFE <- ifelse(is.infinite(MFE ) | (MFE  < 0), 0, MFE)
    return(MFE)
}

FindMAE <- function(x) {
    MAE <- min(x, na.rm = TRUE) 
    MAE <- ifelse(is.infinite(MAE) | (MAE> 0), 0, MAE)
    return(MAE)
}

FindMAEandMFE <- function(x){
        # I know this next line is wrong...
    z <- apply(x, 1, FindMFE, FindMFE)
        return(z)
}

df1 <- data.frame(Bar1=c(1,2,3,-3,-2,-1),Bar2=c(3,1,3,-2,-3,-1))

df1 = transform(df1, 
    FindMAEandMFE(df1)  
)

#DF1 should end up with the following data...
#Bar1   Bar2    MFE MAE
#1      3       3   0
#2      1       2   0
#3      3       3   0
#-3     -2      0   -3
#-2     -3      0   -3
#-1     -1      0   -1

如果能使用plyr库和更基础的方法回答会很好,这两种方法都有助于我的理解。当然,如果我做错了什么,请指出来。;-)

现在我要回到帮助文件!

编辑:我想要一个多元解决方案,因为列名可能会随时间变化而扩展。它还允许将来重复使用代码。

4个回答

19
我认为你在这里想得太复杂了。使用两个单独的apply()调用有什么问题吗?然而,有一种更好的方法可以做到你在这里所做的事情,它不涉及循环/apply调用。我将分别处理这两个解决方案,但第二种解决方案更可取,因为它是真正向量化的。

使用两个 apply 调用的版本

首先是使用所有基本R函数的两个单独的 apply 调用:

df1 <- data.frame(Bar1=c(1,2,3,-3,-2,-1),Bar2=c(3,1,3,-2,-3,-1))
df1 <- transform(df1, MFE = apply(df1, 1, FindMFE), MAE = apply(df1, 1, FindMAE))
df1

这将会得到:

> df1
  Bar1 Bar2 MFE MAE
1    1    3   3   0
2    2    1   2   0
3    3    3   3   0
4   -3   -2   0  -3
5   -2   -3   0  -3
6   -1   -1   0  -1

好的,两次循环遍历df1的行可能有点低效,但即使对于大问题,您花费的时间已经比通过单次操作聪明地完成所需的时间更多。

使用向量化函数pmax()pmin()

因此,更好的方法是注意到pmax()pmin()函数,并意识到它们可以执行每个apply(df1, 1, FindFOO()调用所做的事情。例如:

> (tmp <- with(df1, pmax(0, Bar1, Bar2, na.rm = TRUE)))
[1] 3 2 3 0 0 0

如果从你的问题来看,需要使用 MFE。如果你有两列数据,它们是 Bar1Bar2 或者是 df1 的前两列,那么这个非常简单易用。但是如果你想要对多列进行计算呢?pmax(df1[, 1:2], na.rm = TRUE) 并不能满足我们的需求:

> pmax(df1[, 1:2], na.rm = TRUE)
  Bar1 Bar2
1    1    3
2    2    1
3    3    3
4   -3   -2
5   -2   -3
6   -1   -1

使用 pmax()pmin() 获得通用解决方案的技巧是使用 do.call() 来为我们安排这两个函数的调用。更新您的函数以使用此思路,我们有:
FindMFE2 <- function(x) {
   MFE <- do.call(pmax, c(as.list(x), 0, na.rm = TRUE))
   MFE[is.infinite(MFE)] <- 0
   MFE
}

FindMAE2 <- function(x) {
   MAE <- do.call(pmin, c(as.list(x), 0, na.rm = TRUE))
   MAE[is.infinite(MAE)] <- 0
   MAE
}

这提供了:

> transform(df1, MFE = FindMFE2(df1), MAE = FindMAE2(df1))
  Bar1 Bar2 MFE MAE
1    1    3   3   0
2    2    1   2   0
3    3    3   3   0
4   -3   -2   0  -3
5   -2   -3   0  -3
6   -1   -1   0  -1

没有一个apply()出现在视野中。如果你想一步完成这个操作,现在更容易包装:

FindMAEandMFE2 <- function(x){
    cbind(MFE = FindMFE2(x), MAE = FindMAE2(x))
}

这可以用作:

> cbind(df1, FindMAEandMFE2(df1))
  Bar1 Bar2 MFE MAE
1    1    3   3   0
2    2    1   2   0
3    3    3   3   0
4   -3   -2   0  -3
5   -2   -3   0  -3
6   -1   -1   0  -1

@LookLeft - 关于你的编辑,我相当确定Gavin的向量化pminpmax解决方案将处理任意列数、任意名称的数据框。但是我只是猜测你在这里所说的“多元”的含义。 - joran
非常感谢您的详细描述,有关循环问题和使用 do.call 的通用解决方案非常有洞见。我开始更加理解向量和 R 函数处理它们的方式。我会继续尝试每个示例。 - Look Left
@joran。是的,这次编辑是为了回应Gavin的评论和答案。他发现了限制并提供了一个很好的答案。 - Look Left

19
我将为您翻译如下内容,涉及IT技术:

下面列出了三种替代的一行命令:

  • 使用plyreach函数
  • 使用基础R的plyreach函数
  • 使用可以向量化的pminpmax函数

解决方案1: plyr和each

plyr包定义了each函数,它可以满足您的要求。从?each中可以看到: 将多个函数聚合成一个函数。这意味着您可以使用以下一行代码解决问题:

library(plyr)
adply(df1, 1, each(MAE=function(x)max(x, 0), MFE=function(x)min(x, 0)))

  Bar1 Bar2 MAE MFE
1    1    3   3   0
2    2    1   2   0
3    3    3   3   0
4   -3   -2   0  -3
5   -2   -3   0  -3
6   -1   -1   0  -1

解决方案2:使用each和基本R函数

当然,您可以在基本函数中使用each。以下是如何在apply中使用它 - 请注意,在将结果添加到原始数据框之前,您必须转置结果。

library(plyr)
data.frame(df1, 
  t(apply(df1, 1, each(MAE=function(x)max(x, 0), MFE=function(x)min(x, 0)))))

  Bar1 Bar2 MAE MFE
1    1    3   3   0
2    2    1   2   0
3    3    3   3   0
4   -3   -2   0  -3
5   -2   -3   0  -3
6   -1   -1   0  -1

解决方案3:使用向量化函数

使用向量化函数pminpmax,您可以使用以下一行代码:

transform(df1, MFE=pmax(0, Bar1, Bar2), MAE=pmin(0, Bar1, Bar2))

  Bar1 Bar2 MFE MAE
1    1    3   3   0
2    2    1   2   0
3    3    3   3   0
4   -3   -2   0  -3
5   -2   -3   0  -3
6   -1   -1   0  -1

只是在展示而已。你在pmin/max colls中得到额外的奖励分数。我因为允许在df1中使用任意数量的列而获得额外的奖励分数 :P - Gavin Simpson
@GavinSimpson 我修改后的答案展示了三种解决问题的替代方法(一行代码),其中两种方法允许任意数量的列。 - Andrie
现在你只是在炫耀!;-) 不错。解决方案1和2会很慢(在大问题上非常慢),我们可能不应该鼓励使用非向量化的解决方案而不是向量化的解决方案。但是,不清楚OP是否想要一个多元应用的通用解决方案或者这个特定问题的解决方案。所以我会让过度使用plyr通过这一次 ;-) - Gavin Simpson
我想要一个多元解决方案。哇,非常感谢您提供的所有帮助,让我消化一下,我会尽快回复大家。 - Look Left
完成plyr()的部分将得到+1。非常感谢!我尝试了全部三种方法,Gavin的解决方案是正确的。对于使用我的实际数据(100列x 23000行)和一个稍微复杂一些的循环逐步添加列的过程,每个方法的速度为:do.call(Gavin) 是29秒,Solution 2是105秒,而Solution 1则需要更长时间。所以,在这种情况下,plyr()库并不是最好的解决方案。我将继续测试。 - Look Left

6

这里有很多好的答案。当Gavin Simpson正在编辑时,我开始了这个问题,所以我们涉及了一些相似的内容。并行最小值和最大值函数(pmin和pmax)的功能基本上与您编写函数的目的相同。在pmax(0, Bar1, Bar2)中,0的作用可能有点不透明,但本质上0被循环利用,就像执行

pmax(c(0,0,0,0,0,0), Bar1, Bar2)

这将对传递的三个项目中的每个项目进行查找,并找到它们中的最大值。因此,如果它是负数,那么最大值将为0,并且可以实现你的ifelse语句的大部分功能。你可以重写代码,使用类似于你之前使用的函数来获取向量并结合它们,这可能会使代码更加透明。在这种情况下,我们只需将数据框传递给新的并行和快速的findMFE函数,该函数将适用于任何数值数据框并输出一个向量。

findMFE <- function(dataf){
    MFE <- do.call( pmax, c(dataf, 0, na.rm = TRUE))
}

MFE <- findMFE(df1)

这个函数的作用是向传递的数据框添加一个额外的0列,然后调用pmax函数,将df1的每一列分别作为列表传递(数据框就是列表,所以这很容易)。
现在,我注意到您实际上想要纠正数据中不在示例中的Inf值...我们可以向您的函数添加一行额外的代码...
findMFE <- function(dataf){
    MFE <- do.call( pmax, c(dataf, 0, na.rm = TRUE))
    ifelse(is.infinite(MFE), 0, MFE)
}

现在,这是在向量上正确使用ifelse()函数的示例。我以这种方式为您提供了一个示例,但Gavin Simpson使用MFE[is.infinite(MFE)] <- 0更有效。请注意,此findMFE函数未在循环中使用,而是将整个数据框传递给它。
相应的findMAE函数是...
findMAE <- function(dataf){
    MAE <- do.call( pmin, c(dataf, 0, na.rm = TRUE))
    ifelse(is.infinite(MAE), 0, MAE)
}

而结合功能就是简单地...
findMFEandMAE <- function(dataf){
    MFE <- findMFE(dataf)
    MAE <- findMAE(dataf)
    return(data.frame(MFE, MAE))
}

MFEandMAE <- findMFEandMAE(df1) // 查找MFE和MAE df1 <- cbind(df1, MFEandMAE) // 将MFE和MAE添加到df1中

一些提示:

如果你有一个标量if语句,请不要使用ifelse(),而是使用if() else。在标量情况下,它的速度要快得多。而且,你的函数是标量的,而你正在试图对它们进行向量化。ifelse()已经被矢量化了,在这种用法下运行非常快,但是当标量使用时比if() else慢得多。

此外,如果你将东西放入循环或应用语句中,请尽可能少地放置。例如,在你的情况下,ifelse()真的需要从循环中取出,并在整个MFE结果之后应用。


我接受了挑战,并在我的修订答案中使用 plyr正常的 minmax 函数提供了一行解决方案。 - Andrie
谢谢John。额外0列的描述很有用,ifelse()的最佳使用仍在消化中;即循环对比整体。我希望有一天能够回报每个人的恩情或将它们“向前传递”。 - Look Left

1

如果你真的、真的想要它,你就可以:

FindMAEandMFE <- function(x){
    t(apply(x, 1, function(currow){c(MAE=FindMAE(currow), MFE=FindMFE(currow))}))
}

(未经测试 - 它应该返回一个具有两个(命名的,我认为)列和与数据框中一样多的行的数组)。现在你可以这样做:

df1<-cbind(df1, FindMAEandMFE(df1))

非常棘手。请听从加文的建议。


谢谢,我会遵循Gavin的建议。 - Look Left

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