prédire.lm () avec un niveau de facteur inconnu dans les données d'essai

j'adapte un modèle aux données factorielles et aux prévisions. Si le newdatapredict.lm() contient un niveau de facteur unique qui est inconnu du modèle,predict.lm() échoue et renvoie une erreur.

Est-il un bon moyen de s' predict.lm() retourner une prédiction pour les niveaux de facteurs que le modèle connaît et NA Pour les niveaux de facteurs inconnus, au lieu d'une simple erreur?

exemple de code:

foo <- data.frame(response=rnorm(3),predictor=as.factor(c("A","B","C")))
model <- lm(response~predictor,foo)
foo.new <- data.frame(predictor=as.factor(c("A","B","C","D")))
predict(model,newdata=foo.new)

je voudrais que la toute dernière commande renvoie trois des prédictions "réelles" correspondant aux niveaux de facteur "A", "B" et " C " et un NA correspondant au niveau inconnu "D".

32
demandé sur 李哲源 2010-11-26 15:15:07

7 réponses

Rangé et étendu la fonction par MorgenBall. Il est également mis en œuvre dans sperrorest maintenant.

Caractéristiques supplémentaires

  • gouttes inutilisés facteur de niveau plutôt que de simplement définir les valeurs manquantes NA.
  • questions un message à l'utilisateur que les niveaux de facteurs ont été abandonnées
  • vérifie l'existence de variables de facteurs dans test_data et renvoie les données d'origine.cadre si non sont présent
  • fonctionne non seulement pour lm,glm et glmmPQL

