predict.lm () mit einem unbekannten Faktor Niveau in Testdaten
-
28-09-2019 - |
Frage
Ich bin passend, ein Modell zu Faktordaten und Vorhersage-. Wenn die newdata
in predict.lm()
enthält einen einzigen Faktor Ebene, die dem Modell nicht bekannt ist, alle von predict.lm()
schlägt fehl und gibt einen Fehler zurück.
Gibt es ein guter Weg, predict.lm()
haben eine Vorhersage für die Faktorstufen kehrt das Modell kennt und NA für unbekannte Faktor Ebenen, statt nur einen Fehler entdeckt?
Beispielcode:
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)
Ich möchte der letzte Befehl drei „echte“ Prognosen zurück zu Faktorstufen „A“ entspricht, „B“ und „C“ und eine NA
zu dem unbekannten Niveau „D“ entspricht.
Lösung
in Ordnung gebracht und die Funktion erweitert von MorgenBall . Es ist ebenfalls implementiert in sperrorest jetzt.
Zusätzliche Funktionen
- fällt nicht verwendeten Faktorstufen und nicht nur die fehlenden Werte zu
NA
Einstellung. - Fragen eine Nachricht an den Benutzer, dass Faktorstufen wurden fallen gelassen
- prüft auf die Existenz von Faktor Variablen in
test_data
und gibt original data.frame wenn nicht vorhanden sind - funktioniert nicht nur für
lm
,glm
und aber auch fürglmmPQL
Hinweis: Die Funktion hier gezeigt ist, kann (verbessern) im Laufe der Zeit ändern.
#' @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)
}
Wir können diese Funktion zum Beispiel in der Frage gelten, wie folgt:
predict(model,newdata=remove_missing_levels (fit=model, test_data=foo.new))
Bei dem Versuch, diese Funktion zu verbessern, stieß ich auf die Tatsache, dass SL Methoden wie lm
Lernen, glm
usw. müssen die gleichen Niveaus in Zug & Test während ML Methoden Lernen (svm
, randomForest
) fehlschlagen, wenn die Pegel entfernt werden. Diese Verfahren müssen alle Ebenen in Zug & Test.
Eine allgemeine Lösung ist ziemlich schwer zu erreichen, da jedes angepasste Modell eine andere Art und Weise hat ihre Faktor Pegelkomponente des Speicherns (fit$xlevels
für lm
und fit$contrasts
für glmmPQL
). Zumindest scheint es konsequent über lm
verwandte Modelle.
Andere Tipps
Sie haben die zusätzliche Ebene vor jeder Berechnung zu entfernen, wie:
> 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
Dies ist eine allgemeinere Art und Weise tun, werden alle Ebenen, die in der ursprünglichen Daten NA nicht auftreten. Wie Hadley in den Kommentaren erwähnt, haben sie gewählt, könnte dies in der predict()
Funktion enthalten, aber sie taten es nicht
Warum Sie tun müssen, das wird offensichtlich, wenn man die Berechnung sieht selbst. Intern werden die Prognosen wie folgt berechnet:
model.matrix(~predictor,data=foo) %*% coef(model)
[,1]
1 -0.1676941
2 -0.6454521
3 0.4524391
Am Boden haben Sie beiden Modell-Matrizen. Sie sehen, dass die für foo.new
eine zusätzliche Spalte hat, so dass Sie nicht die Matrix-Berechnung nicht mehr verwenden können. Wenn Sie den neuen Datensatz zu Modell verwenden würden, würden Sie auch ein anderes Modell bekommen, mit einem zusätzlichen Dummy-Variable für die zusätzliche Ebene als einer.
> 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"
Sie können nicht löschen Sie einfach die letzte Spalte aus der Modellmatrix entweder, denn selbst wenn Sie das tun, beide andere Ebene noch beeinflusst werden. Der Code für Level A
wäre (0,0). Für B
ist dies (1,0), für C
dieses (0,1) ... und für D
ist es wieder (0,0)! Also Ihr Modell, das A
und D
annehmen würde, ist auf das gleiche Niveau, wenn sie ganz naiv das letzte Dummy-Variable fallen würden.
Auf einem theoretischen Teil: Es ist möglich, ein Modell zu bauen, ohne alle Ebene aufweisen. Nun, wie ich schon versucht zu erklären, dass Modell ist nur gültig für die Ebenen Sie beim Erstellen des Modells. Wenn Sie auf eine neue Ebene kommen, müssen Sie ein neues Modell erstellen, die zusätzliche Informationen enthalten. Wenn Sie das nicht tun, das einzige, was Sie tun können, ist die Extra-Level aus dem Datensatz löschen. Aber dann verlieren Sie im Grunde alle Informationen, die darin enthalten waren, so ist es in der Regel keine gute Praxis betrachtet.
Wenn Sie mit den fehlenden Ebenen in Ihren Daten umgehen wollen nach dem Filmmodell zu schaffen, sondern vor vorhersagen Aufruf (da wir nicht genau wissen, welche Werte vorher könnte fehlt) ist hier Funktion, die ich gebaut habe auf allen Ebenen eingestellt nicht im Modell NA - wird die Vorhersage auch dann NA geben und Sie können dann eine alternative Methode, um diese Werte zu prognostizieren verwenden
.Objekt Ihre lm Ausgabe von lm sein (..., data = trainData)
Daten wird der Datenrahmen werden Sie wollen die Prognosen erstellen für
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
}
Sounds wie Sie vielleicht zufällige Effekte mögen. Schauen Sie in so etwas wie glmer (lme4 Paket). Mit einem Bayes-Modell, werden Sie Effekte erhalten, die 0 nähern, wenn es nur wenig Informationen zu verwenden ist, wenn sie zu schätzen. Warnung aber, dass Sie sich selbst Vorhersage zu tun haben werden, anstatt vorhersagen ().
Alternativ können Sie einfach Dummy-Variablen machen für die Ebenen wollen Sie in das Modell aufzunehmen, z.B. eine Variable 0/1 für Montag, einen für Dienstag, einen für Mittwoch, usw. Sonntag automatisch aus dem Modell entfernt werden, wenn sie alle 0'en enthält. Aber mit einem 1 in der Sonntag Spalte in den anderen Daten werden nicht die Vorhersage Schritt fehlschlagen. Es wird einfach davon ausgehen, dass der Sonntag eine Wirkung hat, dass die an den anderen Tagen im Durchschnitt (die können oder auch nicht wahr sein).
Eine der Annahmen von Linear / Logistik Regressionen ist zu wenig oder gar keine Multikollinearität; also, wenn die Prädiktorvariablen idealerweise unabhängig voneinander sind, dann gilt das Modell nicht alle möglichen Vielzahl von Faktorstufen sehen müssen. Ein neuer Faktor Stufe (D) ist ein neuer Prädiktor, und können ohne Beeinträchtigung der Vorhersage Fähigkeit der übrigen Faktoren A, B, C auf NA eingestellt werden. Aus diesem Grunde sollte das Modell noch in der Lage sein, Vorhersagen zu machen. Aber Zugabe der neue Ebene D führt das erwartete Schema ab. Das ist das ganze Problem. Einstellen NA-Fixes das.
Das lme4
Paket wird eine neue Ebene behandeln, wenn Sie das Kennzeichen allow.new.levels=TRUE
gesetzt, wenn predict
aufrufen.
Beispiel: Wenn Sie den Tag Faktor der Woche in einer Variablen dow
ist und ein kategorisches Ergebnis b_fail
, könnten Sie führen
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)
Dies ist ein Beispiel mit einer zufälligen Effekten logistischen Regression. Natürlich können Sie regelmäßige Regression ... oder die meisten GLM-Modelle durchzuführen. Wenn Sie möchten, auf dem Bayes-Weg weiter gehen, Blick auf Gelman & Hill ausgezeichnetes Buch und dem Stans Infrastruktur.
Eine schnelle und unsaubere Lösung für Split-Tests, ist selten Wert als „anderen“ neu zu kodieren. Hier ist eine Implementierung:
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)
}
}
Zum Beispiel mit data.table würde der Anruf so etwas wie:
dt[, (xcols) := mclapply(.SD, rare_to_other), .SDcol = xcols] # recode rare levels as other
Dabei gilt xcols
eine beliebige Teilmenge von colnames(dt)
ist.