如何在dplyr::summarize函数中传递多个列给一个函数?

4
我正在尝试将符合条件的数据框中的所有列传递给dplyr的summarize函数中的一个函数,如下所示:
df %>% group_by(Version, Type) %>%
  summarize(mcll(TrueClass, starts_with("pred")))

Error: argument is of length zero

有没有方法可以实现这个?以下是一个可行的示例:

构建一个模拟数据框,包含样本预测。这些被解释为分类算法的输出。

library(dplyr)
nrow <- 40
ncol <- 4
set.seed(567879)

getProbs <- function(i) {
  p <- runif(i)
  return(p / sum(p))
}
df <- data.frame(matrix(NA, nrow, ncol))
for (i in seq(nrow)) df[i, ] <- getProbs(ncol)
names(df) <- paste0("pred.", seq(ncol))

添加一个列来表示真实类别

df$TrueClass <- factor(ceiling(runif(nrow, min = 0, max = ncol)))

增加分类列以进行子集分析

df$Type <- c(rep("a", nrow / 2), rep("b", nrow / 2))
df$Version <-  rep(1:4, times = nrow / 4)

现在我想使用以下函数计算这些预测的多类LogLoss:
mcll <- function (act, pred) 
{
  if (class(act) != "factor") {
    stop("act must be a factor")
  }
  pred[pred == 0] <- 1e-15
  pred[pred == 1] <- 1 - 1e-15
  dummies <- model.matrix(~act - 1)
  if (nrow(dummies) != nrow(pred)) {
    return(0)
  }
  return(-1 * (sum(dummies * log(pred)))/length(act))
}

这可以轻松地使用整个数据集完成。

act <- df$TrueClass
pred <- df %>% select(starts_with("pred"))
mcll(act, pred)

但我希望使用dplyr的group_by函数来计算数据子集的mcll值。

df %>% group_by(Version, Type) %>%
  summarize(mcll(TrueClass, starts_with("pred")))

理想情况下,我希望不改变 mcll() 函数的前提下完成此操作,但如果这能简化其他代码,我也可以做出更改。
谢谢!
注:mcll 的输入是一个真实值向量和一个概率矩阵,每一列都对应一个“pred”列。对于每个数据子集,mcll 应该返回一个标量。我可以使用下面的代码精确地得到我想要的结果,但我希望在 dplyr 的上下文中完成这件事。
mcll_df <- data.frame(matrix(ncol = 3, nrow = 8))
names(mcll_df) <- c("Type", "Version", "mcll")
count = 1
for (ver in unique(df$Version)) {
  for (type in unique(df$Type)) {
    subdat <- df %>% filter(Type == type & Version == ver)
    val <- mcll(subdat$TrueClass, subdat %>% select(starts_with("pred")))
    mcll_df[count, ] <- c(Type = type, Version = ver, mcll = val)
    count = count + 1
  }
}
head(mcll_df)
  Type Version             mcll
1    a       1 1.42972507510096
2    b       1 1.97189000832723
3    a       2 1.97988830406062
4    b       2 1.21387875938737
5    a       3 1.30629638026735
6    b       3 1.48799237895462

我曾试图使用mutate()做类似这样的事情,但好像不可能。你需要在调用那些以 starts_with 开头的函数时有适当的上下文环境,在 summarize() 中我认为这是不可用的(至少在我查看时是这样)。 - MrFlick
理论上,df %>% group_by(Version, Type) %>% summarise_at(vars(starts_with("pred")), funs(mcll(TrueClass, .))) 应该可以完成它 (?) - lukeA
@lukeA 那是我的第一个猜测,但它不起作用... - Sotos
2个回答

2
这很容易通过使用data.table来完成:
library(data.table)

setDT(df)[, mcll(TrueClass, .SD), by = .(Version, Type), .SDcols = grep("^pred", names(df))] 
#   Version Type       V1
#1:       1    a 1.429725
#2:       2    a 1.979888
#3:       3    a 1.306296
#4:       4    a 1.668330
#5:       1    b 1.971890
#6:       2    b 1.213879
#7:       3    b 1.487992
#8:       4    b 1.171286

我本来希望有一种dplyr的方法,但这样也行。谢谢! - Kevin Burnham

0
我必须稍微修改一下 mcll 函数,但之后它就正常工作了。问题出现在第二个 if 语句上。你告诉函数获取 nrow(pred),但如果你正在对多列进行汇总,每次实际上只提供一个向量(因为每列都会被单独分析)。此外,我更改了输入到函数中的参数顺序。
mcll <- function (pred, act) 
{
  if (class(act) != "factor") {
    stop("act must be a factor")
  }
   pred[pred == 0] <- 1e-15
   pred[pred == 1] <- 1 - 1e-15

  dummies <- model.matrix(~act - 1)
  if (nrow(dummies) != length(pred)) { # the main change is here
    return(0)
  }
  return(-1 * (sum(dummies * log(pred)))/length(act))
}

从那里我们可以使用summarise_each函数。

df %>% group_by(Version,Type) %>% summarise_each(funs(mcll(., TrueClass)), matches("pred"))

  Version  Type   pred.1   pred.2   pred.3   pred.4
    (int) (chr)    (dbl)    (dbl)    (dbl)    (dbl)
1       1     a 1.475232 1.972779 1.743491 1.161984
2       1     b 2.030829 1.331629 1.397577 1.484865
3       2     a 1.589256 1.740858 1.898906 2.005511

我对数据的一个子集进行了检查,看起来它是有效的。

mcll(df$pred.1[which(df$Type=="a" & df$Version==1)],
 df$TrueClass[which(df$Type=="a" & df$Version==1)])

[1] 1.475232 #pred.1 mcll when Version equals 1 and Type equals a.

不错,但不完全是我想要的。我已经编辑了上面的问题,使其更清晰。每个pred列应该绑定在一个单独的数据框中,并作为mcll函数的pred参数提供,该函数应该为数据的每个子集返回一个标量。虽然信息很好,但也许现在我可以自己弄清楚了。 - Kevin Burnham

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