Arvores de Regressão

Author

Ricardo Accioly

Published

August 20, 2024

Bibliotecas

Avaliando, selecionando dados

data("Boston")
names(Boston)
 [1] "crim"    "zn"      "indus"   "chas"    "nox"     "rm"      "age"    
 [8] "dis"     "rad"     "tax"     "ptratio" "black"   "lstat"   "medv"   
describe(Boston)
        vars   n   mean     sd median trimmed    mad    min    max  range  skew
crim       1 506   3.61   8.60   0.26    1.68   0.33   0.01  88.98  88.97  5.19
zn         2 506  11.36  23.32   0.00    5.08   0.00   0.00 100.00 100.00  2.21
indus      3 506  11.14   6.86   9.69   10.93   9.37   0.46  27.74  27.28  0.29
chas       4 506   0.07   0.25   0.00    0.00   0.00   0.00   1.00   1.00  3.39
nox        5 506   0.55   0.12   0.54    0.55   0.13   0.38   0.87   0.49  0.72
rm         6 506   6.28   0.70   6.21    6.25   0.51   3.56   8.78   5.22  0.40
age        7 506  68.57  28.15  77.50   71.20  28.98   2.90 100.00  97.10 -0.60
dis        8 506   3.80   2.11   3.21    3.54   1.91   1.13  12.13  11.00  1.01
rad        9 506   9.55   8.71   5.00    8.73   2.97   1.00  24.00  23.00  1.00
tax       10 506 408.24 168.54 330.00  400.04 108.23 187.00 711.00 524.00  0.67
ptratio   11 506  18.46   2.16  19.05   18.66   1.70  12.60  22.00   9.40 -0.80
black     12 506 356.67  91.29 391.44  383.17   8.09   0.32 396.90 396.58 -2.87
lstat     13 506  12.65   7.14  11.36   11.90   7.11   1.73  37.97  36.24  0.90
medv      14 506  22.53   9.20  21.20   21.56   5.93   5.00  50.00  45.00  1.10
        kurtosis   se
crim       36.60 0.38
zn          3.95 1.04
indus      -1.24 0.30
chas        9.48 0.01
nox        -0.09 0.01
rm          1.84 0.03
age        -0.98 1.25
dis         0.46 0.09
rad        -0.88 0.39
tax        -1.15 7.49
ptratio    -0.30 0.10
black       7.10 4.06
lstat       0.46 0.32
medv        1.45 0.41
dados <- Boston 
extrato <- dados %>% select(medv, nox, rm)  
summary(extrato)
      medv            nox               rm       
 Min.   : 5.00   Min.   :0.3850   Min.   :3.561  
 1st Qu.:17.02   1st Qu.:0.4490   1st Qu.:5.886  
 Median :21.20   Median :0.5380   Median :6.208  
 Mean   :22.53   Mean   :0.5547   Mean   :6.285  
 3rd Qu.:25.00   3rd Qu.:0.6240   3rd Qu.:6.623  
 Max.   :50.00   Max.   :0.8710   Max.   :8.780  
boxplot(extrato$medv)

Visualizando os dados

## Distribuição de dados na maior parte simétrica com valores na cauda direta 
## maior do que o esperado para uam distribuição simétrica
ggplot(extrato, aes(x=medv)) +
  geom_histogram(bins = round(1+3.322*log10(nrow(extrato)),0))

## Grafico de dispersão nox vs rm 
ggplot(extrato, aes(x=rm, y=nox)) + 
  geom_point()

Arvore de Regressão

Na biblioteca rpart as arvores de regressão são obtidas usando o método anova. Existem alguns controles que podem ser feitos nos parametros da arvore.

Neste exemplo só definimos o menor conjunto de dados numa partição (minsplit) e o parametro de complexidade cp. Qualquer partição/divisão que não melhore o ajuste por um fator de cp não é tentada. Por exemplo, com a partição pela anova, isso significa que o R-quadrado geral deve aumentar pelo valor de cp a cada etapa. O principal papel deste parâmetro é economizar tempo de computação podando divisões que obviamente não valem a pena. Essencialmente, o usuário informa ao programa que qualquer divisão que não melhore o ajuste pelo cp, provavelmente será podada por validação cruzada, e que, portanto, não é necessário persegui-lo.

##Usando rpart para desenvolver a arvore  
library(rpart)
arvreg <- rpart(medv ~ ., 
                data=extrato,
                method="anova", #para arvore de regressão
                control=rpart.control(minsplit=30,cp=0.06))
