Capítulo 7 Árboles de decisión
Un árbol de decisión es una estructura ramificada que muestra las diferentes opciones y sus consecuencias. Los puntos en los que hay que tomar decisiones se muestran como nodos, las ramas unen estos nodos y las decisiones últimas son las hojas, donde el camino termina (también se denominan nodos terminales).
Existen varios paquetes de R que permiten hacer árboles de decisión.
7.1 rpart
Esta librería rpart
hace árboles de decisión a partir de tablas. La función principal es rpart()
que crea, a partir de un conjunto de datos, y de una fórmula de predicción, un árbol de decisión que puede usarse para pronosticar con la función predict
.
7.1.1 Ejemplo
Para estos ejemplos vamos a inventar nuevamente unos datos. Tenemos una tabla en la que vienen la altura del padre, de la madre y de un hijo. Y queremos ver su relación.
# creamos los datos de ejemplo
# una tabla con alturas del padre la madre, el sexo del hijo y la altura
redondea5<-function(x,base=5){
as.integer(base*round(x/base))
}
a.padre<- redondea5(rnorm(1000, 168, 25),10)
a.madre<- redondea5(rnorm(1000, 150, 10),10)
s.hijo<-factor(rbinom(1000,1,0.5), levels=c(0,1),labels=c("M","F"))
# creo data.frame
t.alturas<-data.frame(a.padre,a.madre,s.hijo)
# Se calcula la altura del hijo con esta formula
t.alturas$a.hijo<-ifelse(t.alturas$s.hijo == "M",
(t.alturas$a.padre +t.alturas$a.madre)*rnorm(1,1,0.07)/2,
(t.alturas$a.padre +t.alturas$a.madre)*rnorm(1,1,0.05)/2)
t.alturas$a.hijo<-redondea5(t.alturas$a.hijo,10)
str(t.alturas)
## 'data.frame': 1000 obs. of 4 variables:
## $ a.padre: int 160 210 140 180 200 160 190 130 180 180 ...
## $ a.madre: int 140 160 160 150 160 150 140 150 160 140 ...
## $ s.hijo : Factor w/ 2 levels "M","F": 1 1 2 1 2 1 1 2 1 1 ...
## $ a.hijo : int 160 200 160 180 190 170 180 150 180 170 ...
knitr::kable(head(t.alturas,10), "markdown")
a.padre | a.madre | s.hijo | a.hijo |
---|---|---|---|
160 | 140 | M | 160 |
210 | 160 | M | 200 |
140 | 160 | F | 160 |
180 | 150 | M | 180 |
200 | 160 | F | 190 |
160 | 150 | M | 170 |
190 | 140 | M | 180 |
130 | 150 | F | 150 |
180 | 160 | M | 180 |
180 | 140 | M | 170 |
# Crear un modelo de clasificacion con rpart
library(rpart)
library(rpart.plot)
# creamos un modelo de clasificación en el que intervengan todas las variables
# el coeficiente cp nos extiende o acorta el arbol, simplifica resultados
model.alturas1 <- rpart(a.hijo ~ .,
data = t.alturas, method = "class", cp = .02)
# pintamos el modelo
rpart.plot(model.alturas1, fallen.leaves = FALSE,
main = "Arbol de decision de alturas hijo\n(en funcion altura padres)\n",
shadow.col = "gray")
## Warning: All boxes will be white (the box.palette argument will be ignored) because
## the number of classes predicted by the model 10 is greater than length(box.palette) 6.
## To silence this warning use box.palette=0 or trace=-1.
# hacemos un pronostico
h2<-data.frame(a.padre= c(190,150),a.madre= c(180,140),s.hijo= c("M","M"))
predict(model.alturas1, h2,type = "class")
## 1 2
## 190 150
## Levels: 120 130 140 150 160 170 180 190 200 210
7.1.2 Ejemplo 2
Vamos a ver otro ejemplo con los datos del Titanic que trae R por defecto.
data(ptitanic)
#---------------------------------------------------------------------------
sobrevive.model <- rpart(survived ~ ., data = ptitanic, cp = .02)
# cp = .02 for small demo tree
# representamos el modelo
rpart.plot(sobrevive.model,
main = "Supervivientes del Titanic \n (respuesta binaria)")
# representamos el modelo de otra forma
rpart.plot(sobrevive.model, type = 2, clip.right.labs = FALSE,
branch = .6,
box.palette = c( "red","green"), # override default GnBu palette
main = "Supervivientes del Titanic \n")
7.2 overfitting
Los árboles de decisión son bastante problemáticos con la sobrestimacion de parametros, pues la metodología obliga siempre a divisiones paralelas a los ejes de variables y puede generar muchas ramas, pese a que el modelo puede ser muy sencillo.
Son necesarias varias acciones previas siempre antes de emprender la clasificación.
- simplificar los datos, normalizar o realizar acciones que agrupen los datos y eviten la multiplicación de casos cruzados. Por ejemplo el simple hecho de redondear los valores numéricos a base 5 o 10 , puede eliminar de golpe miles de opciones irrelevantes.
- usar los parámetros de
rpart
para simplificar el modelo. Esto se hace con el argumentorpart.control(cp= 0.2, maxdepth = 30, minsplit = 20)
de varias formas:cp
es un control global que simplifica todomaxdepth
indica el máximo numero de ramasminsplit
indica el numero mínimo de ocurrencias de ese conjunto para considerarlo en un grupo.
- Otro aspecto fundamental es que se recomienda dividir los datos de partida en dos conjuntos, uno con el 75% de los registros para entrenamiento y otro con el 25% de los datos para test o comprobación del ajuste del modelo. Esto nos hace simplificar y no sobredimiensionar el modelo.
La función sample()
es muy útil en la tarea de seleccionar una muestra de test y otra de entrenamiento.
# ejemplo de division de una muestra
# contamos el num de registro de la base de datos del titanic
nrow(ptitanic)
## [1] 1309
# calculamos el 75%
num_reg_entrena<-as.integer(0.75*nrow(ptitanic))
# Creamos una muestra aleatoria de registros de entrenamiento
v_titanic_train <- sample(nrow(ptitanic), num_reg_entrena)
# Creamos el conjunto de registros de entrenamiento
titanic_train <- ptitanic[v_titanic_train,]
head(titanic_train)
## pclass survived sex age sibsp parch
## 562 2nd died male 59 0 0
## 1077 3rd died female NA 0 0
## 202 1st died male 54 0 0
## 329 2nd died male 34 1 0
## 1107 3rd died female 41 0 5
## 1219 3rd died male 30 0 0
# Creamos los datos de comprobación o test (notese el -)
titanic_test <- ptitanic[-v_titanic_train,]
Vamos a crear el modelo y entrenarlo
# CReamos un modelo de suervivencia en el titanic
sobrevive.model <- rpart(survived ~ ., data = titanic_train, cp = .02)
# ahrora hacemos predicciones sobre el grupo de test
titanic_test$pred <- predict(sobrevive.model,titanic_test, type = "class")
head(titanic_test)
## pclass survived sex age sibsp parch pred
## 2 1st survived male 0.9167 1 2 survived
## 5 1st died female 25.0000 1 2 survived
## 6 1st survived male 48.0000 0 0 died
## 7 1st survived female 63.0000 1 0 survived
## 11 1st died male 47.0000 1 0 died
## 13 1st survived female 24.0000 0 0 survived
# Examinamos los resultados con la matriz de confusion
table(titanic_test$pred,titanic_test$survived)
##
## died survived
## died 193 66
## survived 10 59
# Calculamos la bondad del modelo sobre el grupo de test
100*mean(titanic_test$pred==titanic_test$survived)
## [1] 76.82927
Como vemos el 77 % de ajuste es un valor alto, pero no infalible.
7.3 Poda de los árboles
Dada la facilidad con la que un árbol se complica, muchos paquetes tienen funciones especiales para cortar, limitar y optimizar el tamaño y la forma de los arboles. Por ejemplo con la librería rpart
lo podemos hacer limitando la profundidad del árbol y el número de divisiones máximo.
El proceso puede hacerse antes o después de crear el árbol, en lo que llamamos pre y post poda de control. En concreto la librería rpart
contiene un parámetro que hemos estado usando el cp, que controla la complejidad del árbol.
# Ejemplo de pre-poda en rpart
require(rpart)
control.poda <- rpart.control(maxdepth = 2, minsplit = 10)
Titanic.model <- rpart(survived ~ .,
data = titanic_train,
method = "class",
control = control.poda)
rpart.plot(Titanic.model)
# cambiando los parametros de poda el modelo es diferente:
control.poda <- rpart.control(maxdepth = 4)
Titanic.model <- rpart(survived ~ .,
data = titanic_train,
method = "class",
control = control.poda)
rpart.plot(Titanic.model)
# Ejemplo de post poda
# el parametro cp, controla la post poda
# podemos ver su influencia dibujando la grafica de cp
plotcp(Titanic.model)
# y simplificar el modelo anterior ya calculado
# como apreciamos a partir de cp=0.1 el modelo se simplifica mucho
rpart.plot(prune(Titanic.model, cp = 0.10))