11

通常,我想在包含一些因子变量的数据集上运行交叉验证,并且在运行一段时间后,交叉验证例程失败并出现错误:factor x has new levels Y.

例如,使用包启动

library(boot)
d <- data.frame(x=c('A', 'A', 'B', 'B', 'C', 'C'), y=c(1, 2, 3, 4, 5, 6))
m <- glm(y ~ x, data=d)
m.cv <- cv.glm(d, m, K=2) # Sometimes succeeds
m.cv <- cv.glm(d, m, K=2)
# Error in model.frame.default(Terms, newdata, na.action = na.action, xlev = object$xlevels) : 
#   factor x has new levels B

更新:这是一个玩具示例。同样的问题也出现在较大的数据集上,其中出现了几次级别,但训练分区C中都不存在。


包中的函数createDataPartition函数对结果变量进行分层抽样并正确警告:caret

此外,对于“createDataPartition”,非常小的班级规模 (<= 3) 班级可能不会同时出现在训练和测试数据中。

有两种解决方案浮现在脑海中:

  1. 首先,通过首先选择一个随机样本来创建数据的子集factor level,从最稀有的类别(按频率)开始,然后贪婪地满足下一个稀有类别,依​​此类推。然后createDataPartition在数据集的其余部分上使用并合并结果以创建一个新的火车数据集,其中包含所有levels.
  2. 使用createDataPartitions和做拒绝抽样。

到目前为止,由于数据大小的原因,选项2对我有用,但我不禁认为必须有比手动推出的解决方案更好的解决方案。

理想情况下,我想要一个仅适用于创建分区的解决方案,如果无法创建此类分区,则会提前失败。

软件包不提供此功能是否有根本的理论原因?他们是否提供它而我只是因为盲点而无法发现它们?有没有更好的方法来进行这种分层抽样?

如果我应该在stats.stackoverflow.com上提出这个问题,请发表评论。


更新

这就是我手工推出的解决方案 (2) 的样子:

get.cv.idx <- function(train.data, folds, factor.cols = NA) {

    if (is.na(factor.cols)) {
        all.cols        <- colnames(train.data)
        factor.cols     <- all.cols[laply(llply(train.data[1, ], class), function (x) 'factor' %in% x)]
    }

    n                   <- nrow(train.data)
    test.n              <- floor(1 / folds * n)

    cond.met            <- FALSE
    n.tries             <- 0

    while (!cond.met) {
        n.tries         <- n.tries + 1
        test.idx        <- sample(nrow(train.data), test.n)
        train.idx       <- setdiff(1:nrow(train.data), test.idx)

        cond.met        <- TRUE

        for(factor.col in factor.cols) {
            train.levels <- train.data[ train.idx, factor.col ]
            test.levels  <- train.data[ test.idx , factor.col ]
            if (length(unique(train.levels)) < length(unique(test.levels))) {
                cat('Factor level: ', factor.col, ' violated constraint, retrying.\n')
                cond.met <- FALSE
            }
        }
    }

    cat('Done in ', n.tries, ' trie(s).\n')

    list( train.idx = train.idx
        , test.idx  = test.idx
        )
}
4

3 回答 3

8

每个人都同意肯定有一个最佳解决方案。但就个人而言,我只会打电话trycv.glm直到它使用while.

m.cv<- try(cv.glm(d, m, K=2)) #First try
class(m.cv) #Sometimes error, sometimes list
while ( inherits(m.cv, "try-error") ) {
m.cv<- try(cv.glm(d, m, K=2))
}
class(m.cv) #always list

我已经在 data.fame 中尝试了 100,000 行,并且只需要几秒钟。

library(boot)
n <-100000
d <- data.frame(x=c(rep('A',n), rep('B', n), 'C', 'C'), y=1:(n*2+2))
m <- glm(y ~ x, data=d)

m.cv<- try(cv.glm(d, m, K=2))
class(m.cv) #Sometimes error, sometimes list
while ( inherits(m.cv, "try-error") ) {
m.cv<- try(cv.glm(d, m, K=2))
}
class(m.cv) #always list
于 2013-11-22T22:54:53.093 回答
1

当我调用回溯时,我得到了这个:

> traceback()
9: stop(sprintf(ngettext(length(m), "factor %s has new level %s", 
       "factor %s has new levels %s"), nm, paste(nxl[m], collapse = ", ")), 
       domain = NA)
8: model.frame.default(Terms, newdata, na.action = na.action, xlev = object$xlevels)
7: model.frame(Terms, newdata, na.action = na.action, xlev = object$xlevels)
6: predict.lm(object, newdata, se.fit, scale = 1, type = ifelse(type == 
       "link", "response", type), terms = terms, na.action = na.action)
5: predict.glm(d.glm, data[j.out, , drop = FALSE], type = "response")
4: predict(d.glm, data[j.out, , drop = FALSE], type = "response")
3: mean((y - yhat)^2)
2: cost(glm.y[j.out], predict(d.glm, data[j.out, , drop = FALSE], 
       type = "response"))