plot(arvreg)
text(arvreg,pretty=0)

arvreg
n= 506 

node), split, n, deviance, yval
      * denotes terminal node

1) root 506 42716.300 22.53281  
  2) rm< 6.941 430 17317.320 19.93372  
    4) nox>=0.6695 97  2214.391 13.73918 *
    5) nox< 0.6695 333 10296.590 21.73814 *
  3) rm>=6.941 76  6059.419 37.23816  
    6) rm< 7.437 46  1899.612 32.11304 *
    7) rm>=7.437 30  1098.850 45.09667 *

Segmentos

A partir da árvore obtida no item anterior podemos fazer uma representação gráfica das partições obtidas.

ggplot(extrato, aes(x=rm, y=nox)) + 
  geom_point() +
  geom_segment(aes(x = 0, y = 0.6695, xend = 6.941, yend = 0.6695), 
               linetype="dashed", color="red", size=1) +
  geom_vline(xintercept = 6.941, linetype="dashed", color="red", size=1) +
  geom_vline(xintercept = 7.437, linetype="dashed", color="red", size=1) 

  # scale_y_continuous(limits = c(0.3, 1)) +

Treino e Teste com todas as variáveis

Agora vamos trabalhar com o conjunto completo criando um conjunto de treino e teste.

## Vamos criar os conjuntos de treino teste e desenvolver a arvore 
## com todas as variáveis.
library(caret)
set.seed(21)
indice <- createDataPartition(dados$medv, times=1, p=0.75, list=FALSE)
conj_treino <- dados[indice,]
conj_teste <- dados[-indice,]
head(conj_treino)
     crim   zn indus chas   nox    rm  age    dis rad tax ptratio  black lstat
1 0.00632 18.0  2.31    0 0.538 6.575 65.2 4.0900   1 296    15.3 396.90  4.98
3 0.02729  0.0  7.07    0 0.469 7.185 61.1 4.9671   2 242    17.8 392.83  4.03
4 0.03237  0.0  2.18    0 0.458 6.998 45.8 6.0622   3 222    18.7 394.63  2.94
5 0.06905  0.0  2.18    0 0.458 7.147 54.2 6.0622   3 222    18.7 396.90  5.33
6 0.02985  0.0  2.18    0 0.458 6.430 58.7 6.0622   3 222    18.7 394.12  5.21
7 0.08829 12.5  7.87    0 0.524 6.012 66.6 5.5605   5 311    15.2 395.60 12.43
  medv
1 24.0
3 34.7
4 33.4
5 36.2
6 28.7
7 22.9
head(conj_teste)
      crim   zn indus chas   nox    rm  age    dis rad tax ptratio  black lstat
2  0.02731  0.0  7.07    0 0.469 6.421 78.9 4.9671   2 242    17.8 396.90  9.14
10 0.17004 12.5  7.87    0 0.524 6.004 85.9 6.5921   5 311    15.2 386.71 17.10
12 0.11747 12.5  7.87    0 0.524 6.009 82.9 6.2267   5 311    15.2 396.90 13.27
16 0.62739  0.0  8.14    0 0.538 5.834 56.5 4.4986   4 307    21.0 395.62  8.47
19 0.80271  0.0  8.14    0 0.538 5.456 36.6 3.7965   4 307    21.0 288.99 11.69
23 1.23247  0.0  8.14    0 0.538 6.142 91.7 3.9769   4 307    21.0 396.90 18.72
   medv
2  21.6
10 18.9
12 18.9
16 19.9
19 20.2
23 15.2

Arvore de Regressão Treino

## A função rpart tem diversos parametros aqui foi configurado um deles
# cp o parametro de complexidade
# Um valor de cp muito pequeno ocasiona overfitting e um valor muito grande 
# resulta numa arvore muito pequena (underfitting).
# Nos dois casos se diminui o desempenho do modelo.
arvreg1 <- rpart(medv ~ ., 
                data=conj_treino,
                method="anova", #para arvore de regressão
                control=rpart.control(minsplit=30,cp=0.01))
plot(arvreg1)
text(arvreg1,pretty=0)

arvreg1
n= 381 

