Olá quarenteners, como tá seu recorde de séries maratonadas?

Hoje passei, depois de muitos meses é verdade, para falar um pouco sobre uma métrica muito bem-quista para avaliar performance de modelos de classificação binária, a métrica KS. O motivo é simples, dentre as métricas de modelo de classificação mais adotadas ( Acurácia, sensibilidade, F1, curva ROC) ela é a mais caixa preta, então vamos desvendar um pouco sobre ela. Vale avisar que para uma boa compreensão desse post você precisa conhecer o básico sobre modelos de classificação, pois vamos apenas relembrar alguns conceitos e pôr a mão na massa.

Antes de começar vamos relembrar o conceito de algumas métricas para justificar a importância da KS, essas métricas são: Taxa de Verdadeiro Positivo (TVP), Taxa de Verdadeiro Negativo (TVN), Taxa de Falso Positivo (TFP) e Taxa de Falso Negativo (TFN), considerando uma classificação binária onde 1 é positivo e 0 é negativo.

  • TVP , é o quanto dos meus positivos são de fato classificados pelo modelo como positivos. VP/(VP + FN), métrica conhecida como ‘sensibilidade’.

  • TVN , é o quanto dos meus negativos são de fato classificados pelo modelo como negativos. VN/(VN + FP), métrica conhecida como ‘especificidade’.

  • TFP , é o quanto dos meus negativos são classificados como positivos pelo modelo. FP/(FP + VN), métrica conhecida como Erro tipo I.

  • TVP , é o quanto dos meus positivos são classificados como negativos pelo modelo. FN/(FN + VP), métrica conhecida como Erro tipo II.

Observe que essas métricas dão resultados específicos e se avaliadas separadamente, podem dar a falsa impressão de que temos um modelo bem ajustado, por exemplo, o modelo pode ter baixa TFP e alta TVP ao mesmo tempo, inclusive esse tipo de problema é comum quando a variável binária que queremos prever é desbalanceada, pois se o modelo não recebe informação suficiente sobre uma das classes ele provavelmente não vai saber diferenciá-la da classe concorrente.

Agora sim é possível partir para a explicação do KS e entender porque é interessante aplicar.

Essa métrica considera uma base de classificação binária e um modelo ajustado que retorne probabilidades da observação pertencer aos positivos. O teste KS oferece um resultado considerado mais completo, por avaliar a qualidade de diferenciação de classe que o modelo tem, vamos para o passo a passo para entender melhor o que isso quer dizer :

  • Rodar o modelo de classificação para obter as probabilidades das observações possuírem resposta positiva;

  • Separar essas probabilidades de acordo com a classe;

  • Calcular a função de distribuição acumulada empírica para cada um dos vetores de probabilidade;

  • Obter o KS como a maior distância entre as duas acumuladas.

Para ilustrar o passo a passo vamos fazer isso manualmente e comparar com o resultado da função ks.test, para isso vamos criar uma base sintética de 1000 observações.

library(tidyverse)
set.seed(2020)

label <- base::sample(c(0,1),1000,prob = c(.3,.7),replace = T) %>% as.factor()
letras <- base::sample(c("a","b","c"),1000,prob = c(.3,.5,.2),replace = T)
numers <- rgamma(1000, shape = 2) + rnorm(1000,7)*as.numeric(label)


modelo <- glm(label ~ letras + numers^2, family = binomial())
summary(modelo)

preditos <- ifelse(modelo$fitted.values > .5,1,0)

table(label,preditos)

#          preditos
# label     0   1
# 
#        0 254  29
#        1  20 697

No bloco acima fizemos o passo 1, geramos uma base sintética e ajustamos um modelo linear generalizado, em seguida consideramos probabilidades acima de 0.5 como positivo ( ou, predição = 1) e negativo caso contrário. Para ter uma noção do que esperar do resultado da KS, podemos dar uma olhada na matriz de confusão, que no código acima é plotada por table(label,preditos). Observamos que tem muito mais VP e VN em relação aos FP e FN, então esperamos um valor alto da métrica KS. Sabendo disso, passamos para o “cálculo manual” da métrica.

# passo 2: separando as probabilidades atribuídas a cada classe

positivos <- modelo$fitted.values[label == 1]
negativos <- modelo$fitted.values[label == 0]

#############################
# passo 3 :

# obtendo função de distribuição acumulada para as probabilidades da classe 1
acum_positos <- ecdf(positivos)

# obtendo função de distribuição acumulada para as probabilidades da classe 0
acum_negativos <- ecdf(negativos)

# passo 4 :

# calculando a distância máxima entre as distribuições obtidas acima
max(abs(acum_positos(seq(0,1,length.out = 1000)) - 
acum_negativos(seq(0,1,length.out = 1000))))
#0.8854424
#############################

A função ecdf() retorna a função de distribuição acumulada empírica, por isso que para obter os resultados da acumulada precisamos setar como entrada uma variável frequência, no nosso caso : seq(0,1,length.out = 1000). A função abs() permite capturar a diferença em módulo, obtemos então a diferença máxima de 0.8854424. Agora podemos validar se esse é realmente o valor da métrica KS aplicando positivos ; negativos na função ks.test do R, função do pacote nativo stats que já nos retorna o valor final da métrica.

KS <- ks.test(positivos,negativos)$statistic
KS
#        D 
# 0.8854424 

Pretty! O valor bate, sabemos agora porque a métrica é interessante, e sabendo o que acontece nos bastidores temos segurança de que ela é confiável. Agora a título de curiosidade, existe uma outra forma de calcular a KS, talvez um método que faça mais sentido por partir das outras métricas mais simples que citamos, ao invés de partir de termos como “função de distribuição acumulada empírica” que pode não soar amigável para todo mundo. Esta versão alternativa do cálculo é através da diferença entre TVP e TFP, que vai depender do cutoff, que no nosso caso é 0.5.

 # calculando TVP - TFP para vários cutoffs
cutoff <- seq(0,1,by = 0.001)
TVP <- TFP <- numeric()

for(i in 1:length(cutoff)){
  
  preditos <- ifelse(modelo$fitted.values > cutoff[i],1,0)
  
  store <- data.frame(preditos, label)
  
  VP <- store %>% summarise(sum((preditos == 1 & label == 1))) %>% as.numeric()
  FN <- store %>% summarise(sum((preditos == 0 & label == 1))) %>% as.numeric()
  FP <- store %>% summarise(sum((preditos == 1 & label == 0))) %>% as.numeric()
  VN <- store %>% summarise(sum((preditos == 0 & label == 0))) %>% as.numeric()
  
  TVP[i] <- VP/(VP + FN)
  TFP[i] <- FP/(FP + VN)
  
}

# TVP - TFP no cutoff = 0.5

TVP[which(cutoff == .5)] - TFP[which(cutoff == .5)]
#[1] 0.8696325

Essa forma de calcular a KS é um pouco mais grosseira, nos dando uma aproximação do resultado real 0.8696325, mas é interessante por explicitar que a métrica KS relaciona outras duas, que levam em consideração o quão certo o modelo está classificando uma das classes ao mesmo tempo que leva em consideração o “tamanho” das classificações erradas.

É isso, ficamos por aqui. Lavem as mãos, usem máscaras e até a próxima.