Medidas de desempenho para modelos de Classificação

Nessa aula veremos com mais detalhes como usar as medidas de desempenho para os métodos de classificação. Suponha a base de dados dividida em treino e teste e que a base de treino foi usada para ajustar os modelos.

Para os problemas de regressão a variável resposta \(Y\) é qualitativa. Vamos supor que ela será binária, isto é, admite apenas duas classe. O problema com mais de duas classes será discutido mais para frente.

Comparação entre modelos

Suponha que um modelo de classificação \(k\) realizou a previsão \(\hat{y}_i^k\) para a i-ésima observação da variável \(Y\). Como se trata de um modelo de classificação binário, as classes serão 0 ou 1 e a previsão \(\hat{y}_i^k\) é um valor no intervalo \((0,1)\).

Seja \(y_i\) a classe real observada para a i-ésima observação da variável \(Y\). Veja que temos duas opções, \(y_i-0\) ou \(y_i=1\). Para comparar os diferentes modelos de classificação vamos seguir os seguintes passos.

Primeiro calcula-se a previsão da variável resposta \(Y\) para cada unidade amostral, para cada um dos modelos ajustados. A tabela abaixo representa os valores calculados.

i Classe real Previsão para o Modelo 1 Previsão para o Modelo 2 Previsão para o Modelo 3
1 \(y_1\) \(\hat y_1^1\) \(\hat y_1^2\) \(\hat y_1^3\)
2 \(y_2\) \(\hat y_2^1\) \(\hat y_2^2\) \(\hat y_2^3\)
3 \(y_3\) \(\hat y_3^1\) \(\hat y_3^2\) \(\hat y_3^3\)
N \(y_N\) \(\hat y_N^1\) \(\hat y_N^2\) \(\hat y_N^3\)

Uma vez conhecidas as previsões para a variável resposta considerando todos os modelos ajusatados, podemos calcular a EC (entropia cruzada) e usar essa medida como comparação de qualidade do ajuste.

\[ EC = - \dfrac{1}{N} \sum_{i=1}^N \left( y_i\ln(\hat{y}_i) + (1-y_i)\ln(1-\hat{y}_i) \right) \]

Veja que quando \(\hat{y}_i\) está próximo da classe real a parcela \(i\) do somatório é bem pequena e quando \(\hat{y}_i\) está próxima da classificação errada, a parcela \(i\) do somatório é bem grande.

Veja que a conta acima pode ser feita considerando tanto a base de treino quanto a base de teste. Em geral vamos medir a EC para as duas bases.

Uma vez calculada as EC podemos criar a seguinte tabela, que vai ajudar a decidir qual dos modelos é mais adequado.

Modelo MSE na base de treino MSE na base de teste
1 \(EC^1_{treino}\) \(EC^1_{teste}\)
2 \(EC^2_{treino}\) \(EC^2_{teste}\)
3 \(EC^3_{treino}\) \(EC^3_{teste}\)

O modelo com menor EC na base de teste será aquele que parece ter melhor desempenho. Porém observar o valor do EC na base de treino pode trazer informações importantes. Por exemplo, se um modelo apresenta EC na base de treino bem menor que a EC na base de teste, principalmente quando comparado com os outros modelos, percebemos que para este modelo em questão está ocorrendo o sobreajuste (overfiting).

Assim como no caso da regressão, a comparação da EC pode indicar a importância (ou não) de uma covariável.

A entropia cruzada é a medida adequada para comparar o desempenho de modelos de classificação.

Validação Cruzada

A validação cruzada (k-fold cross-validation) também pode ser adotada para comparar modelos de classificação. O processo é semelhante aquele apresentado para os modelos de regressão, porém em vez de compararmos as estatísticas para o MSE serão comparadas as estatísticas para a EC.