1: cv.glm(d, m, K = 2)

并且查看cv.glm函数给出:

> cv.glm
function (data, glmfit, cost = function(y, yhat) mean((y - yhat)^2), 
    K = n) 
{
    call <- match.call()
    if (!exists(".Random.seed", envir = .GlobalEnv, inherits = FALSE)) 
        runif(1)
    seed <- get(".Random.seed", envir = .GlobalEnv, inherits = FALSE)
    n <- nrow(data)
    out <- NULL
    if ((K > n) || (K <= 1)) 
        stop("'K' outside allowable range")
    K.o <- K
    K <- round(K)
    kvals <- unique(round(n/(1L:floor(n/2))))
    temp <- abs(kvals - K)
    if (!any(temp == 0)) 
        K <- kvals[temp == min(temp)][1L]
    if (K != K.o) 
        warning(gettextf("'K' has been set to %f", K), domain = NA)
    f <- ceiling(n/K)
    s <- sample0(rep(1L:K, f), n)
    n.s <- table(s)
    glm.y <- glmfit$y
    cost.0 <- cost(glm.y, fitted(glmfit))
    ms <- max(s)
    CV <- 0
    Call <- glmfit$call
    for (i in seq_len(ms)) {
        j.out <- seq_len(n)[(s == i)]
        j.in <- seq_len(n)[(s != i)]
        Call$data <- data[j.in, , drop = FALSE]
        d.glm <- eval.parent(Call)
        p.alpha <- n.s[i]/n
        cost.i <- cost(glm.y[j.out], predict(d.glm, data[j.out, 
            , drop = FALSE], type = "response"))
        CV <- CV + p.alpha * cost.i
        cost.0 <- cost.0 - p.alpha * cost(glm.y, predict(d.glm, 
            data, type = "response"))
    }
    list(call = call, K = K, delta = as.numeric(c(CV, CV + cost.0)), 
        seed = seed)
}

似乎问题与您极小的样本量和分类效果(值“A”、“B”和“C”)有关。您正在拟合具有 2 种效果的 glm:“B:A”和“C:A”。在每次 CV 迭代中,您从样本数据集引导并拟合一个新模型d.glm。给定大小,自举数据保证会出现 1 次或多次迭代,其中值“C”没有被采样,因此错误来自于从训练数据中计算自举模型的拟合概率,其中验证数据具有在训练数据中未观察到 x 的“C”级别。

Frank Harrell(经常在 stats.stackexchange.com 上)在回归建模策略中写道,当样本量较小和/或分类数据分析中的某些单元格计数较小时,应该反对拆分样本验证。奇点(正如你在这里看到的)是我认为这是真的的众多原因之一。

鉴于此处的样本量较小,您应该考虑一些拆分样本交叉验证替代方案,例如置换测试或参数引导。另一个重要的考虑因素就是为什么你觉得基于模型的推理是不正确的。正如 Tukey 所说的 bootstrap,他喜欢称它为霰弹枪。只要您愿意重新组装零件,它就会解决任何问题。

于 2013-11-18T22:51:29.847 回答
1

网络上似乎没有很多简单的解决方案,所以这是我制定的一个,应该很容易概括为您需要的尽可能多的因素。它使用预安装的软件包和 Caret,但如果你真的想要的话,你可以只使用基本 R。

要在有多个因素时使用交叉验证,请遵循两步过程。将因子转换为数字,然后将它们相乘。将此新变量用作分层抽样函数中的目标变量。创建折叠后,请务必将其移除或保留在训练集中。

如果 y 是你的 DV 并且 x 是一个因素,那么:

#Simulated factors (which are conveniently distributed for the example)
dataset <-data.frame(x=as.factor(rep(c(1,10),1000)),y=as.factor(rep(c(1,2,3,4),250)[sample(1000)]))

#Convert the factors to numerics and multiply together in new variable
dataset$cv.variable <-as.numeric(levels(dataset$x))[dataset$x]*as.numeric(levels(dataset$y))[dataset$y]


prop.table(table(dataset$y)) #One way to view distribution of levels
ftable(dataset$x,dataset$y)  #A full table of all x and y combinations

folds <- caret::createFolds(dataset$cv.variable,k=10) 
testIndexes <- folds[[k]]
testData <- as.data.frame(dataset[testIndexes, ])
trainData <- as.data.frame(dataset[-testIndexes, ])

prop.table(table(testData$y)) 
ftable(testData$x,testData$y) #evaluate distribution

这应该会产生接近平衡的结果。

注意:在现实生活中,如果您的样本缺乏必要的独特因素组合,那么您的问题就更难克服,而且可能是不可能的。您可以在创建折叠之前从考虑中删除某些级别,也可以使用某种过度采样。

于 2019-10-29T05:26:35.790 回答