node), split, n, deviance, yval
      * denotes terminal node

 1) root 381 31196.9300 22.34672  
   2) rm< 6.8375 311 10862.7700 19.42958  
     4) lstat>=14.405 131  2579.9460 14.70534  
       8) crim>=7.006285 53   523.4645 11.23774 *
       9) crim< 7.006285 78   986.1646 17.06154 *
     5) lstat< 14.405 180  3231.2930 22.86778  
      10) rm< 6.5445 145  1867.9540 21.78414 *
      11) rm>=6.5445 35   487.6657 27.35714 *
   3) rm>=6.8375 70  5929.5660 35.30714  
     6) rm< 7.435 49  2010.0620 31.05102  
      12) lstat>=9.65 10   467.4640 23.84000 *
      13) lstat< 9.65 39   889.2800 32.90000 *
     7) rm>=7.435 21   960.7895 45.23810 *

Erros a partir do conjunto de treino

  • O erro relativo (Rel error) é obtido através de 1 - R2
  • O xerror é obtido através da validação cruzadada (10 fold)
  • O xtsd é o desvio padrão dos valores obtidos na validação cruzada.
## Mostra 2 gráficos:
# 1) Variação do R2 aparente e relativo vs número de partições
# 2) Erro Relativo vs número de partições
rsq.rpart(arvreg1)

Regression tree:
rpart(formula = medv ~ ., data = conj_treino, method = "anova", 
    control = rpart.control(minsplit = 30, cp = 0.01))

Variables actually used in tree construction:
[1] crim  lstat rm   

Root node error: 31197/381 = 81.882

n= 381 

        CP nsplit rel error  xerror     xstd
1 0.461731      0   1.00000 1.01028 0.096645
2 0.161924      1   0.53827 0.65330 0.065046
3 0.094840      2   0.37634 0.49315 0.059728
4 0.034308      3   0.28151 0.37245 0.050769
5 0.028069      4   0.24720 0.34074 0.051145
6 0.020942      5   0.21913 0.29254 0.042692
7 0.010000      6   0.19819 0.28562 0.042529

## Mostra a variação do Erro relativo vs cp(parametro de complexidade)
plotcp(arvreg1)

Gráfico de importancia das variáveis

A importancia das variáveis é calculada com base nos resultados das melhores partições

# Gráfico de Importância de variável
var_imp <- arvreg1$variable.importance
nomes_var <- names(var_imp)
var_impdf <- data.frame(Importancia=unname(var_imp), Variavel=nomes_var) %>%
                        arrange(Importancia)
var_impdf$Variavel <- factor(var_impdf$Variavel, levels=var_impdf$Variavel)
ggplot(var_impdf, aes(x=Variavel, y=Importancia)) + 
         geom_col() + 
        coord_flip()

Mostrando a árvore e gerando previsões

# Mostrando a arvore
par(xpd = NA)
plot(arvreg1)
text(arvreg1,pretty=0)

# Fazendo Previsões
previsao1 <- arvreg1 %>% predict(conj_teste)
head(previsao1)
       2       10       12       16       19       23 
21.78414 17.06154 21.78414 21.78414 21.78414 17.06154 
# Calcula os erros de previsão
RMSE(previsao1, conj_teste$medv)
[1] 5.303593

Arvore de Regressão com caret

Aqui vamos usar a biblioteca caret que tem umas facilidades para otimização do cp e apresentação dos resultados

set.seed(21)
## Otimizamos o valor de cp usando um 10-fold cv
# O parametro tuneLength diz para o algoritmo escolher diferentes valores para cp
# O parametro tuneGrid permite decidir que valores cp deve assumir enquanto que o
# tuneLength somente limita o número default de parametros que se usa.
arvreg2 <- train(medv ~ . , data = conj_treino, method = "rpart",
                 trControl = trainControl("cv", number = 10),
                 tuneGrid = data.frame(cp = seq(0.01,0.10, length.out=100)) 
                 )
# Mostra a acurácia vs cp (parametro de complexidade)
plot(arvreg2)

## Indica o melhor valor de cp
arvreg2$bestTune
          cp
4 0.01272727

Desenhando a Árvore

## Apresenta o modelo final de arvore ajustado
par(xpd = NA)
plot(arvreg2$finalModel)
text(arvreg2$finalModel,  digits = 3)

## usando o rpart.plot
library(rpart.plot)
rpart.plot(arvreg2$finalModel)

Previsões

# Regras de Decisão
arvreg2$finalModel
n= 381 