Note: la fonction montrée ici peut changer (s'améliorer) avec le temps.

#' @title remove_missing_levels
#' @description Accounts for missing factor levels present only in test data
#' but not in train data by setting values to NA
#'
#' @import magrittr
#' @importFrom gdata unmatrix
#' @importFrom stringr str_split
#'
#' @param fit fitted model on training data
#'
#' @param test_data data to make predictions for
#'
#' @return data.frame with matching factor levels to fitted model
#'
#' @keywords internal
#'
#' @export
remove_missing_levels <- function(fit, test_data) {

  # https://stackoverflow.com/a/39495480/4185785

  # drop empty factor levels in test data
  test_data %>%
    droplevels() %>%
    as.data.frame() -> test_data

  # 'fit' object structure of 'lm' and 'glmmPQL' is different so we need to
  # account for it
  if (any(class(fit) == "glmmPQL")) {
    # Obtain factor predictors in the model and their levels
    factors <- (gsub("[-^0-9]|as.factor|\(|\)", "",
                     names(unlist(fit$contrasts))))
    # do nothing if no factors are present
    if (length(factors) == 0) {
      return(test_data)
    }

    map(fit$contrasts, function(x) names(unmatrix(x))) %>%
      unlist() -> factor_levels
    factor_levels %>% str_split(":", simplify = TRUE) %>%
      extract(, 1) -> factor_levels

    model_factors <- as.data.frame(cbind(factors, factor_levels))
  } else {
    # Obtain factor predictors in the model and their levels
    factors <- (gsub("[-^0-9]|as.factor|\(|\)", "",
                     names(unlist(fit$xlevels))))
    # do nothing if no factors are present
    if (length(factors) == 0) {
      return(test_data)
    }

    factor_levels <- unname(unlist(fit$xlevels))
    model_factors <- as.data.frame(cbind(factors, factor_levels))
  }

  # Select column names in test data that are factor predictors in
  # trained model

  predictors <- names(test_data[names(test_data) %in% factors])

  # For each factor predictor in your data, if the level is not in the model,
  # set the value to NA

  for (i in 1:length(predictors)) {
    found <- test_data[, predictors[i]] %in% model_factors[
      model_factors$factors == predictors[i], ]$factor_levels
    if (any(!found)) {
      # track which variable
      var <- predictors[i]
      # set to NA
      test_data[!found, predictors[i]] <- NA
      # drop empty factor levels in test data
      test_data %>%
        droplevels() -> test_data
      # issue warning to console
      message(sprintf(paste0("Setting missing levels in '%s', only present",
                             " in test data but missing in train data,",
                             " to 'NA'."),
                      var))
    }
  }
  return(test_data)
}

On peut appliquer cette fonction à l'exemple de la question comme suit:

predict(model,newdata=remove_missing_levels (fit=model, test_data=foo.new))

en essayant D'améliorer cette fonction, je suis tombé sur le fait que les méthodes D'apprentissage SL comme lm,glm etc. besoin des mêmes niveaux dans le train & test tandis que ML méthodes d'apprentissage (svm, randomForest) échoue si les niveaux sont supprimés. Ces méthodes nécessitent tous les niveaux de formation et de test.

une solution générale est assez difficile à réaliser puisque chaque modèle équipé a une façon différente de stocker son composant de niveau de facteur (fit$xlevelslm et fit$contrastsglmmPQL). Au moins, il semble être cohérent à travers lm modèles connexes.

6
répondu pat-s 2017-08-03 08:10:03

vous devez supprimer les niveaux supplémentaires avant tout calcul, comme:

> id <- which(!(foo.new$predictor %in% levels(foo$predictor)))
> foo.new$predictor[id] <- NA
> predict(model,newdata=foo.new)
         1          2          3          4 
-0.1676941 -0.6454521  0.4524391         NA 

C'est une façon plus générale de le faire, il va définir tous les niveaux qui ne se produisent pas dans les données originales à NA. Comme Hadley l'a mentionné dans les commentaires, ils auraient pu choisir de l'inclure dans le predict() fonction, mais ils n'ont pas

Pourquoi vous avez à faire devient évident si vous regardez le calcul lui-même. En interne, les prédictions sont calculés comme :

model.matrix(~predictor,data=foo) %*% coef(model)
        [,1]
1 -0.1676941
2 -0.6454521
3  0.4524391

en bas vous avez les deux matrices de modèle. Vous voyez que l'un pour l' foo.new a une colonne supplémentaire, donc vous ne pouvez plus utiliser le calcul matriciel. Si vous utilisiez le nouvel ensemble de données pour modéliser, vous obtiendriez aussi un modèle différent, celui avec une variable auxiliaire supplémentaire pour le niveau supplémentaire.

> model.matrix(~predictor,data=foo)
  (Intercept) predictorB predictorC
1           1          0          0
2           1          1          0
3           1          0          1
attr(,"assign")
[1] 0 1 1
attr(,"contrasts")
attr(,"contrasts")$predictor
[1] "contr.treatment"

> model.matrix(~predictor,data=foo.new)
  (Intercept) predictorB predictorC predictorD
1           1          0          0          0
2           1          1          0          0
3           1          0          1          0
4           1          0          0          1
attr(,"assign")
[1] 0 1 1 1
attr(,"contrasts")
attr(,"contrasts")$predictor
[1] "contr.treatment"

vous ne pouvez pas supprimer la dernière colonne de la matrice du modèle non plus, parce que même si vous faites cela, les deux autres niveaux sont encore influencés. Code pour le niveau A (0,0). B c'est (1,0), pour le C ce (0,1)... et pour D c'est nouveau (0,0)! Donc votre modèle supposerait que A et D sont le même niveau si elle laisserait tomber naïvement la dernière variable muette.

Sur une partie plus théorique: Il est possible de construire un modèle sans avoir à tous les niveaux. Maintenant, comme j'ai essayé de l'expliquer, ce modèle est valide pour les niveaux que vous avez utilisés lors de la construction du modèle. Si vous venez à travers de nouveaux niveaux, vous devez construire un nouveau modèle pour inclure les informations supplémentaires. Si vous ne le faites pas, la seule chose que vous pouvez faire est de supprimer les niveaux supplémentaires de l'ensemble de données. Mais en gros, vous perdez toute l'information qui y était contenue, donc ce n'est généralement pas considéré comme une bonne pratique.

29
répondu Joris Meys 2016-03-28 23:18:13

si vous voulez traiter les niveaux manquants dans vos données après avoir créé votre modèle lm mais avant d'appeler predict (étant donné que nous ne savons pas exactement quels niveaux pourraient être manquants à l'avance) voici la fonction que j'ai construit pour définir tous les niveaux non dans le modèle à NA - la prédiction donnera aussi NA et vous pouvez ensuite utiliser une méthode alternative pour prédire ces valeurs.

objet sera votre lm sortie de lm(..., data=trainData)

sera la trame de données que vous souhaitez créer des prédictions pour

