KNN

Author

Ricardo Accioly

Published

August 20, 2024

KNN

O KNN é um algoritmo muito simples no qual cada observação é prevista com base em sua “semelhança” com outras observações. Ao contrário da maioria dos métodos, KNN é um algoritmo baseado na memória e não pode ser resumido por um modelo de forma fechada. Isso significa que as amostras de treinamento são necessárias no tempo de execução e as previsões são feitas diretamente das relações amostrais. Consequentemente, os KNNs também são conhecidos como aprendizes preguiçosos

Carregando Bibliotecas

#>  default    student       balance           income     
#>  No :9667   No :7056   Min.   :   0.0   Min.   :  772  
#>  Yes: 333   Yes:2944   1st Qu.: 481.7   1st Qu.:21340  
#>                        Median : 823.6   Median :34553  
#>                        Mean   : 835.4   Mean   :33517  
#>                        3rd Qu.:1166.3   3rd Qu.:43808  
#>                        Max.   :2654.3   Max.   :73554
str(Default)
#> 'data.frame':    10000 obs. of  4 variables:
#>  $ default: Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 1 1 1 1 ...
#>  $ student: Factor w/ 2 levels "No","Yes": 1 2 1 1 1 2 1 2 1 1 ...
#>  $ balance: num  730 817 1074 529 786 ...
#>  $ income : num  44362 12106 31767 35704 38463 ...
head(Default)
#>   default student   balance    income
#> 1      No      No  729.5265 44361.625
#> 2      No     Yes  817.1804 12106.135
#> 3      No      No 1073.5492 31767.139
#> 4      No      No  529.2506 35704.494
#> 5      No      No  785.6559 38463.496
#> 6      No     Yes  919.5885  7491.559

Manipulando os dados

credito <- tibble(Default)
summary(credito)
#>  default    student       balance           income     
#>  No :9667   No :7056   Min.   :   0.0   Min.   :  772  
#>  Yes: 333   Yes:2944   1st Qu.: 481.7   1st Qu.:21340  
#>                        Median : 823.6   Median :34553  
#>                        Mean   : 835.4   Mean   :33517  
#>                        3rd Qu.:1166.3   3rd Qu.:43808  
#>                        Max.   :2654.3   Max.   :73554
# renomeando colunas
credito <- credito %>% 
                rename( inadimplente = default, estudante = student, balanco = balance,
                receita = income)
credito <- credito %>% mutate( inadimplente =  case_when(
                           inadimplente == "No"  ~ "Nao",
                           inadimplente == "Yes" ~ "Sim"
                          )) %>% mutate(inadimplente = factor(inadimplente))
credito <- credito %>% mutate( estudante =  case_when(
                           estudante == "No"  ~ 0,
                           estudante == "Yes" ~ 1
                          )) 

str(credito)
#> tibble [10,000 × 4] (S3: tbl_df/tbl/data.frame)
#>  $ inadimplente: Factor w/ 2 levels "Nao","Sim": 1 1 1 1 1 1 1 1 1 1 ...
#>  $ estudante   : num [1:10000] 0 1 0 0 0 1 0 1 0 0 ...
#>  $ balanco     : num [1:10000] 730 817 1074 529 786 ...
#>  $ receita     : num [1:10000] 44362 12106 31767 35704 38463 ...
summary(credito)
#>  inadimplente   estudante         balanco          receita     
#>  Nao:9667     Min.   :0.0000   Min.   :   0.0   Min.   :  772  
#>  Sim: 333     1st Qu.:0.0000   1st Qu.: 481.7   1st Qu.:21340  
#>               Median :0.0000   Median : 823.6   Median :34553  
#>               Mean   :0.2944   Mean   : 835.4   Mean   :33517  
#>               3rd Qu.:1.0000   3rd Qu.:1166.3   3rd Qu.:43808  
#>               Max.   :1.0000   Max.   :2654.3   Max.   :73554

Matriz de dispersão

Vamos agora explorar os dados originais para termos algum visão do comportamento das variáveis explicativas e a variável dependente.

library(psych)
pairs.panels(credito, 
             method = "pearson", # metodo de correlação
             hist.col = "#00AFBB",
             density = TRUE,  # mostra graficos de densidade
             ellipses = FALSE # mostra elipses de correlação
             )

Avaliando o comportamento das variáveis em função do status (inadimplente / estudante)

library(patchwork)
p1 <- ggplot(credito, aes(x=inadimplente, y=balanco, color=inadimplente)) +
  geom_boxplot()
p2 <- ggplot(credito, aes(x=inadimplente, y=receita, color=inadimplente)) +
  geom_boxplot()
p3 <- ggplot(credito, aes(x=as.factor(estudante), y=balanco, color=as.factor(estudante))) +
  geom_boxplot()
p4 <- ggplot(credito, aes(x=as.factor(estudante), y=receita, color=as.factor(estudante))) +
  geom_boxplot()
(p1 + p2) / (p3 + p4)

Explorando um pouco mais Balanço e Receita

p5 <- ggplot(credito, aes(x=balanco)) +
  geom_histogram(bins = round(1+3.322*log10(nrow(credito)),0))