node), split, n, deviance, yval
      * denotes terminal node

 1) root 381 31196.9300 22.34672  
   2) rm< 6.8375 311 10862.7700 19.42958  
     4) lstat>=14.405 131  2579.9460 14.70534  
       8) crim>=7.006285 53   523.4645 11.23774 *
       9) crim< 7.006285 78   986.1646 17.06154 *
     5) lstat< 14.405 180  3231.2930 22.86778  
      10) rm< 6.5445 145  1867.9540 21.78414 *
      11) rm>=6.5445 35   487.6657 27.35714 *
   3) rm>=6.8375 70  5929.5660 35.30714  
     6) rm< 7.435 49  2010.0620 31.05102  
      12) lstat>=11.315 7   367.8886 21.88571 *
      13) lstat< 11.315 42   956.1507 32.57857 *
     7) rm>=7.435 21   960.7895 45.23810 *
# Fazendo Previsões
previsao2 <- arvreg2 %>% predict(conj_teste)
head(previsao2)
       2       10       12       16       19       23 
21.78414 17.06154 21.78414 21.78414 21.78414 17.06154 
# Calcula os erros de previsão
RMSE(previsao2, conj_teste$medv)
[1] 5.304604

Vamos comparar com Regressão Multipla

library(leaps)
## Cria uma função de predição para o leaps
predict.regsubsets <- function(object,newdata,id,...){
  form <- as.formula(object$call[[2]])
  mat <- model.matrix(form,newdata)
  coefi <- coef(object,id=id)
  mat[,names(coefi)]%*%coefi
}
set.seed(21)
envelopes <- sample(rep(1:5,length=nrow(conj_treino)))
table(envelopes)
envelopes
 1  2  3  4  5 
77 76 76 76 76 
erro_cv <- matrix(NA,5,13)
for(k in 1:5){
  melh_ajus <- regsubsets(medv ~ ., data=conj_treino[envelopes!=k,], 
                          nvmax=13,method="forward")
  for(i in 1:13){
    prev <- predict(melh_ajus, conj_treino[envelopes==k,],id=i)
    erro_cv[k,i] <- mean( (conj_treino$medv[envelopes==k]-prev)^2)
  }
}
rmse_cv <- sqrt(apply(erro_cv,2,mean))  # Erro medio quadratico de cada modelo
plot(rmse_cv,pch=19,type="b")          

Obtem a fórmula do modelo

coef(melh_ajus, 11)
  (Intercept)          crim            zn          chas           nox 
 37.051974686  -0.144753575   0.047778839   1.780110617 -14.931943579 
           rm           dis           rad           tax       ptratio 
  3.815637016  -1.500993904   0.349882023  -0.015845438  -0.986230946 
        black         lstat 
  0.009024705  -0.534163142 

Teste com o conjunto de teste

previsao3 <- predict(melh_ajus, conj_teste, 11) 
RMSE(previsao3, conj_teste$medv)
[1] 5.936577

E se usarmos outra semente?

## Vamos criar os conjuntos de treino teste e desenvolver a arvore 
## com todas as variáveis.
library(caret)
set.seed(23)
indice <- createDataPartition(dados$medv, times=1, p=0.75, list=FALSE)
conj_treino <- dados[indice,]

arvreg1s <- rpart(medv ~ ., 
                data=conj_treino,
                method="anova", #para arvore de regressão
                control=rpart.control(minsplit=30,cp=0.01))
plot(arvreg1s)
text(arvreg1s,pretty=0)

arvreg1s
n= 381 

node), split, n, deviance, yval
      * denotes terminal node

 1) root 381 31257.8400 22.41496  
   2) rm< 6.92 321 11786.7500 19.74361  
     4) lstat>=14.4 133  2558.5280 14.88722  
       8) crim>=6.99237 59   866.3051 11.96949 *
       9) crim< 6.99237 74   789.4865 17.21351 *
     5) lstat< 14.4 188  3872.3890 23.17926  
      10) lstat>=5.51 165  2166.8920 22.28970  
        20) lstat>=9.675 86   527.0414 20.76977 *
        21) lstat< 9.675 79  1224.8950 23.94430 *
      11) lstat< 5.51 23   638.2548 29.56087 *
   3) rm>=6.92 60  4925.2370 36.70667  
     6) rm< 7.437 39  1510.0630 31.43077  
      12) ptratio>=18.55 10   750.6440 25.84000 *
      13) ptratio< 18.55 29   339.0703 33.35862 *
     7) rm>=7.437 21   313.5495 46.50476 *

Esta é a principal fragilidade da árvore (única), qualquer mudança na amostra pode trazer uma configuração diferente.