¿A dónde va Vicente?
Cuando estamos haciendo un modelo y tratamos con variables categóricas como predictoras, hay que ser muy cuidadoso. Por ejemplo hay que tener en cuenta qué pasa cuándo tenemos un nuevo nivel en el conjunto de datos a predecir que no estaba en el de entrenamiento. Por ejemplo, si estoy utilizando un algoritmo moderno tipo xgboost, y tengo como variable predictora la provincia. ¿Qué pasa si en el conjunto de entrenamiento no tengo datos de “Granada”, pero en el de predicción si?
En el xgboost por defecto las categóricas se codifican con One-Hot encoder, por lo que al no tener datos de Granada en entrenamiento a la hora de predecir la fila de Granada siempre va a tirar hacia el 0, por ejemplo, un corte en uno de los árboles podría ser Almeria = 0 para la izquierda y Almeria = 1 para la derecha. Esto es lo que suelen hacer la mayoría de las implementaciones. Pero cabe preguntarse si es la mejor solución. Otra alternativa podría ser, dado que tengo que predecir para un nivel no visto en entrenamiento, podría asignarle el valor del target que había en el nodo superior. Esta decisión plantea el problema de como sigues el proceso de partición de datos del árbol. Otra posible decisión podría ser recorrer todos los caminos posibles y promediar. En el caso anterior sería ver qué predicción acaba teniendo cuando Almería = 0 y cuando Almería = 1 y promediar. Sería una solución más justa, aunque plantea el problema de tener que recorrer más ramas.
Otra solución es en cada corte que implique a la variable categórica en cuestión, tirar hacia dónde van la mayoría de los caso “¿a dónde va Vicente? A dónde va la gente”. Esta es la solución que tiene la gente de h2o en su implementación de los Random Forest o de los Gradient Boosting. Dicen textualmente.
What happens when you try to predict on a categorical level not seen during training? Unseen categorical levels are turned into NAs, and thus follow the same behavior as an NA. If there are no NAs in the training data, then unseen categorical levels in the test data follow the majority direction (the direction with the most observations). If there are NAs in the training data, then unseen categorical levels in the test data follow the direction that is optimal for the NAs of the training data.
Ejemplo
Iniciamos h2o y cargamos datos
## Not run:
library(h2o)
h2o.init( max_mem_size = "25G")
#> Connection successful!
#>
#> R is connected to the H2O cluster:
#> H2O cluster uptime: 4 minutes 48 seconds
#> H2O cluster timezone: Europe/Madrid
#> H2O data parsing timezone: UTC
#> H2O cluster version: 3.38.0.1
#> H2O cluster version age: 1 month and 28 days
#> H2O cluster name: H2O_started_from_R_jose_ltm884
#> H2O cluster total nodes: 1
#> H2O cluster total memory: 21.82 GB
#> H2O cluster total cores: 12
#> H2O cluster allowed cores: 12
#> H2O cluster healthy: TRUE
#> H2O Connection ip: localhost
#> H2O Connection port: 54321
#> H2O Connection proxy: NA
#> H2O Internal Security: FALSE
#> R Version: R version 4.2.2 Patched (2022-11-10 r83330)
Importamos los datos del titanic. Ponemos como variables predictoras de la supervivencia, solo la clase y el sexo.
<- "https://s3.amazonaws.com/h2o-public-test-data/smalldata/gbm_test/titanic.csv"
f <- h2o.importFile(f)
titanic #>
|
| | 0%
|
|====== | 8%
|
|======================================================================| 100%
'survived'] <- as.factor(titanic['survived'])
titanic[<- c("pclass","sex")
predictors <- "survived"
response
# convertimos la clase a factor
$pclass <- as.factor(titanic$pclass)
titanich2o.getTypes(titanic$pclass)
#> [[1]]
#> [1] "enum"
Partimos en train y test
<- h2o.splitFrame(data = titanic, ratios = .8, seed = 1234)
splits <- splits[[1]]
train <- splits[[2]]
valid
h2o.table(train$pclass)
#> pclass Count
#> 1 1 260
#> 2 2 223
#> 3 3 571
#>
#> [3 rows x 2 columns]
h2o.table(train$sex)
#> sex Count
#> 1 female 387
#> 2 male 667
#>
#> [2 rows x 2 columns]
h2o.table(valid$pclass)
#> pclass Count
#> 1 1 63
#> 2 2 54
#> 3 3 138
#>
#> [3 rows x 2 columns]
Y ahora cambio en test para que aparezcan valores en pclass y en sex que no están en train.
$pclass = h2o.ifelse(valid$pclass == "3", "unknown", valid$pclass)
valid$sex = h2o.ifelse(valid$sex == "male", "unknown", valid$sex )
valid
h2o.table(valid$pclass)
#> pclass Count
#> 1 1 63
#> 2 2 54
#> 3 unknown 138
#>
#> [3 rows x 2 columns]
h2o.table(valid$sex)
#> sex Count
#> 1 female 79
#> 2 unknown 176
#>
#> [2 rows x 2 columns]
Para ver bien qué sucede con los casos en que tenemos nivel nuevo en clase y sexo nos quedamos con el siguiente conjunto de datos a predecir
<- valid[valid$pclass== "unknown" & valid$sex == "unknown",]
test
test#> pclass survived name sex age sibsp parch ticket
#> 1 unknown 0 Abbott Master. Eugene Joseph unknown 13 0 2 NaN
#> 2 unknown 1 Abelseth Mr. Olaus Jorgensen unknown 25 0 0 348122
#> 3 unknown 0 Ali Mr. Ahmed unknown 24 0 0 NaN
#> 4 unknown 0 Andersen Mr. Albert Karvin unknown 32 0 0 NaN
#> 5 unknown 0 Andersson Mr. Anders Johan unknown 39 1 5 347082
#> 6 unknown 0 Andreasson Mr. Paul Edvin unknown 20 0 0 347466
#> fare cabin embarked boat body home.dest
#> 1 20.2500 <NA> S NaN NaN East Providence RI
#> 2 7.6500 F G63 S NaN NaN Perkins County SD
#> 3 7.0500 <NA> S NaN NaN <NA>
#> 4 22.5250 <NA> S NaN 260 Bergen Norway
#> 5 31.2750 <NA> S NaN NaN Sweden Winnipeg MN
#> 6 7.8542 <NA> S NaN NaN Sweden Chicago IL
#>
#> [99 rows x 14 columns]
Modelo xgboost
Hacemos un sólo árbol
<- h2o.xgboost(
modeloxgseed = 155,
x = predictors,
y = response,
max_depth = 3,
training_frame = train,
ntrees =1
)#>
|
| | 0%
|
|======================================================================| 100%
Y al predecir, nos da un warning que nos dice ¡¡ojo, tengo nuevos niveles que no estaban en train!! . Aún así , no casca y devuelve una predicción
h2o.predict(modeloxg, test)
#>
|
| | 0%
|
|======================================================================| 100%
#> predict p0 p1
#> 1 0 0.6016703 0.3983298
#> 2 0 0.6016703 0.3983298
#> 3 0 0.6016703 0.3983298
#> 4 0 0.6016703 0.3983298
#> 5 0 0.6016703 0.3983298
#> 6 0 0.6016703 0.3983298
#>
#> [99 rows x 3 columns]
h2o.predict_leaf_node_assignment(modeloxg, test)
#> T1.C1
#> 1 LL
#> 2 LL
#> 3 LL
#> 4 LL
#> 5 LL
#> 6 LL
#>
#> [99 rows x 1 column]
Podemos extraer información del árbol con
<- h2o.getModelTree(model = modeloxg, tree_number = 1)
arbol_ind_xg
<- function(arbol_ind){
NodesInfo for (i in 1:length(arbol_ind)) {
<-
info sprintf(
"Node ID %s has left child node with index %s and right child node with index %s The split feature is %s. The NA direction is %s",
@node_ids[i],
arbol_ind@left_children[i],
arbol_ind@right_children[i],
arbol_ind@features[i],
arbol_ind@nas[i]
arbol_ind
)print(info)
}}
NodesInfo(arbol_ind_xg)
#> [1] "Node ID 0 has left child node with index 1 and right child node with index 2 The split feature is sex.female. The NA direction is LEFT"
#> [1] "Node ID 1 has left child node with index 3 and right child node with index 4 The split feature is pclass.1. The NA direction is LEFT"
#> [1] "Node ID 2 has left child node with index 5 and right child node with index 6 The split feature is pclass.3. The NA direction is LEFT"
#> [1] "Node ID 3 has left child node with index -1 and right child node with index -1 The split feature is NA. The NA direction is NA"
#> [1] "Node ID 4 has left child node with index -1 and right child node with index -1 The split feature is NA. The NA direction is NA"
#> [1] "Node ID 5 has left child node with index -1 and right child node with index -1 The split feature is NA. The NA direction is NA"
#> [1] "Node ID 6 has left child node with index -1 and right child node with index -1 The split feature is NA. The NA direction is NA"
Y vemos que nos da información de hacia dónde van los NA (los niveles nuevos no vistos en train) y siempre van hacia la izquierda.
Podemos pintar el árbol. Script Y vemos que los NA, siempre van hacia los 0’s.
## importo funciones , encontradas por internet, como no, para pintar el árbol
source("plot_h2o_tree.R")
= createDataTree(arbol_ind_xg) titanicDataTree_XG
plotDataTree(titanicDataTree_XG, rankdir = "TB")
Modelo h2o.gbm
Veamos qué hace la implementación de h2o.
<- h2o.gbm(
gbm_h2o seed = 155,
x = predictors,
y = response,
max_depth = 3,
distribution = "bernoulli",
training_frame = train,
ntrees = 1
)#>
|
| | 0%
|
|======================================================================| 100%
h2o.predict(gbm_h2o, test)
#>
|
| | 0%
|
|======================================================================| 100%
#> predict p0 p1
#> 1 0 0.6375663 0.3624337
#> 2 0 0.6375663 0.3624337
#> 3 0 0.6375663 0.3624337
#> 4 0 0.6375663 0.3624337
#> 5 0 0.6375663 0.3624337
#> 6 0 0.6375663 0.3624337
#>
#> [99 rows x 3 columns]
h2o.predict_leaf_node_assignment(gbm_h2o, test)
#> T1.C1
#> 1 LLR
#> 2 LLR
#> 3 LLR
#> 4 LLR
#> 5 LLR
#> 6 LLR
#>
#> [99 rows x 1 column]
Y pintando lo mismo
<- h2o.getModelTree(model = gbm_h2o, tree_number = 1)
arbol_ind NodesInfo(arbol_ind )
#> [1] "Node ID 0 has left child node with index 1 and right child node with index 2 The split feature is sex. The NA direction is LEFT"
#> [1] "Node ID 1 has left child node with index 3 and right child node with index 4 The split feature is pclass. The NA direction is LEFT"
#> [1] "Node ID 2 has left child node with index 5 and right child node with index 6 The split feature is pclass. The NA direction is RIGHT"
#> [1] "Node ID 3 has left child node with index 7 and right child node with index 8 The split feature is pclass. The NA direction is RIGHT"
#> [1] "Node ID 11 has left child node with index -1 and right child node with index -1 The split feature is NA. The NA direction is NA"
#> [1] "Node ID 12 has left child node with index -1 and right child node with index -1 The split feature is NA. The NA direction is NA"
#> [1] "Node ID 6 has left child node with index 9 and right child node with index 10 The split feature is pclass. The NA direction is RIGHT"
#> [1] "Node ID 13 has left child node with index -1 and right child node with index -1 The split feature is NA. The NA direction is NA"
#> [1] "Node ID 14 has left child node with index -1 and right child node with index -1 The split feature is NA. The NA direction is NA"
#> [1] "Node ID 15 has left child node with index -1 and right child node with index -1 The split feature is NA. The NA direction is NA"
#> [1] "Node ID 16 has left child node with index -1 and right child node with index -1 The split feature is NA. The NA direction is NA"
Al pintar vemos un par de cosas curiosas, en primer lugar, h2o.gbm no ha codificado con one-hot las variables categóricas, esto permite por ejemplo que se pueden obtener reglas de corte como Izquierda:(Madrid, Barcelona, Valencia), Derecha: (resto de provincias), mientras que One Hot ese tipo de partición requiere más profundidad en el árbol. Y en segundo lugar vemos que por ejemplo los NA’s (y los nuevos niveles en test) de la variable pclass en un nodo van junto con p_class (2,3), en otro junto con p_class=1 y en otro junto p_class=3. El criterio elegido es en cada corte, los Nas y por ende los nuevos niveles no vistos en train van hacia dónde va la gente.
= createDataTree(arbol_ind)
titanicDataTree plotDataTree(titanicDataTree, rankdir = "TB")
Nota: Los valores de prediction que saca plotDataTree no son las predicciones del modelo, sino las raw que saca ese árbol en particular. Como en los modelos de gradient boosting se va construyendo cada árbol sobre los errores del anterior, ni siquiera es la probabilidad en escala logit. He buscado en la docu de h2o pero no está claro qué es este valor. Eso sí, las ramas en el árbol están bien.