p6 <- ggplot(credito, aes(x=receita)) +
    geom_histogram(bins = round(1+3.322*log10(nrow(credito)),0))
p5 + p6

Balanço vs Receita

ggplot(data = credito, aes(x=balanco,  y = receita, col = inadimplente)) + geom_point() 

KNN

Vamos usar a função knn da biblioteca caret que tem ótimas funcionalidades. Observem que a saída pode ser as classes ou as probabilidades de pertencer a uma classe

Como o KNN usa as distancias entre os pontos ele é afetado pela escala dos dados, portanto, é necessário que os dados sejam normalizados (padronizados) para eliminar este efeito.

Quando temos diversas variáveis explicativas em diferentes escalas, em geral, elas devem ser transformadas para ter media zero e desvio padrão 1

Criando conjuntos de treino e teste e normalizando variáveis

library(caret)
set.seed(2024)
y <- credito$inadimplente
credito_split <- createDataPartition(y, times = 1, p = 0.80, list = FALSE)

conj_treino <- credito[credito_split,]
conj_treino[,3:4] <- scale(conj_treino[,3:4]) # scale normaliza
conj_teste <- credito[-credito_split,]
conj_teste[,3:4] <- scale(conj_teste[, 3:4])
                           
summary(conj_treino)
#>  inadimplente   estudante         balanco            receita        
#>  Nao:7734     Min.   :0.0000   Min.   :-1.72131   Min.   :-2.45922  
#>  Sim: 267     1st Qu.:0.0000   1st Qu.:-0.73380   1st Qu.:-0.91305  
#>               Median :0.0000   Median :-0.02068   Median : 0.07759  
#>               Mean   :0.2945   Mean   : 0.00000   Mean   : 0.00000  
#>               3rd Qu.:1.0000   3rd Qu.: 0.68805   3rd Qu.: 0.77310  
#>               Max.   :1.0000   Max.   : 3.74460   Max.   : 2.93799
summary(conj_teste)
#>  inadimplente   estudante         balanco            receita        
#>  Nao:1933     Min.   :0.0000   Min.   :-1.75009   Min.   :-2.12222  
#>  Sim:  66     1st Qu.:0.0000   1st Qu.:-0.72205   1st Qu.:-0.91629  
#>               Median :0.0000   Median :-0.04085   Median : 0.09891  
#>               Mean   :0.2941   Mean   : 0.00000   Mean   : 0.00000  
#>               3rd Qu.:1.0000   3rd Qu.: 0.65118   3rd Qu.: 0.76539  
#>               Max.   :1.0000   Max.   : 3.50597   Max.   : 2.93133

1a Modelo

Vamos usar a regra da raiz quadrada do tamanho da amostra para definir o número de vizinhos do KNN.

library(caret)
sqrt(nrow(conj_treino)) ## ~90
#> [1] 89.44831
set.seed(23)

t_knn1 <- knn3(inadimplente ~ balanco + receita + estudante, data = conj_treino, k = 90)
t_knn1
#> 90-nearest neighbor model
#> Training set outcome distribution:
#> 
#>  Nao  Sim 
#> 7734  267

Avaliando o modelo

Através da função matriz de confusão do pacote caret conseguimos obter as principais medidas de avaliação de um modelo de classificação.

Veja que a acurácia deu um valor alto, mas isto não é suficiente para considerarmos que temos um bom modelo. Veja que a sensibilidade está muito baixa e que o ideal é que tenhamos valores altos de sensibilidade e especificidade.

Observar que a prevalência é muito baixa o que está afetando os resultados do modelo.

y_chapeu_knn1 <- predict(t_knn1, conj_teste, type = "class")


confusionMatrix(y_chapeu_knn1, conj_teste$inadimplente, positive="Sim") 
#> Confusion Matrix and Statistics
#> 
#>           Reference
#> Prediction  Nao  Sim
#>        Nao 1932   50
#>        Sim    1   16
#>                                           
#>                Accuracy : 0.9745          
#>                  95% CI : (0.9666, 0.9809)
#>     No Information Rate : 0.967           
#>     P-Value [Acc > NIR] : 0.03105         
#>                                           
#>                   Kappa : 0.3771          
#>                                           
#>  Mcnemar's Test P-Value : 1.801e-11       
#>                                           
#>             Sensitivity : 0.242424        
#>             Specificity : 0.999483        
#>          Pos Pred Value : 0.941176        
#>          Neg Pred Value : 0.974773        
#>              Prevalence : 0.033017        
#>          Detection Rate : 0.008004        
#>    Detection Prevalence : 0.008504        
#>       Balanced Accuracy : 0.620953        
#>                                           
#>        'Positive' Class : Sim             
#> 

Curva ROC

Para a curva ROC é necessário que obtenhamos as probabilidades e não das classes, vejam nos comandos abaixo como se obtem as probabilidades.

library(pROC)