missingLevelsToNA<-function(object,data){

  #Obtain factor predictors in the model and their levels ------------------

  factors<-(gsub("[-^0-9]|as.factor|\(|\)", "",names(unlist(object$xlevels))))
  factorLevels<-unname(unlist(object$xlevels))
  modelFactors<-as.data.frame(cbind(factors,factorLevels))


  #Select column names in your data that are factor predictors in your model -----

  predictors<-names(data[names(data) %in% factors])


  #For each factor predictor in your data if the level is not in the model set the value to NA --------------

  for (i in 1:length(predictors)){
    found<-data[,predictors[i]] %in% modelFactors[modelFactors$factors==predictors[i],]$factorLevels
    if (any(!found)) data[!found,predictors[i]]<-NA
  }

  data

}
5
répondu MorganBall 2016-09-15 09:18:08

on dirait que vous pourriez aimer les effets aléatoires. Regardez dans quelque chose comme glmer (paquet lme4). Avec un modèle bayésien, vous obtiendrez des effets qui approchent 0 quand il y a peu d'information à utiliser pour les estimer. Attention, cependant, que vous devrez faire la prédiction vous-même, plutôt que d'utiliser predict().

alternativement, vous pouvez simplement faire des variables fictives pour les niveaux que vous voulez inclure dans le modèle, par exemple une variable 0/1 pour le lundi, une pour le mardi, une pour le mercredi, etc. Dimanche sera automatiquement supprimé du modèle s'il contient tous les 0. Mais avoir un 1 dans la colonne du dimanche dans les autres données ne manquera pas l'étape de prédiction. On supposera simplement que le dimanche a un effet moyen sur les autres jours (ce qui peut être vrai ou pas).

2
répondu tiffany 2013-11-11 19:05:21

une des hypothèses des régressions linéaires / logistiques est à peu ou pas de multi-colinéarité; donc si les variables prédictives sont idéalement indépendantes les unes des autres, alors le modèle n'a pas besoin de voir toute la variété possible de niveaux de facteurs. Un nouveau niveau de facteur (D) est un nouveau prédicteur, et peut être réglé à NA sans affecter la capacité de prédiction des autres facteurs A,B,C. C'est pourquoi le modèle devrait toujours être en mesure de faire des prédictions. Mais l'ajout du nouveau niveau D schéma attendu. C'est toute la question. Réglage NA correctifs.

1
répondu Kingz 2015-05-08 23:57:50

lme4 le paquet gérera les nouveaux niveaux si vous mettez le drapeau allow.new.levels=TRUE quand vous appelez predict.

exemple: si votre facteur de jour de la semaine est dans une variable dow et catégorique résultat b_fail, vous pouvez exécuter

M0 <- lmer(b_fail ~ x + (1 | dow), data=df.your.data, family=binomial(link='logit')) M0.preds <- predict(M0, df.new.data, allow.new.levels=TRUE)

il s'agit d'un exemple de régression logistique à effets aléatoires. Bien sûr, vous pouvez effectuer régulièrement une régression ... ou la plupart des modèles GLM. Si vous voulez aller plus loin sur le chemin bayésien, regardez L'excellent Gelman & Hill livre et de la Stan de l'infrastructure.

1
répondu Lantern Rouge 2017-08-23 19:41:18

une solution rapide et sale pour les tests de dédoublement, est de recoder des valeurs rares comme "autre". Voici une implémentation:

rare_to_other <- function(x, fault_factor = 1e6) {
  # dirty dealing with rare levels:
  # recode small cells as "other" before splitting to train/test,
  # assuring that lopsided split occurs with prob < 1/fault_factor
  # (N.b. not fully kosher, but useful for quick and dirty exploratory).

  if (is.factor(x) | is.character(x)) {
    min.cell.size = log(fault_factor, 2) + 1
    xfreq <- sort(table(x), dec = T)
    rare_levels <- names(which(xfreq < min.cell.size))
    if (length(rare_levels) == length(unique(x))) {
      warning("all levels are rare and recorded as other. make sure this is desirable")
    }
    if (length(rare_levels) > 0) {
      message("recoding rare levels")
      if (is.factor(x)) {
        altx <- as.character(x)
        altx[altx %in% rare_levels] <- "other"
        x <- as.factor(altx)
        return(x)
      } else {
        # is.character(x)
        x[x %in% rare_levels] <- "other"
        return(x)
      }
    } else {
      message("no rare levels encountered")
      return(x)
    }
  } else {
    message("x is neither a factor nor a character, doing nothing")
    return(x)
  }
}

par exemple, avec data.tableau, l'appel serait quelque chose comme:

dt[, (xcols) := mclapply(.SD, rare_to_other), .SDcol = xcols] # recode rare levels as other

xcols est un sous-ensemble de colnames(dt).

0
répondu dzeltzer 2018-08-05 08:13:14