Neste processo a base de dados é dividido aleatoriamente em \(k\) pedaços de (aproximadamente) mesmo tamanho. Uma vez a base dividida, considera-se um dos \(k\) pedaços a base de teste e os outros \(k-1\) pedaços a base de treino. O modelo é ajustado na base de treino e é calculado o erro (EC) na base de teste. Esse processo é feito \(k\) vezes, sendo que em cada uma das vezes uma das partes é utilizada como base de teste e as outras \(k-1\) como base de treino. Ao final do processo, em vez de um valor da EC teremos \(k\) valores para cada modelo.

  1. Divide o dataset em \(k\) partes;

  2. Faz \(i=1\);

  3. Considere a parte \(i\) como base de teste e o restante da base de dados como base de treino.

  4. Ajuste cada modelo para a base de treino.

  5. Calcule o EC na base de teste para cada modelo ajustado na base de treino.

  6. Faz \(i = i + 1\) e se \(i \le k\), volte para o passo 3.

Matriz de Confusão

Os modelos de classificação retornam como previsão para a \(i\)-ésima observação um valor \(\hat{y}_i^k \in (0,1)\). Mas o objetivo final é prever a classe da observação \(i\).

Uma vez escolhido o modelo de classificação que melhor se ajusta aos dados, podemos realizar agora a previsão da classe para a observação \(i\). Suponha o seguinte critério para a escolha da classe prevista:

\[ \hat{c}_i = \left\{ \begin{array}{ll} 0 & \hbox{, se } \hat{y}_i < q\\ 1 & \hbox{, se } \hat{y}_i \ge q\\ \end{array} \right. \] Para entender melhor a capacidade de previsão do modelo de classificação queremos encontrar as medidas de Acurácia, Precisão, Sensibilidade, entre outras. Estas medidas serão definidas a partir da matriz de confusão, que é formada pela contagem de classes reais e classes previstas.

Real 0 Real 1
Prev 0 VN FN
Prev 1 FP VP

VN = verdadeiro negativo = número de observações iguais a 0 que foram previstas como 0.

FN = falso negativo = número de observações iguais a 1 que foram previstas como 0.

FP = falso positivo = número de observações iguais a 0 que foram previstas como 1.

VP = verdadeiro positivo = número de observações iguais a 1 que foram previstas como 1.

Quanto maior o número de observações na diagonal principal da matriz de confusão melhor. A partir desta tabela podemos calcular algumas medidas de desempenho para os modelos de classificação.

Acurácia

A acurácia é a taxa de acerto do classificador. Ela é a proporção de predições corretas dentre todas as predições.

\[ Acurácia = \dfrac{V P + V N}{V P + V N + FP + FN} \]

Sensibilidade (ou Recall)

A sensibilidade é a taxa de acerto dos casos positivos. Ela é a proporção de casos positivos que foram corretamente classificados como positivos.

\[ Sensibilidade = \dfrac{VP}{VP + FN} \]

Especificidade

A especificidade é a taxa de acerto dos casos negativos. Ela é a proporção dos casos negativos que foram corretamente classificados como negativos.

\[ Especificidade = \dfrac{VN}{V N + FP} \]

Precisão

A precisão é a taxa de acerto dentras previsões positivas. Ela é a proporção dos acertos entre os casos classificados como positivos.

\[ Precisão = \dfrac{VP}{V P + FP} \]

F1-Score

É uma combinação da Precisão e do Recall que na prática é a média harmônica entre a Precisão e o Recall.

\[ F1-score = 2 \dfrac{Precisão \times Recall}{Precisão + Recall} \]

Curva ROC

Mas qual valor escolher para \(q\)? A escolha do valor de corte \(q\) pode ser feita a partir da curva ROC. A curva ROC é uma curva parametrizada pelo valor \(q\) definida por:

\[ ROC(q) = (1-Especificidade(q)\ , \ Sensibilidade(q)) \ , \quad q \in (0,1) \]

A Figura 1 mostra como em geral é a curva ROC.

Curva ROC
Figura 1: Curva ROC

Vamos escolher o valor de \(q\) que gerou o ponto mais acima e à esquerda. O valor da área embaixo da curva ROC (AUC) também é uma medida de qualidade do ajuste bastante usada.

Exemplo

Vamos usar o exemplo da aula prática e ajustar diferentes modelos de redes neurais perceptron camada única para prever se um cliente é considerado de alto custo ou não. A difereça entre os diferentes modelos será apenas as covariáveis usadas como entrada.

Carregar pacotes necessários

library(caret)
library(tidyverse)
library(neuralnet)

Leitura da base de dados

base = read.csv2("insurance.csv",sep = ",",dec=".")
base = tibble::as_tibble(base)
base = base |> 
  mutate(altocusto = ifelse(base$charges > 15000,"sim","nao")) 
base = base |> select(-charges)

Divisão entre treino e teste

set.seed(123456789)
N = dim(base)[1]
indices_treino = createDataPartition(1:N,p=0.7)[[1]]
base_treino = base[indices_treino,]
base_teste  = base[-indices_treino,]

Padronização das variáveis quantitativas

scale = scale(base_treino$age)
age_ = scale[,1] 
media_age = attr(scale,"scaled:center")
dp_age = attr(scale,"scaled:scale")

scale = scale(base_treino$bmi)
bmi_ = scale[,1] 
media_bmi = attr(scale,"scaled:center")
dp_bmi = attr(scale,"scaled:scale")

scale = scale(base_treino$children)
children_ = scale[,1] 
media_children = attr(scale,"scaled:center")
dp_children = attr(scale,"scaled:scale")

#completar como ficam as variaveis quantitativas depos da base modificada
base_treino_ = base_treino |> mutate(age = age_,
                                     bmi = bmi_,
                                     children = children_) 

Tratamento para as variáveis categóricas

matriz_treino_ <- model.matrix( ~ age + sex + bmi + children + smoker + region + altocusto, data = base_treino_)
#colnames(matriz_treino_)

Ajuste dos modelos

#modelo completo
modelo_1 = neuralnet(altocustosim ~ age + sexmale + bmi + children + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)

#modelo sem idade
modelo_2 = neuralnet(altocustosim ~ sexmale + bmi + children + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)

#modelo sem sexo
modelo_3 = neuralnet(altocustosim ~ age + bmi + children + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)

#modelo sem bmi
modelo_4 = neuralnet(altocustosim ~ age + sexmale + children + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)

#modelo sem children
modelo_5 = neuralnet(altocustosim ~ age + sexmale + bmi  + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)

#modelo sem smoker
modelo_6 = neuralnet(altocustosim ~ age + sexmale + bmi + children + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)

#modelo sem region
modelo_7 = neuralnet(altocustosim ~ age + sexmale + bmi + children + smokeryes, data = matriz_treino_, hidden = 0,linear.output = FALSE)

Previsão na base de treino

prev_treino_1 = modelo_1$net.result[[1]][,1]
prev_treino_2 = modelo_2$net.result[[1]][,1]
prev_treino_3 = modelo_3$net.result[[1]][,1]
prev_treino_4 = modelo_4$net.result[[1]][,1]
prev_treino_5 = modelo_5$net.result[[1]][,1]
prev_treino_6 = modelo_6$net.result[[1]][,1]
prev_treino_7 = modelo_7$net.result[[1]][,1]

EC na base de treino

EC = function(real,previsao){
  n = length(real)
  ec = -sum(ifelse(real==1,log(previsao),log(1-previsao)))/n
  return(ec)
}
classe_real_treino = matriz_treino_[,"altocustosim"]
EC_treino_1 = EC(classe_real_treino,prev_treino_1)
EC_treino_2 = EC(classe_real_treino,prev_treino_2)
EC_treino_3 = EC(classe_real_treino,prev_treino_3)
EC_treino_4 = EC(classe_real_treino,prev_treino_4)
EC_treino_5 = EC(classe_real_treino,prev_treino_5)
EC_treino_6 = EC(classe_real_treino,prev_treino_6)
EC_treino_7 = EC(classe_real_treino,prev_treino_7)

Processamento na base de teste

base_teste_ = base_teste |> mutate(age = (age - media_age)/dp_age ,
                                   bmi = (bmi - media_bmi)/dp_bmi,
                                   children = (children - media_children)/dp_children)

matriz_teste_ <- model.matrix( ~ age + sex + bmi + children + smoker + region + altocusto, data = base_teste_)

Previsão na base de teste

prev_teste_1 = (modelo_1 |> neuralnet::compute(matriz_teste_))$net.result[,1]
prev_teste_2 = (modelo_2 |> neuralnet::compute(matriz_teste_))$net.result[,1]
prev_teste_3 = (modelo_3 |> neuralnet::compute(matriz_teste_))$net.result[,1]
prev_teste_4 = (modelo_4 |> neuralnet::compute(matriz_teste_))$net.result[,1]
prev_teste_5 = (modelo_5 |> neuralnet::compute(matriz_teste_))$net.result[,1]
prev_teste_6 = (modelo_6 |> neuralnet::compute(matriz_teste_))$net.result[,1]
prev_teste_7 = (modelo_7 |> neuralnet::compute(matriz_teste_))$net.result[,1]

EC na base de teste

classe_real_teste = matriz_teste_[,"altocustosim"]

EC_teste_1 = EC(classe_real_teste,prev_teste_1)
EC_teste_2 = EC(classe_real_teste,prev_teste_2)
EC_teste_3 = EC(classe_real_teste,prev_teste_3)
EC_teste_4 = EC(classe_real_teste,prev_teste_4)
EC_teste_5 = EC(classe_real_teste,prev_teste_5)
EC_teste_6 = EC(classe_real_teste,prev_teste_6)
EC_teste_7 = EC(classe_real_teste,prev_teste_7)

Comparação entre EC de diferentes modelos

Modelo treino teste Observações
1 0.2844 0.1633 completo
2 0.2812 0.1797 sem idade
3 0.284 0.1635 sem sexo
4 0.2805 0.1692 sem bmi
5 0.2803 0.176 sem nº filhos
6 0.5824 0.5542 sem smoker
7 0.2913 0.1635 sem região
Tabela 1: Comparação da EC na base de treino e teste.

Tabela 2: Comparação do MSE na base de treino e teste.

Quais interpretações podemos tirar da Tabela 1 e da Figura 2 ?

  • A variável smoker traz muita informação para o modelo.

  • A variável retirada da covariável sex até melhorou a EC na base de teste.

  • As outras variáveis parecem não contribuir muito.

10-fold cross-validation

K = 10
N = dim(base)[1]
folds = createFolds(1:N, k = K, list = TRUE, returnTrain = FALSE)

EC_treino = matrix(NA,ncol=7,nrow = K)
EC_teste  = matrix(NA,ncol=7,nrow = K)

for(k in 1:K){
  
  base_treino = base[-folds[[k]],]
  
  scale = scale(base_treino$age)
  age_ = scale[,1] 
  media_age = attr(scale,"scaled:center")
  dp_age = attr(scale,"scaled:scale")
  
  scale = scale(base_treino$bmi)
  bmi_ = scale[,1] 
  media_bmi = attr(scale,"scaled:center")
  dp_bmi = attr(scale,"scaled:scale")
  
  scale = scale(base_treino$children)
  children_ = scale[,1] 
  media_children = attr(scale,"scaled:center")
  dp_children = attr(scale,"scaled:scale")
  
  base_treino_ = base_treino |> mutate(age = age_,
                                       bmi = bmi_,
                                       children = children_)

  matriz_treino_ = model.matrix( ~ age + sex + bmi + children + smoker + region + altocusto, data = base_treino_)
  
  modelos = list()
    
  #modelo completo
  modelos[[1]] = neuralnet(altocustosim ~ age + sexmale + bmi + children + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)
    
  #modelo sem idade
  modelos[[2]] = neuralnet(altocustosim ~ sexmale + bmi + children + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)
    
  #modelo sem sexo
  modelos[[3]] = neuralnet(altocustosim ~ age + bmi + children + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)
    
  #modelo sem bmi
  modelos[[4]] = neuralnet(altocustosim ~ age + sexmale + children + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)
    
  #modelo sem children
  modelos[[5]] = neuralnet(altocustosim ~ age + sexmale + bmi + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)
    
  #modelo sem smoker
  modelos[[6]] = neuralnet(altocustosim ~ age + sexmale + bmi + children + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)
    
  #modelo sem region
  modelos[[7]] = neuralnet(altocustosim ~ age + sexmale + bmi + children + smokeryes, data = matriz_treino_, hidden = 0,linear.output = FALSE)
    
    
  base_teste = base[folds[[k]],]
  
  base_teste_ = base_teste |> 
    mutate(age = (age - media_age)/dp_age,
           bmi = (bmi - media_bmi)/dp_bmi,
           children = (children - media_children)/dp_children)
     
  matriz_teste_ = model.matrix( ~ age + sex + bmi + children + smoker + region + altocusto,data = base_teste_)
     
  altocusto_treino = matriz_treino_[,"altocustosim"]
  altocusto_teste = matriz_teste_[,"altocustosim"]

  for(i in 1:7){
    
    prev_treino = modelos[[i]]$net.result[[1]][,1]
    EC_treino[k,i] = EC(altocusto_treino,prev_treino)
    
    prev_teste = (modelos[[i]] |> neuralnet::compute(matriz_teste_))$net.result[,1]
    EC_teste[k,i] =  EC(altocusto_teste,prev_teste)
  }
}
Modelo Min. 1st Qu. Median Mean 3rd Qu. Max. obs
1 0.234 0.283 0.387 0.39 0.475 0.563 completo
2 0.235 0.249 0.252 0.25 0.253 0.257 sem idade
3 0.235 0.282 0.388 0.39 0.498 0.544 sem sexo
4 0.234 0.252 0.301 0.302 0.327 0.412 sem bmi
5 0.234 0.249 0.251 0.249 0.252 0.257 sem children
6 0.566 0.571 0.572 0.573 0.574 0.579 sem smoker
7 0.238 0.269 0.341 0.359 0.438 0.515 sem região
Tabela 3: Comparação entre estatísticas da EC na base de treino.

Modelo Min. 1st Qu. Median Mean 3rd Qu. Max. obs
1 0.204 0.331 0.384 0.416 0.476 0.735 completo
2 0.193 0.227 0.244 0.258 0.267 0.405 sem idade
3 0.203 0.318 0.388 0.415 0.517 0.672 sem sexo
4 0.21 0.25 0.291 0.324 0.376 0.494 sem bmi
5 0.198 0.236 0.237 0.259 0.27 0.38 sem children
6 0.52 0.565 0.579 0.578 0.594 0.642 sem smoker
7 0.2 0.283 0.342 0.377 0.446 0.668 sem região
Tabela 4: Comparação entre estatísticas do MSE na base de teste.

Tabela 5: Comparação entre média da EC para a validação cruzada.

Tabela 6: Comparação entre as medianas da EC para a validação cruzada.

Classificação

Vamos seguir com os modelos 2 e 5, sem o primeiro sem a variável age e o segundo sem a variável children.

modelo_2 = neuralnet(altocustosim ~ sexmale + bmi + children + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)
prev_treino_2 = modelo_2$net.result[[1]][,1]
prev_teste_2 = (modelo_2 |> neuralnet::compute(matriz_teste_))$net.result[,1]

modelo_5 = neuralnet(altocustosim ~ age + sexmale + bmi + smokeryes + regionnorthwest + regionsoutheast +  regionsouthwest, data = matriz_treino_, hidden = 0,linear.output = FALSE)
prev_treino_5 = modelo_5$net.result[[1]][,1]
prev_teste_5 = (modelo_5 |> neuralnet::compute(matriz_teste_))$net.result[,1]
library(pROC)
classe_treino = matriz_treino_[,"altocustosim"]
roc2 = roc(response = classe_treino, predictor = prev_treino_2)
plot.roc(roc2,print.auc = TRUE)

q2 = coords(roc2, "best", ret = "threshold")[1,1]
roc5 = roc(response = classe_treino, predictor = prev_treino_5)
plot.roc(roc5,legacy.axes=TRUE,print.auc = TRUE)

q5 = coords(roc5, "best", ret = "threshold")[1,1]

Vamos computar a classificação e as medidas de qualidade da classificação para o Modelo 2.

classe_treino_pred_mod_2 = ifelse(prev_treino_2 < q2,0,1)
tabela_treino_2 = table(prev2=classe_treino_pred_mod_2,real=classe_treino)
CM_treino_2 = confusionMatrix(tabela_treino_2,positive = "1")
classe_teste = matriz_teste_[,"altocustosim"]
classe_teste_pred_mod_2 = ifelse(prev_teste_2 < q2,0,1)
tabela_teste_2 = table(prev2=classe_teste_pred_mod_2,real=classe_teste)
CM_teste_2 = confusionMatrix(tabela_teste_2,positive = "1")
Tabela 7: .