# 
p_chapeu_knn1 <- predict(t_knn1, conj_teste, type = "prob")
head(p_chapeu_knn1)
#>            Nao        Sim
#> [1,] 1.0000000 0.00000000
#> [2,] 0.9777778 0.02222222
#> [3,] 1.0000000 0.00000000
#> [4,] 1.0000000 0.00000000
#> [5,] 1.0000000 0.00000000
#> [6,] 0.9888889 0.01111111
# Aqui gera o curva e salvo numa variável
roc_knn1 <- roc(conj_teste$inadimplente ~ p_chapeu_knn1[,2], plot = TRUE, print.auc=FALSE, col="black", legacy.axes=TRUE)

legend("bottomright",legend=c("KNN1"), 
       col=c("black"),lwd=4)

Area embaixo da curva ROC

# Area abaixo da Curva (AUC)
as.numeric(roc_knn1$auc)
#> [1] 0.9468286

Variando K

Anteriormente usamos k=90. Este parametro deve ser ajustado para melhoramos os modelo KNN. Para isto vamos usar a função train da biblioteca caret

Observe que a otimização de k é feita através de acurácia.

set.seed(2024)

# Usando validação cruzada para obter o valor de k através da função train da biblioteca caret e o controle do treino e fazendo um gride de valores para k.
ctrl <- trainControl(method = "repeatedcv", 
                     number = 10,
                     repeats = 5)
t_knn2 <- train(inadimplente ~ balanco + receita + estudante,
                method = "knn", 
                trControl= ctrl,
                tuneGrid = data.frame(k = seq(5,100, by=5)),
                metric = "Accuracy",
                data = conj_treino)
## Resultados do treino
t_knn2
#> k-Nearest Neighbors 
#> 
#> 8001 samples
#>    3 predictor
#>    2 classes: 'Nao', 'Sim' 
#> 
#> No pre-processing
#> Resampling: Cross-Validated (10 fold, repeated 5 times) 
#> Summary of sample sizes: 7200, 7202, 7200, 7202, 7201, 7200, ... 
#> Resampling results across tuning parameters:
#> 
#>   k    Accuracy   Kappa    
#>     5  0.9687539  0.3713723
#>    10  0.9710787  0.4074849
#>    15  0.9719035  0.3949430
#>    20  0.9714038  0.3650255
#>    25  0.9716285  0.3568810
#>    30  0.9716288  0.3532068
#>    35  0.9715038  0.3445969
#>    40  0.9715037  0.3418931
#>    45  0.9714537  0.3369602
#>    50  0.9715037  0.3314707
#>    55  0.9711037  0.3129172
#>    60  0.9707038  0.2893980
#>    65  0.9705289  0.2761428
#>    70  0.9704536  0.2663185
#>    75  0.9699037  0.2364880
#>    80  0.9696287  0.2176720
#>    85  0.9693037  0.1946375
#>    90  0.9688291  0.1614589
#>    95  0.9685543  0.1362015
#>   100  0.9680543  0.1070964
#> 
#> Accuracy was used to select the optimal model using the largest value.
#> The final value used for the model was k = 15.
plot(t_knn2)

## Previsões com o resultaddos do treino
prev_knn2 <- predict(t_knn2, conj_teste)
confusionMatrix(prev_knn2, conj_teste$inadimplente,  positive="Sim")
#> Confusion Matrix and Statistics
#> 
#>           Reference
#> Prediction  Nao  Sim
#>        Nao 1924   41
#>        Sim    9   25
#>                                           
#>                Accuracy : 0.975           
#>                  95% CI : (0.9672, 0.9814)
#>     No Information Rate : 0.967           
#>     P-Value [Acc > NIR] : 0.02267         
#>                                           
#>                   Kappa : 0.4885          
#>                                           
#>  Mcnemar's Test P-Value : 1.165e-05       
#>                                           
#>             Sensitivity : 0.37879         
#>             Specificity : 0.99534         
#>          Pos Pred Value : 0.73529         
#>          Neg Pred Value : 0.97913         
#>              Prevalence : 0.03302         
#>          Detection Rate : 0.01251         
#>    Detection Prevalence : 0.01701         
#>       Balanced Accuracy : 0.68707         
#>                                           
#>        'Positive' Class : Sim             
#> 

Curva ROC dos 2 melhores modelos k=90 e k=15

prev_knn1 <- predict(t_knn1, conj_teste, type = "prob")
prev_knn2 <- predict(t_knn2, conj_teste, type = "prob")
roc_knn1 <- roc(conj_teste$inadimplente ~ prev_knn1[,2], plot = TRUE, print.auc=FALSE, col="black", legacy.axes=TRUE)
roc_knn2 <- roc(conj_teste$inadimplente ~ prev_knn2[,2], plot = TRUE, print.auc=FALSE, col="green", legacy.axes=TRUE, add=TRUE)
legend("bottomright",legend=c("KNN1", "KNN2"), 
       col=c("black","green"),lwd=4)

## Area embaixo das curvas
as.numeric(roc_knn1$auc)
#> [1] 0.9468286
as.numeric(roc_knn2$auc)
#> [1] 0.8927715

Observe que os resultados de área abaixo da ROC não são suficientes para a escolha do k, pois precisamos estar atentos a sensibilidade e especificidade!