Challenge Project 02 - Warm Up: Machine Learning with a Heart

29 de março, 2020

1. Descrição geral do problema


Heart

As doenças cardíacas são a principal causa de morte no mundo. Para aprender a prevenir doenças cardíacas, precisamos primeiro aprender a detectá-las com segurança.

Nosso conjunto de dados é de um estudo de doenças cardíacas que está aberto ao público há muitos anos. O estudo coleta várias medidas sobre a saúde do paciente e as estatísticas cardiovasculares e, é claro, torna as identidades dos pacientes anônimas.

Os dados são fornecidos como cortesia do Cleveland Heart Disease Database) através do repositório UCI Machine Learning.

Prevenir doenças cardíacas é importante. Bons sistemas orientados a dados para prever doenças cardíacas podem melhorar todo o processo de pesquisa e prevenção, garantindo que mais pessoas possam ter uma vida saudável.

Nos Estados Unidos, os Centros de Controle e Prevenção de Doenças são um bom recurso para obter informações sobre doenças cardíacas. De acordo com o site:

  • Cerca de 610.000 pessoas morrem de doenças cardíacas nos Estados Unidos a cada ano - isso é 1 em cada 4 mortes.
  • As doenças cardíacas são a principal causa de morte para homens e mulheres. Mais da metade das mortes por doenças cardíacas em 2009 ocorreram em homens.
  • A doença cardíaca coronária (DCC) é o tipo mais comum de doença cardíaca, matando mais de 370.000 pessoas anualmente.
  • Todos os anos, cerca de 735.000 americanos sofrem um ataque cardíaco. Destes, 525.000 são um primeiro ataque cardíaco e 210.000 ocorrem em pessoas que já tiveram um ataque cardíaco.
  • As doenças cardíacas são a principal causa de morte para pessoas da maioria das etnias dos Estados Unidos, incluindo afro-americanos, hispânicos e brancos. Para índios americanos ou nativos do Alasca e asiáticos ou ilhéus do Pacífico, as doenças cardíacas perdem apenas para o câncer.

Para obter mais informações, consulte o site dos centros de controle e prevenção de doenças.

Objetivo: utilizar a linguagem R para prever a classe binária heart_disease_present, que representa se um paciente tem ou não uma doença cardíaca utilizando para a avaliação de desempenho dos modelos preditivos a serem criados a métrica Logloss).


2. Carregando Dados

2.1 Importando bibliotecas necessárias

Vamos começar nosso projeto importanto todas as bilbiotecas necessárias para a realização das fases iniciais de exploração e transformação dos dados (Data Munging).

In [ ]:
# Definindo a oculatação de warnings.

options(warn = -1)

# Caso não possua uma das bibliotecas importadas abaixo, a instale com o comando a seguir:

install.packages(c(
    'dplyr',
    'corrplot',
    'ggplot2',
    'gmodels',
    'vcd',
    'pROC',
    'e1071'
))
In [ ]:
# Importando bibliotecas.

library(dplyr)
library(corrplot)
library(ggplot2)
library(gmodels)
library(vcd)
library(pROC)
library(e1071)

2.2 Carregando dados

In [ ]:
# Importando os dados do dataset.

dataset <- read.table("/content/datasets/heart.dat")

# Visualizando dados Importados. 

head(dataset)
A data.frame: 6 × 14
V1V2V3V4V5V6V7V8V9V10V11V12V13V14
<dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><int>
170141303220210902.42332
267031155640216001.62071
357121242610014100.31072
464141282630010510.22171
574021202690212110.21131
665141201770014000.41071
In [ ]:
# Renomeando colunas do dataset.

names(dataset) <- c('age', 'sex', 
                  'chest_pain_type', 
                  'resting_blood_pressure', 
                  'serum_cholestoral', 
                  'fasting_blood_sugar',
                  'resting_electro_results',
                  'max_heart_rate_achieved',
                  'exercise_induced_angina',
                  'oldpeak',
                  'slope_of_the_peak',
                  'number_of_major_vessels',
                  'thal',
                  'heart_disease')
In [ ]:
# Criando uma cópia do dataset.

data <- dataset

3. Data Munging - Preparando dados para a análise exploratória

3.1 Visão geral dos dados

Segundo as documentações do DrivenData e da UCI Machine Learning Repository) referente ao projeto, cada linha dos dados contém um registro, com as seguintes variáveis:

Variável Tipo Descrição
heart_disease (Target) binário representa se um paciente tem ou não uma doença cardíaca (1: não apresenta doença cardíaca, 2: apresenta doença cardíaca);
slope_of_peak int é a inclinação do segmento ST do pico do exercício, uma eletrocardiografia lida indicando a qualidade do fluxo sanguíneo no coração;
thal categórico são os resultados do teste de stress com tálio para o fluxo sanguíneo de medição para o coração, com os valores possíveis: normal, fixed_defect, reversible_defect;
resting_blood_pressure int é a pressão arterial em repouso;
chest_pain_type int é o tipo de dor no peito (1-4);
number_major_vessels int é o número de vasos principais (0-3) coloridos por flourosopy;
fasting_blood_sugar binário indica se o açúcar no sangue em jejum é > 120 mg/dl;
resting_electro_results int são os resultados eletrocardiográficos em repouso (0-2);
serum_cholesterol int é o colestorol sérico em mg/dl;
oldpeak float oldpeak = depressão do ST induzida pelo exercício em relação ao repouso, é uma medida de anormalidade nos eletrocardiogramas;
sex binário indica o sexo do paciente (0: feminino, 1: masculino);
age int indica a idade em anos do paciente;
max_heart_rate_achieved int é a frequência cardíaca máxima atingida (batimentos por minuto) e;
exercise_induced_angina binário indica se há dor no peito induzida por exercícios ( 0: falso, 1: verdadeiro).

Nesta etapa vamos buscar organizar os dados das variáveis para efetuar a análise exploratória e extrair insigths que possam auxiliar no processo de criação dos modelos preditivos.

In [ ]:
# Verificando os tipos das colunas carregadas no dataset.

glimpse(data)
Rows: 270
Columns: 14
$ age                     <dbl> 70, 67, 57, 64, 74, 65, 56, 59, 60, 63, 59, 5…
$ sex                     <dbl> 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, …
$ chest_pain_type         <dbl> 4, 3, 2, 4, 2, 4, 3, 4, 4, 4, 4, 4, 3, 1, 4, …
$ resting_blood_pressure  <dbl> 130, 115, 124, 128, 120, 120, 130, 110, 140, …
$ serum_cholestoral       <dbl> 322, 564, 261, 263, 269, 177, 256, 239, 293, …
$ fasting_blood_sugar     <dbl> 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, …
$ resting_electro_results <dbl> 2, 2, 0, 0, 2, 0, 2, 2, 2, 2, 0, 2, 2, 0, 2, …
$ max_heart_rate_achieved <dbl> 109, 160, 141, 105, 121, 140, 142, 142, 170, …
$ exercise_induced_angina <dbl> 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, …
$ oldpeak                 <dbl> 2.4, 1.6, 0.3, 0.2, 0.2, 0.4, 0.6, 1.2, 1.2, …
$ slope_of_the_peak       <dbl> 2, 2, 1, 2, 1, 1, 2, 2, 2, 2, 2, 1, 1, 2, 1, …
$ number_of_major_vessels <dbl> 3, 0, 0, 1, 1, 0, 1, 1, 2, 3, 0, 0, 0, 2, 1, …
$ thal                    <dbl> 3, 7, 7, 7, 3, 7, 6, 7, 7, 7, 7, 7, 3, 3, 3, …
$ heart_disease           <int> 2, 1, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 2, 1, …

Verificamos a existência de 14 variáveis numéricas e 270 observações dentro do dataset.

In [ ]:
# Verificando a existência de valores NA no dataset.

print(sapply(data, function(v) {
    anyNA(v)
}))
                    age                     sex         chest_pain_type 
                  FALSE                   FALSE                   FALSE 
 resting_blood_pressure       serum_cholestoral     fasting_blood_sugar 
                  FALSE                   FALSE                   FALSE 
resting_electro_results max_heart_rate_achieved exercise_induced_angina 
                  FALSE                   FALSE                   FALSE 
                oldpeak       slope_of_the_peak number_of_major_vessels 
                  FALSE                   FALSE                   FALSE 
                   thal           heart_disease 
                  FALSE                   FALSE 

Não foi detectado nenhum valor NA dentro do conjunto de dados.

In [ ]:
# Verificando o número de valores únicos presentes em cada uma das variáveis especificadas.

print(sapply(data, function(v) {
    length(unique(v))
}))
                    age                     sex         chest_pain_type 
                     41                       2                       4 
 resting_blood_pressure       serum_cholestoral     fasting_blood_sugar 
                     47                     144                       2 
resting_electro_results max_heart_rate_achieved exercise_induced_angina 
                      3                      90                       2 
                oldpeak       slope_of_the_peak number_of_major_vessels 
                     39                       3                       4 
                   thal           heart_disease 
                      3                       2 

3.2 Alterando tipos de dados das variáveis

A partir da documentação fornecida sobre o conjunto de dados, iremos alterar o tipo de dado de cada variável.

In [ ]:
## Alterando o tipo da variável sex.

data$sex <- as.factor(data$sex)

# Alterando as etiquetas dos valores da variável.

levels(data$sex) <- c('F', 'M')

## Alterando o tipo da variável fasting_blood_sugar.

data$fasting_blood_sugar <- as.factor(data$fasting_blood_sugar)

# Alterando as etiquetas dos valores da variável.

levels(data$fasting_blood_sugar) <- c('No', 'Yes')

## Alterando o tipo da variável exercise_induced_angina.

data$exercise_induced_angina <- as.factor(data$exercise_induced_angina)

# Alterando as etiquetas dos valores da variável.

levels(data$exercise_induced_angina) <- c('No', 'Yes')

# Alterando o tipo da variável resting_electro_results.

data$resting_electro_results <- as.factor(data$resting_electro_results)

# Alterando o tipo da variável chest_pain_type.

data$chest_pain_type <- as.factor(data$chest_pain_type)

# Alterando o tipo da variável slope_of_the_peak.

data$slope_of_the_peak <- factor(data$slope_of_the_peak, ordered = T)

## Alterando o tipo da variável thal.

data$thal <- as.factor(data$thal)

# Alterando as etiquetas dos valores da variável.

levels(data$thal) <- c('Normal', 'Fixed defect', 'Reversable defect')

## Alterando o tipo da variável heart_disease.

data$heart_disease <- as.factor(data$heart_disease)

# Alterando as etiquetas dos valores da variável.

levels(data$heart_disease) <- c('No', 'Yes')

# Alterando o tipo da variável age.

data$age <- as.integer(data$age)

# Alterando o tipo da variável resting_blood_pressure.

data$resting_blood_pressure <- as.integer(data$resting_blood_pressure)

# Alterando o tipo da variável serum_cholestoral.

data$serum_cholestoral <- as.integer(data$serum_cholestoral)

# Alterando o tipo da variável max_heart_rate_achieved.

data$max_heart_rate_achieved <- as.integer(data$max_heart_rate_achieved)

# Alterando o tipo da variável number_of_major_vessels.

data$number_of_major_vessels <- as.factor(data$number_of_major_vessels)
In [ ]:
# Verificando os tipos das colunas carregadas no dataset.

glimpse(data)
Rows: 270
Columns: 14
$ age                     <int> 70, 67, 57, 64, 74, 65, 56, 59, 60, 63, 59, 5…
$ sex                     <fct> M, F, M, M, F, M, M, M, M, F, M, M, M, M, F, …
$ chest_pain_type         <fct> 4, 3, 2, 4, 2, 4, 3, 4, 4, 4, 4, 4, 3, 1, 4, …
$ resting_blood_pressure  <int> 130, 115, 124, 128, 120, 120, 130, 110, 140, …
$ serum_cholestoral       <int> 322, 564, 261, 263, 269, 177, 256, 239, 293, …
$ fasting_blood_sugar     <fct> No, No, No, No, No, No, Yes, No, No, No, No, …
$ resting_electro_results <fct> 2, 2, 0, 0, 2, 0, 2, 2, 2, 2, 0, 2, 2, 0, 2, …
$ max_heart_rate_achieved <int> 109, 160, 141, 105, 121, 140, 142, 142, 170, …
$ exercise_induced_angina <fct> No, No, No, Yes, Yes, No, Yes, Yes, No, No, N…
$ oldpeak                 <dbl> 2.4, 1.6, 0.3, 0.2, 0.2, 0.4, 0.6, 1.2, 1.2, …
$ slope_of_the_peak       <ord> 2, 2, 1, 2, 1, 1, 2, 2, 2, 2, 2, 1, 1, 2, 1, …
$ number_of_major_vessels <fct> 3, 0, 0, 1, 1, 0, 1, 1, 2, 3, 0, 0, 0, 2, 1, …
$ thal                    <fct> Normal, Reversable defect, Reversable defect,…
$ heart_disease           <fct> Yes, No, Yes, No, No, No, Yes, Yes, Yes, Yes,…

Perfeito, agora podemos ir para as próxima etapas de exploração dos dados.

3.3 Explorando a distribuição de cada variável individualmente

Antes de começarmos, queremos deixar claro que quando utilizarmos expressões que descrevam uma pessoa como sendo saudável ou sadia estaremos especificamente declarando que ela não possui registros de doenças cardiovasculares.

Independente do indivíduo apresentar ou não outras doenças, adotaremos esta nomeclatura para aumentar a fluidez na escrita da nossa análise que tem como foco estudar problemas cardíacos.

3.3.1 Criando funções auxiliares

3.3.1.1 Funções para a criação de gráficos

Criaremos algumas funções para padronizar as plotagens de gráficos que efetuaremos.

In [ ]:
# Definindo um função para criar gráficos de barras.

barPlot <- function(col, data, target = 'heart_disease') {
    
    ggplot(data = data, aes(x = data[, col], fill = data[, target])) + 
        geom_bar(alpha = 0.85, position = 'dodge') +
        ggtitle(paste("Bar chart for variable:", col)) +
        xlab(col) +
        labs(fill = "Heart Disease") +
        theme_bw()
}
In [ ]:
# Definindo um função para criar gráficos de densidade.

densityPlot <- function(col, data, target = 'heart_disease') {
    
    ggplot(data = data, aes(x = data[, col], fill = data[, target])) + 
        geom_density(alpha = 0.85) +
        ggtitle(paste("Density chart for variable:", col)) +
        xlab(col) +
        labs(fill = "Heart Disease") +
        theme_bw() 
}
In [ ]:
# Definindo um função para criar gráficos de boxplot.

boxPlot <- function(col, data, target = 'heart_disease') {
  
    ggplot(data = data, aes(x = data[, target],  y = data[, col], fill = data[, target])) + 
        geom_boxplot(alpha = 0.85) +
        ggtitle(paste("Boxplot for variable:", col)) +
        ylab(col) +
        xlab("") + 
        labs(fill = "Heart Disease") +
        theme_bw()  
}
3.3.1.2 Funções para calcular estatísticas

Criaremos uma função para padronizar as estatísticas que calcularemos para cada uma das variáveis a serem estudadas.

In [ ]:
# Definindo um função para gerar um dataframe com as estatísticas de uma variável do dataset.

varStats <- function(col, data, target = 'heart_disease') {

    if(!is.factor(data[, col])) {                 

        # Calcula estatísticas de variáveis numéricas.
        
        data %>%
            group_by_at(target) %>%
            summarize(
                Min       = min(get(col)),
                Q1        = unname(quantile(get(col), probs = c(0.25))),
                Median    = median(get(col)),
                mean      = mean(get(col)),
                Q3        = unname(quantile(get(col), probs = c(0.75))),
                Max       = max(get(col)),
                Sd        = sd(get(col)),
                Sk        = skewness(get(col)),
                Ck        = kurtosis(get(col))
            )

    } else {                                      

        # Calcula estatísticas de variáveis categóricas.

        l = list()

        # Contabiliza o número de registros dentro de cada classe da variável.

        l$variableClasses <- data %>%
            group_by_at(col) %>%
            summarise(
                count = n(),
                prop  = n() / nrow(data) * 100
            )
        
        # Contabiliza o número de registros dentro de cada classe da variável e segmenta cada subconjunto a partir 
        #da variável alvo.

        l$variableClassesGroupedByTargetVariable <- data %>%
            group_by_at(c(col, target)) %>%
            summarise(
                count = n(),
                prop  = n() / nrow(data) * 100
            )

        # Retorna a lista com as estatísticas computadas.

        l
    }
}

O coeficente de Assimetria (Skewness) indica como os dados estão distribuídos e para interpretar seu resultado podemos olhar a tabela a seguir:

Índice de Assimetria Descrição
SK ≈ 0 Os dados são simétricos. Tanto a cauda do lado direito quanto a do lado esquerdo da função densidade de probabilidade são iguais;
SK < 0 A assimetria é negativa. A cauda do lado esquerdo da função densidade de probabilidade é maior que a do lado direito e;
SK > 0 A assimetria é positiva. A cauda do lado direito da função densidade de probabilidade é maior que a do lado esquerdo.

O coeficiente de Curtose (Kurtosis) é uma medida que caracteriza o achatamento da curva da função de distribuição e para interpretar seu resultado podemos olhar a tabela a seguir:

Índice de Curtose Descrição
CK ≈ 0 A distribuição é normal e é chamada de Curtose Mesocúrtica;
CK < 0 A Cauda é mais leve que a normal. Para um coeficiente de Curtose negativo, tem-se uma Curtose Platicúrtica e;
CK > 0 A Cauda é mais pesada que a normal. Para um coeficiente de Curtose positivo, tem-se uma Curtose Leptocúrtica.

Atenção: Há diferentes fórmulas para calcular estes coeficientes. Mas, para este estudo utilizamos as funções fornecidas pela biblioteca e1071 com suas respectivas configurações padrão. Em caso de dúvida, consulte a documentação.

3.3.2 Variável age

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'age'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, data)
In [ ]:
# Criando um gráfico de densidade para a variável especificada.

densityPlot(col, data)

Os dados dos indivíduos que apresentam problemas cardíacos está mais deslocado para a direita quando comparado ao dos indivíduos que não apresentam estas dificuldades. Isto nos indica que as pessoas com problemas cardiovasculares tendem a ser mais velhas do que aquelas que apresentam um coração sadio.

In [ ]:
# Criando um gráfico de boxplot para a variável especificada.

boxPlot(col, data)

O gráfico de boxplot nos permite detectar a presença de outliers dentro dos dados dos indivíduos que apresentam problemas cardíacos.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

varStats(col, data)
A tibble: 2 × 10
heart_diseaseMinQ1MedianmeanQ3MaxSdSkCk
<fct><int><dbl><dbl><dbl><dbl><int><dbl><dbl><dbl>
No 29455252.7066759769.509830 0.1532323-0.63383151
Yes35525856.5916762778.116273-0.5327168 0.01991217

Concluímos que há uma diferença mediana de 6 anos entre os índividuos classificados com ou sem problemas cardíacos.

3.3.3 Variável sex

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'sex'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, data)
In [ ]:
# Calculando algumas estatísticas para a variável especificada.

stats <- varStats(col, data)

# Verificando a distribuição de registros entre as classes da variável.

stats$variableClasses
A tibble: 2 × 3
sexcountprop
<fct><int><dbl>
F 8732.22222
M18367.77778

Observamos que dentro do nosso conjunto de dados aproximadamente 32.2% dos registros estão associados a mulheres e 67.8% estão associados a homens.

In [ ]:
# Verificando a distribuição de registros entre as classes da variável especificada agrupadas pela variável alvo.

stats$variableClassesGroupedByTargetVariable
A grouped_df: 4 × 4
sexheart_diseasecountprop
<fct><fct><int><dbl>
FNo 6724.814815
FYes 20 7.407407
MNo 8330.740741
MYes10037.037037

É interessante observar que a proporção de mulheres com problemas cardíacos dentro do conjunto de dados é aproximadamente 3.34 vezes menor do que aquelas que estão saudáveis e 5 vezes menor do que a proporção de homens doentes.

3.3.4 Variável chest_pain_type

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'chest_pain_type'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, data)

Vemos que as pessoas que apresentam dores do tipo 1, 2 e 3 são predominantemente sadias, mas este padrão se inverte para a dor tipo 4 onde a maior parte dos indivíduos que a possuem apresentam problemas cardíacos.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

stats <- varStats(col, data)

# Verificando a distribuição de registros entre as classes da variável.

stats$variableClasses
A tibble: 4 × 3
chest_pain_typecountprop
<fct><int><dbl>
1 20 7.407407
2 4215.555556
3 7929.259259
412947.777778

Observamos que as proporções dos registros do tipo de dor crescem de maneira incremental, sendo a dor do tipo 1 a menos frequente (presente em aproximadamente 7.4% dos registros) e a do tipo 4 a mais frequente (presente em aproximadamente 47.8% dos registros).

In [ ]:
# Verificando a distribuição de registros entre as classes da variável especificada agrupadas pela variável alvo.

stats$variableClassesGroupedByTargetVariable
A grouped_df: 8 × 4
chest_pain_typeheart_diseasecountprop
<fct><fct><int><dbl>
1No 15 5.555556
1Yes 5 1.851852
2No 3512.962963
2Yes 7 2.592593
3No 6222.962963
3Yes17 6.296296
4No 3814.074074
4Yes9133.703704

Destacamos que 78.5% dos indivíduos que apresentam a dor do tipo 3 tem um coração saudável enquanto 70.5% dos indivíduos que aprentam a dor do tipo 4 possuem problemas cardíacos.

3.3.5 Variável resting_blood_pressure

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'resting_blood_pressure'

# Criando um gráfico de densidade para a variável especificada.

densityPlot(col, data)

Bom, parece que a pressão arterial de todos os indivíudos em repouso apresenta um valor mediano muito próximo independente da existência de problemas cardiovasculares.

In [ ]:
# Criando um gráfico de boxplot para a variável especificada.

boxPlot(col, data)

O boxplot nos confirma que o valor mediano destes conjuntos de dados está muito próximo; nos permite detectar a exitência de outliers e ainda observar a significativa assimetria à direita dos dados dos indivíduos que apresentam doenças cardíacas.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

varStats(col, data)
A tibble: 2 × 10
heart_diseaseMinQ1MedianmeanQ3MaxSdSkCk
<fct><int><dbl><dbl><dbl><dbl><int><dbl><dbl><dbl>
No 94120130128.866714018016.457660.40529330.1702971
Yes100120130134.441714520019.095420.86653130.8092740

Veja só! O valor médiano para a pressão arterial dos indivíudos em repouso é o mesmo independente de estarem ou não doentes.

Note que o coeficiente de assimetria (Sk) também comprova nossa teoria de que a distribuição dos indivíduos com problemas cardíacos apresenta uma assimetria à direita.

3.3.6 Variável serum_cholestoral

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'serum_cholestoral'

# Criando um gráfico de densidade para a variável especificada.

densityPlot(col, data)

O gráfico de densidade está indicando que há uma diferença no valor mediano do colestorol sérico em mg/dl entre os individuos classificados com ou sem doenças cardíacas.

In [ ]:
# Criando um gráfico de boxplot para a variável especificada.

boxPlot(col, data)
In [ ]:
# Calculando algumas estatísticas para a variável especificada.

varStats(col, data)
A tibble: 2 × 10
heart_diseaseMinQ1MedianmeanQ3MaxSdSkCk
<fct><int><dbl><dbl><dbl><dbl><int><dbl><dbl><dbl>
No 126209.00236.0244.2133268.7556454.019091.74726317.4636429
Yes149227.25255.5256.4667286.5040947.969170.28882620.4057267

Concluímos que a diferença mediana entre as classes é de 19.5 mg/dl.

3.3.7 Variável fasting_blood_sugar

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'fasting_blood_sugar'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, data)

Interessante! A maior parte dos indivíduos registrados não possuem uma taxa de açúcar no sangue em jejum maior do que 120 mg/dl.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

stats <- varStats(col, data)

# Verificando a distribuição de registros entre as classes da variável.

stats$variableClasses
A tibble: 2 × 3
fasting_blood_sugarcountprop
<fct><int><dbl>
No 23085.18519
Yes 4014.81481

Aproximadamente 14.8% dos indivíduos registrados apresentam uma taxa de açúcar no sangue em jejum maior do que 120 mg/dl.

In [ ]:
# Verificando a distribuição de registros entre as classes da variável especificada agrupadas pela variável alvo.

stats$variableClassesGroupedByTargetVariable
A grouped_df: 4 × 4
fasting_blood_sugarheart_diseasecountprop
<fct><fct><int><dbl>
No No 12747.037037
No Yes10338.148148
YesNo 23 8.518519
YesYes 17 6.296296

Está tabela nos informa a proporção de registros em cada uma das classes da variável que avalia a taxa de açúcar no sangue agrupadas em subgrupos que indicam se o índividuo possui ou não problemas cardíacos. Mas, para este caso as informações fornecidas não nos ajudam a extrair muitas informações interessantes.

Por isso, vamos criar outra tabela em que dividimos a coluna count pelo número total de registros classificados em cada grupo da variável fasting_blood_sugar e recalcularemos as proporções. De maneira simples, o que estamos querendo é determinar se a proporção de indivíduos com problemas cardíacos varia muito entre a classe de indivíduos que apresentam ou não uma taxa de açúcar no sangue em jejum maior do que 120 mg/dl.

In [ ]:
# Calculando as proporções de indivíduos com ou sem problemas cardíacos dentro de cada classe da variável especificada.

stats$variableClassesGroupedByTargetVariable %>%
    group_by(fasting_blood_sugar) %>%
    mutate(prop = count / sum(count) * 100)
A grouped_df: 4 × 4
fasting_blood_sugarheart_diseasecountprop
<fct><fct><int><dbl>
No No 12755.21739
No Yes10344.78261
YesNo 2357.50000
YesYes 1742.50000

Vemos que as proporções são bem próximas.

Dentro da classe de indivíduos que não possuem uma taxa de açúcar no sangue em jejum maior do que 120 mg/dl, aproximadamente 55.2% tem um coração sadio enquanto 44.8% possuem doenças.

Dentro da classe de indivíduos que possuem uma taxa de açúcar no sangue em jejum maior do que 120 mg/dl, 57.5% tem um coração sadio enquanto 42.5% possuem doenças.

3.3.8 Variável resting_electro_results

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'resting_electro_results'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, data)

Os resultados eletrocardiográficos em repouso que apresentam valor 0 estão predominantemente associados a indivíduos que não possuem doenças enquanto aqueles classificados com o valor 2 estão predominantemente associados a índivíduos doentes.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

stats <- varStats(col, data)

# Verificando a distribuição de registros entre as classes da variável.

stats$variableClasses
A tibble: 3 × 3
resting_electro_resultscountprop
<fct><int><dbl>
013148.5185185
1 2 0.7407407
213750.7407407

Os resultados eletrocardiográficos em repouso que obtiveram o valor 1 são aqueles que apresentam a menor proporção dentro do conjunto de dados, aproximadamente 0.74%.

In [ ]:
# Verificando a distribuição de registros entre as classes da variável especificada agrupadas pela variável alvo.

stats$variableClassesGroupedByTargetVariable
A grouped_df: 6 × 4
resting_electro_resultsheart_diseasecountprop
<fct><fct><int><dbl>
0No 8531.4814815
0Yes4617.0370370
1No 1 0.3703704
1Yes 1 0.3703704
2No 6423.7037037
2Yes7327.0370370

Destacamos que dentro dos resultados eletrocardiográficos em repouso classificados com o valor 0, a quantidade de indivíduos que são sadios é cerca de 1.85 vezes maior do que aqueles que são doentes.

3.3.9 Variável max_heart_rate_achieved

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'max_heart_rate_achieved'

# Criando um gráfico de densidade para a variável especificada.

densityPlot(col, data)

Parece que o número mediano de batimentos cardíacos por minuto dos indivíduos sádios é maior do que o dos indivíduos com doenças.

In [ ]:
# Criando um gráfico de boxplot para a variável especificada.

boxPlot(col, data)

Detectamos a existência de outliers dentro dos dois subconjuntos analisados.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

varStats(col, data)
A tibble: 2 × 10
heart_diseaseMinQ1MedianmeanQ3MaxSdSkCk
<fct><int><dbl><dbl><dbl><dbl><int><dbl><dbl><dbl>
No 96148.25161.0158.3333172.0020219.28336-0.6639215 0.3710206
Yes71124.75141.5138.8583157.2519523.13072-0.2390985-0.3985598

Bom, como suspeitávamos pessoas com doenças cardiovasculares apresentam um número mediano menor de batimentos cardíacos (cerca de 21 batimentos a menos). Também observamos que o conjunto de dados de índividuos sadios tem um desvio-padrão menor.

3.3.10 Variável exercise_induced_angina

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'exercise_induced_angina'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, data)

Vemos que predominantemente não há dor no peito induzida por exercício para os indivíduos sadios, enquanto a maior parte dos doentes registraram sentir algo.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

stats <- varStats(col, data)

# Verificando a distribuição de registros entre as classes da variável.

stats$variableClasses
A tibble: 2 × 3
exercise_induced_anginacountprop
<fct><int><dbl>
No 18167.03704
Yes 8932.96296

Observamos que em aproximadamente 67% dos registros os indivíduos não sentiram dor no peito.

In [ ]:
# Verificando a distribuição de registros entre as classes da variável especificada agrupadas pela variável alvo.

stats$variableClassesGroupedByTargetVariable
A grouped_df: 4 × 4
exercise_induced_anginaheart_diseasecountprop
<fct><fct><int><dbl>
No No 12747.037037
No Yes 5420.000000
YesNo 23 8.518519
YesYes 6624.444444

Por fim, verificamos que cerca de 24.4% dos indivíduos apresenta dor no peito induzida por execícios e efetivamente possui doenças cardíacas.

3.3.11 Variável oldpeak

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'oldpeak'

# Criando um gráfico de densidade para a variável especificada.

densityPlot(col, data)

Podemos ver uma clara diferença entre a distribuição desta medida de anormalidade nos eletrocardiogramas entre o conjunto de indivíduos sadios e os doentes.

In [ ]:
# Criando um gráfico de boxplot para a variável especificada.

boxPlot(col, data)

Descobrimos a existência de outliers dentro dos conjuntos.

Note que esta medida de anormalidade tende a se concentrar próximo de um valor quando analisamos o conjunto de indivíduos saudáveis.

Quanto estudamos o conjunto de dados de pessoas doentes vemos que esta medida tende a variar muito mais.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

varStats(col, data)
A tibble: 2 × 10
heart_diseaseMinQ1MedianmeanQ3MaxSdSkCk
<fct><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>
No 00.00.20.62266671.1754.20.80085081.52523252.6036596
Yes00.61.41.58416672.4256.21.28206740.75698660.5796318

Concluímos destacando a diferença entre os valores medianos dos conjuntos e os altos índices de Assimetria e Curtose do grupo de indivíduos sem doenças.

3.3.12 Variável slope_of_the_peak

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'slope_of_the_peak'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, data)

Bom, o gráfico nos indica que a inclinação do segmento ST do pico do exercício e predominantemente 1 quando o indivíduo é sadio e 2 ou 3 quando apresenta alguma doença.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

stats <- varStats(col, data)

# Verificando a distribuição de registros entre as classes da variável.

stats$variableClasses
A tibble: 3 × 3
slope_of_the_peakcountprop
<ord><int><dbl>
113048.148148
212245.185185
3 18 6.666667

A maior parte dos registros apresentam uma inclinação do segmento ST do pico do exercício com valor 1 ou 2.

In [ ]:
# Verificando a distribuição de registros entre as classes da variável especificada agrupadas pela variável alvo.

stats$variableClassesGroupedByTargetVariable
A grouped_df: 6 × 4
slope_of_the_peakheart_diseasecountprop
<ord><fct><int><dbl>
1No 9836.296296
1Yes3211.851852
2No 4416.296296
2Yes7828.888889
3No 8 2.962963
3Yes10 3.703704

3.3.13 Variável number_of_major_vessels

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'number_of_major_vessels'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, data)

O gráfico gerado por está variável é bem interessante! Veja que a medida que o número de vasos principais coloridos por flourosopy aumenta, a proporção de indivíduos saudáveis diminuiu drasticamente.

Vemos isso ao analisar a coluna 0 e a coluna 1. Quando não há vasos principais coloridos por flourosopy o conjunto de dados é predominantemente de indivíduos sem doenças. Mas, basta que um vaso seja colorido para que essa predominância mude e os indivíduos com doenças passem a se destacar.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

stats <- varStats(col, data)

# Verificando a distribuição de registros entre as classes da variável.

stats$variableClasses
A tibble: 4 × 3
number_of_major_vesselscountprop
<fct><int><dbl>
016059.259259
1 5821.481481
2 3312.222222
3 19 7.037037

Temos em aproximadamente 59.26% dos registros indivíduos que não possuem vasos coloridos por flourosopy.

In [ ]:
# Verificando a distribuição de registros entre as classes da variável especificada agrupadas pela variável alvo.

stats$variableClassesGroupedByTargetVariable
A grouped_df: 8 × 4
number_of_major_vesselsheart_diseasecountprop
<fct><fct><int><dbl>
0No 12044.444444
0Yes 4014.814815
1No 20 7.407407
1Yes 3814.074074
2No 7 2.592593
2Yes 26 9.629630
3No 3 1.111111
3Yes 16 5.925926

Veja atentamente está tabela!

Quando o número de vasos principais coloridos por flourosopy é 0, a proporção de indivíduos sadios é 3 vezes maior do que a de doentes.

Quando o número de vasos principais coloridos por flourosopy é 1, a predominância muda e a proporção de indivíduos doentes se torna 2 vezes maior do que a dos sadios.

3.3.14 Variável thal

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'thal'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, data)

Detectamos que predominantemente a maior parte dos indivíduos saudáveis apresenta um resutlado normal para o teste de stress com tálio. Porém, quando o resultado é um defeito fixo ou um defeito reversível a predominância é de indivíduos com problemas cardíacos.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

stats <- varStats(col, data)

# Verificando a distribuição de registros entre as classes da variável.

stats$variableClasses
A tibble: 3 × 3
thalcountprop
<fct><int><dbl>
Normal 15256.296296
Fixed defect 14 5.185185
Reversable defect10438.518519

Cerca de 56.3% dos registros dos indivíduos apresentou um resultado normal.

In [ ]:
# Verificando a distribuição de registros entre as classes da variável especificada agrupadas pela variável alvo.

stats$variableClassesGroupedByTargetVariable
A grouped_df: 6 × 4
thalheart_diseasecountprop
<fct><fct><int><dbl>
Normal No 11944.074074
Normal Yes 3312.222222
Fixed defect No 6 2.222222
Fixed defect Yes 8 2.962963
Reversable defectNo 25 9.259259
Reversable defectYes 7929.259259

3.3.15 Variável heart_disease

In [ ]:
# Definindo o nome da variável a ser analisada.

col <- 'heart_disease'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, data)

Detectamos um desbalanceamento entre as classes da variável preditora.

In [ ]:
# Calculando algumas estatísticas para a variável especificada.

stats <- varStats(col, data)

# Verificando a distribuição de registros entre as classes da variável.

stats$variableClasses
A tibble: 2 × 3
heart_diseasecountprop
<fct><int><dbl>
No 15055.55556
Yes12044.44444

Deveremos tratar este desbalanceamento antes de prosseguir para a análise preditiva.

4. Feature Engineering

4.1 Importando bibliotecas necessárias

Importaremos todas as bilbiotecas necessárias para a realização das etapas de modelagem preditiva.

In [ ]:
# Caso não possua uma das bibliotecas importadas abaixo, a instale com o comando a seguir:

install.packages(c(
    'xgboost',
    'MLmetrics',
    'ROSE', 
    'DMwR',
    'caret',
    'randomForest',
    'C50',
    'fastAdaboost',
    'data.table',
    'neuralnet'
))
In [ ]:
# Importando bibliotecas.

library(xgboost)
library(MLmetrics)
library(caret)
library(randomForest)
library(C50)
library(fastAdaboost)
library(ROSE)
library(DMwR)
library(neuralnet)
library(data.table)

4.2 Preparando dados para modelagem preditiva

Muito bem, como a maior parte dos algoritmos de Machine Learning trabalham melhor com variáveis númericas, iremos criar uma nova cópia do conjunto de dados sem qualquer uma das modificações que implementamos para executar a fase de exploração de dados.

In [ ]:
# Carregando dataset não modificado.

data <- dataset

Na fase de exploração dos dados pudemos observar que as variáveis oldpeak, resting_blood_pressure e serum_cholestoral possuem uma assimetria à direita muita acentuada e isso pode atrapalhar a criação dos nossos modelos.

Para contornar este problema de distorção nos dados usaremos a função de transformação log1p (ou log(x + 1)) na variável oldpeak e a função de tranformação log nas outras duas variáveis com o objetivo de diminuir a irregularidade nos dados e tornar os padrões que apresentam mais visíveis.

Para mais informações sobre como a função log atua sobre dados distorcidos consulte este link. Para entender melhor a função log1p consulte este link.

Destacamos que o principal motivo de utilizarmos a função log1p é há existência de valores nulos dentro dos dados da variável oldpeak o que inviabiliza o uso da função log pois o log(0) é um valor indefinido.

In [ ]:
# Aplicando a função de transformação log1p aos valores da variável oldpeak.

data$oldpeak <- log1p(data$oldpeak) 

# Aplicando a função de transformação log aos valores da variável resting_blood_pressure.

data$resting_blood_pressure <- log(data$resting_blood_pressure)

# Aplicando a função de transformação log aos valores da variável serum_cholestoral.

data$serum_cholestoral <- log(data$serum_cholestoral)

A próxima etapa que iremos efetuar é a de escalar os valores das variáveis preditoras entre 0 e 1.

In [ ]:
# Definindo método de pré-processamento.

params <- preProcess(data %>% select(- heart_disease), method = 'range')

# Transformando os dados.

data <- predict(params, data)

# Visualizando as primeiras linhas do dataset.

head(data)
A data.frame: 6 × 14
agesexchest_pain_typeresting_blood_pressureserum_cholestoralfasting_blood_sugarresting_electro_resultsmax_heart_rate_achievedexercise_induced_anginaoldpeakslope_of_the_peaknumber_of_major_vesselsthalheart_disease
<dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><int>
10.854166711.00000000.42944370.6260255010.290076300.619921580.51.000000002
20.791666700.66666670.26706131.0000000010.679389300.484028480.50.000000011
30.583333310.33333330.36685890.4858900000.534351100.132904510.00.000000012
40.729166711.00000000.40890890.4909833000.259542010.092357690.50.333333311
50.937500000.33333330.32343000.5060338010.381679410.092357690.00.333333301
60.750000011.00000000.32343000.2267641000.526717600.170445000.00.000000011

Como estamos trabalhando com um problema de classificação, converteremos a variável a ser prevista para o tipo factor. Caso algum algoritmo solicite um ajuste específico para utilizar o conjunto de dados, faremos a modificação de forma temporária para gerar o modelo em questão e não alterar as variáveis do dataset.

In [ ]:
# Convertendo a variável a ser prevista para o tipo factor.

data$heart_disease <- as.factor(data$heart_disease)

5. Modelagem Preditiva

5.1 Criando dados de treino e de teste

Para iniciar a modelagem preditiva, devemos criar os dados de treino e de teste.

In [ ]:
# Definindo um seed.

set.seed(100)

# Criando as partições com dados de treino e de teste.

inTrain <- createDataPartition(data$heart_disease, p = .90, list = F)

# Segmentando dados de treino e de teste por partição.

train <- data[inTrain, ]
test  <- data[-inTrain, ]

5.2 Criando funções de busca dos melhores parâmetros para os modelos a serem criados

Como não sabemos qual o algoritmo mais adequado para realizar as previsões para o nosso problema nem que valores utilizar em suas configurações, criaremos funções que gerem diferentes modelos com diferentes ajustes baseados nos seguintes algoritmos: Random Forest, C 5.0, Adaboost, XGboost e Neuralnet.

Adotando esta estratégia conseguiremos extrair a melhor performance de cada um dos algoritmos a serem implementados.

5.2.1 Modelo Random Forest

Note que está função também tem a capaciade de avaliar a importância de cada variável preditora a ser utilizada no modelo.

In [ ]:
# Definindo uma função para gerar diferentes modelos com diferentes valores de parametrização baseados no algoritmo 
# Random Forest.

getBetterRandomForestParameters <- function(train, f, test = NULL, positive = NULL, nTree = 1:100, nNode = 1:100, 
                                            importance = F, statusPrint = F) {

    # Criando o dataframe para salvar os resultados dos modelos.

    featuresRF <- data.frame()

    # Define o número total de modelos a serem criados.

    total <- length(nTree) * length(nNode)

    # Capturando o nome da variável a ser prevista.

    target <- as.character(f[[2]])

    # Define uma varíavel auxiliar para permitir o acompanhamento do progresso na avaliação dos modelos criados.

    count <- 0

    for(t in nTree) {
        
        for(n in nNode) {
            
            # Define um seed para permitir que os mesmos resultados dos experimentos sejam reproduzíveis.
            
            set.seed(100)
            
            # Cria o modelo Random Forest a ser avaliado.

            model <- randomForest(
                formula    = f,
                data       = train, 
                ntree      = t, 
                nodesize   = n, 
                importance = importance
            )

            # Determina a Confusion Matrix para a avaliação de importância das variáveis preditoras ou para realizar 
            # previsões.

            if(importance) {

                # Computa a Confusion Matrix gerada a partir do modelo criado para determinar a importância das variáveis.

                cm <- confusionMatrix(table(
                        data      = model$y, 
                        reference = model$predicted
                    ),  
                    positive = positive
                )

                # Armazena os parâmetros utilizados para criação do modelo e a acurácia obtida no dataframe.

                featuresRF <- rbind(featuresRF, data.frame(
                    nodes    = n, 
                    nTree    = t, 
                    accuracy = unname(cm$overall['Accuracy'])
                ))

            } else {

                # Realizando as previsões com o modelo baseado no algoritmo Random Forest.

                pred <- predict(model, test)

                # Criando a Confusion Matrix a partir das previsões.  

                cm <- confusionMatrix(table(pred = pred, data = test[ , target]), positive = positive)

                # Armazena os parâmetros utilizados para criação do modelo, a acurácia e o score da métrica Logloss 
                # obtidos no dataframe.

                featuresRF <- rbind(featuresRF, data.frame(
                    nodes    = n, 
                    nTree    = t, 
                    accuracy = unname(cm$overall['Accuracy']),
                    logloss  = LogLoss(predict(model, test, type = 'prob')[, positive], as.integer(test[, target]) - 1)
                ))
            }
            
            # Incrementa o número de modelos avaliados.

            count <- count + 1

            # Imprime a porcetagem de progresso do treinamento e a melhor acurácia ou valor da métrica logloss já alcançada.

            if(statusPrint) {

                if(importance) {
                    print(paste(100 * count / total, '%, best accuracy: ', max(featuresRF$accuracy)))
                } else {
                    print(paste(100 * count / total, '%, best LogLoss: ', min(featuresRF$logloss)))
                }
            }
        }
    }

    # Retorna o dataframe com os resultados obtidos pelo treinamento de cada modelo.
    
    featuresRF
}

5.2.2 Modelo C 5.0

In [ ]:
# Definindo uma função para gerar diferentes modelos com diferentes valores de parametrização baseados no algoritmo C 5.0.

getBetterC50Parameters <- function(train, test, cost, f, positive, trials = 1:100, statusPrint = F) {

    # Criando o dataframe para salvar os resultados dos modelos.

    featuresC50 <- data.frame()

    # Define o número total de modelos a serem criados.
    
    total <- length(trials)

    # Capturando o nome da variável a ser prevista.

    target <- as.character(f[[2]])

    # Define uma varíavel auxiliar para permitir o acompanhamento do progresso na avaliação dos modelos criados.

    count <- 0

    for(t in trials) {

        # Define um seed para permitir que os mesmos resultados dos experimentos sejam reproduzíveis.
        
        set.seed(100)

        # Criando o modelo baseado no algoritmo C 5.0.

        model_c50 <-  C5.0(
            formula = f, 
            data    = train, 
            trials  = t, 
            rules   = T,
            cost    = cost,
            control = C5.0Control(
                winnow          = T,
                noGlobalPruning = T,
                earlyStopping   = T
            )
        )

        # Realizando as previsões com o modelo baseado no algoritmo C 5.0.

        pred <- predict(model_c50, test)

        # Criando a Confusion Matrix a partir das previsões.  

        cm <- confusionMatrix(table(pred = pred, data = test[ , target]), positive = positive)

        # Armazena os parâmetros utilizados para criação do modelo, a acurácia e o score da métrica Logloss obtidos no 
        #dataframe.

        featuresC50 <- rbind(featuresC50, data.frame(
            trials   = t, 
            accuracy = unname(cm$overall['Accuracy']),
            logloss  = LogLoss(predict(model_c50, test, type = 'prob')[ , positive], as.integer(test[, target]) - 1)
        ))
        
        # Incrementa o número de modelos avaliados.

        count <- count + 1

        # Imprime a porcetagem de progresso do treinamento e o melhor valor da métrica logloss já alcançado.

        if(statusPrint) {
            print(paste(100 * count / total, '%, best LogLoss: ', min(featuresC50$logloss)))
        }
    }

    # Retorna o dataframe com os resultados obtidos pelo treinamento de cada modelo.

    featuresC50
}

5.2.3 Modelo Adaboost

In [ ]:
# Definindo uma função para gerar diferentes modelos com diferentes valores de parametrização baseados no algoritmo 
# Adaboost.

getBetterAdaboostParameters <- function(train, test, f, positive, nIters = 1:100, treeDepth = 3, statusPrint = F) {

    # Criando o dataframe para salvar os resultados dos modelos.

    featuresAdaboost <- data.frame()

    # Define o número total de modelos a serem criados.

    total <- length(nIters) * length(treeDepth)

    # Capturando o nome da variável a ser prevista.

    target <- as.character(f[[2]])

    # Define uma varíavel auxiliar para permitir o acompanhamento do progresso na avaliação dos modelos criados.

    count <- 0

    for(n in nIters) {
        
        for(t in treeDepth) {

            # Define um seed para permitir que os mesmos resultados dos experimentos sejam reproduzíveis.
            
            set.seed(100)

            # Criando o modelo baseado no algoritmo Adaboost.

            model_adaboost <-  adaboost(
                formula    = f, 
                data       = train, 
                nIter      = n, 
                tree_depth = t
            )

            # Realizando as previsões com o modelo baseado no algoritmo Adaboost.

            pred <- predict(model_adaboost, test, type = 'class')

            # Criando a Confusion Matrix a partir das previsões.  

            cm <- confusionMatrix(table(pred = pred$class, data = test[ , target]), positive = positive)

            # Armazena os parâmetros utilizados para criação do modelo, a acurácia e o score da métrica Logloss obtidos 
            # no dataframe.

            featuresAdaboost <- rbind(featuresAdaboost, data.frame(
                nIter     = n, 
                treeDepth = t,
                accuracy  = unname(cm$overall['Accuracy']),
                logloss   = LogLoss(predict(model_adaboost, test, type = 'raw')$votes[, 2], as.integer(test[, target]) - 1)
            ))
            
            # Incrementa o número de modelos avaliados.

            count <- count + 1

            # Imprime a porcetagem de progresso do treinamento e o melhor valor da métrica logloss já alcançado.

            if(statusPrint) {
                print(paste(100 * count / total, '%, best LogLoss: ', min(featuresAdaboost$logloss)))
            }
        }
    }

    # Retorna o dataframe com os resultados obtidos pelo treinamento de cada modelo.

    featuresAdaboost
}

5.2.4 Modelo XGboost

In [ ]:
# Definindo uma função para gerar diferentes modelos com diferentes valores de parametrização baseados no algoritmo XGboost.

getBetterXGboostParameters <- function(train, trainTarget, test, testTarget, maxDepth = 1:10, nEta = seq(0.1, 1, 0.1), 
                                       nRounds = 1:100, threshold = 0.5,  statusPrint = F) {

    # Criando o dataframe para salvar os resultados dos modelos.

    featuresXGboost <- data.frame()

    # Define o número total de modelos a serem criados.

    total <- length(maxDepth) * length(nEta) * length(nRounds)

    # Define uma varíavel auxiliar para permitir o acompanhamento do progresso na avaliação dos modelos criados.

    count <- 0

    for(m in maxDepth) {

        for(e in nEta) {

            for(r in nRounds) {

                # Define um seed para permitir que os mesmos resultados dos experimentos sejam reproduzíveis.
                
                set.seed(100)

                # Criando o modelo baseado no algoritmo XGboost.

                model_xgboost <- xgboost(
                    data        = train,             # Define as variáveis preditoras.
                    label       = trainTarget,       # Define a variável target.
                    max.depth   = m,                 # Define o tamanho máximo da árvore.
                    eta         = e,                 # Define a taxa de aprendizado do modelo.
                    nthread     = 16,                # Define o número de threads que devem ser usadas. 
                                                     # Quanto maior for esse número, mais rápido será o treinamento.
                    nrounds     = r,                 # Define o número de iterações.
                    objective   = "binary:logistic", # Define que o modelo deve ser baseado em uma regressão 
                                                     # logistica binária.
                    verbose     = F,                 # Define a exibição da queda da taxa de erro durante o treinamento.
                    eval_metric = 'logloss',         # Define a função de avaliação a ser utilizada.
                    maximize    = F                  # Define que a pontuação da avaliação deve ser minimizada.
                )

                # Realizando as previsões com o modelo baseado no algoritmo XGboost.

                pred <- predict(model_xgboost, test)

                # Definindo o threshold.
                
                for(th in threshold) {

                    # Classificando os resultados segundo o threshold especificado.

                    predClass <- ifelse(pred > th, 1, 0)

                    # Criando a Confusion Matrix a partir das previsões.  

                    cm <- confusionMatrix(table(
                            pred = factor(predClass, levels = c(0, 1)), 
                            data = factor(testTarget, levels = c(0, 1))
                        ), 
                        positive =  '1'
                    )

                    # Armazena os parâmetros utilizados para criação do modelo, a acurácia e o score da métrica Logloss 
                    # obtidos no dataframe.

                    featuresXGboost <- rbind(featuresXGboost, data.frame(
                        maxDepth = m, 
                        th       = th,
                        eta      = e,
                        nRounds  = r,
                        accuracy = unname(cm$overall['Accuracy']),
                        logloss  = LogLoss(as.numeric(pred), testTarget)
                    ))
                }

                # Incrementa o número de modelos avaliados.

                count <- count + 1

                # Imprime a porcetagem de progresso do treinamento e a melhor acurácia já alcançada.

                if(statusPrint) {
                    print(paste(100 * count / total, '%, best LogLoss: ', min(featuresXGboost$logloss)))
                }
            }
        }
    }

    # Retorna o dataframe com os resultados obtidos pelo treinamento de cada modelo.
    
    featuresXGboost
}

5.2.5 Modelo Neuralnet

In [ ]:
# Definindo uma função para gerar diferentes modelos com diferentes valores de parametrização baseados no algoritmo 
# Neuralnet.

getBetterNNParameters <- function(train, test, f, neurons = 1:100, thresholdModel = seq(0.05, 1, 0.05), 
                                  thresholdClass = 0.5,  statusPrint = F) {

    # Criando o dataframe para salvar os resultados dos modelos.

    featuresNN <- data.frame()

    # Define o número total de modelos a serem criados.

    total <- length(neurons) * length(thresholdModel)

    # Capturando o nome da variável a ser prevista.

    target <- as.character(f[[2]])

    # Define uma varíavel auxiliar para permitir o acompanhamento do progresso na avaliação dos modelos criados.
    
    count <- 0

    for(n in neurons) {

        for(r in thresholdModel) {

                # Define um seed para permitir que os mesmos resultados dos experimentos sejam reproduzíveis.
                
                set.seed(100)
                
                # Criando o modelo baseado no algoritmo Neuralnet.
                
                nn <- neuralnet(
                    formula       = f, 
                    data          = train, 
                    hidden        = n, 
                    act.fct       = "logistic", 
                    stepmax       = 1e+08, 
                    linear.output = FALSE, 
                    threshold     = r
                )
                
                # Captura score da acurácia e da métrica Logloss para o modelo criado.

                metrics = tryCatch({
                    
                    # Realiza a previsão para os dados de treino com o modelo criado.

                    pred <- predict(nn, test)

                    for(th in thresholdClass) {
                    
                        # Classificando os resultados segundo o threshold especificado.

                        predClass <- ifelse(pred > th, 1, 0)

                        # Criando a Confusion Matrix a partir das previsões.  

                        cm <- confusionMatrix(table(
                                pred = factor(predClass, levels = c(0, 1)), 
                                data = factor(test[, target], levels = c(0, 1))
                            ), 
                            positive =  '1'
                        )

                        # Adiciona a configuração e os resultados obtidos pelo modelo avaliado ao dataframe.

                        featuresNN <- rbind(featuresNN, data.frame(
                            n              = n, 
                            thresholdModel = r, 
                            thresholdClass = th, 
                            Accuracy       = unname(cm$overall['Accuracy']), 
                            logloss        = LogLoss(pred, test[, target])
                        ))
                    }
                        
                }, error = function(e) {

                    # Caso a rede neural não consiga convergir, retorna o valor NA para o score da acurácia e 
                    # da métrica Logloss para o modelo criado.

                    featuresNN <- rbind(featuresNN, data.frame(
                        n              = n, 
                        thresholdModel = r, 
                        thresholdClass = th, 
                        Accuracy       = NA, 
                        logloss        = NA
                    ))
                })
                
                # Incrementa o número de modelos avaliados.

                count <- count + 1

                # Imprime a porcetagem de progresso do treinamento e a melhor acurácia já alcançada.

                if(statusPrint) {
                    print(paste(100 * count / total, '%, best LogLoss: ', min(featuresNN$logloss)))
                }
        }
    }

    # Eliminando resultados de modelos que não convergiram.

    featuresNN <- featuresNN[!is.na(featuresNN$logloss), ]

    # Retorna o dataframe com os resultados obtidos pelo treinamento de cada modelo.

    featuresNN
}

5.3 Avaliando a importância das variáveis com o algoritmo Random Forest

Antes de iniciarmos a criação dos modelos propriamente dita, é interessante definirmos o quanto cada variável dentro do dataset ajuda a prever o valor da variável target.

Para fazermos isto, utilizaremos o algoritmo Random Forest para computar tais valores. Como não sabemos qual o ajuste de parâmetros do algoritmo é mais adequado para o nosso conjunto de dados, criaremos modelos que possuam de 1 a 100 árvores com 1 a 100 nós.

Salvaremos a acurárica de cada modelo em um Dataframe. Note que por ser um processo de treinamento que tende a demandar muito tempo para ser processado, salvaremos os resultados obtidos em um arquivo CSV que pode ser carregado com todos os resultados gerados pelo bloco de código a seguir.

In [ ]:
# Gerando diferentes modelos baseados no algoritmo Random Forest para avaliar a importância das variáveis preditoras 
# no conjunto de dados.

impFeaturesRF <- getBetterRandomForestParameters(data, f = heart_disease ~ ., importance = T, statusPrint = T)
In [ ]:
# Salvando dataframe com os resultados gerados em um arquivo .csv.

fwrite(impFeaturesRF, '/content/outputs/impFeaturesRF.csv')

Caso deseje pular a execução do bloco de código anterior, basta carregar os resultados já processados que estão salvos no arquivo CSV abaixo:

In [ ]:
# Carregando dataframe com os resultados obtidos para cada modelo Random Forest criado.

impFeaturesRF <- fread('/content/outputs/impFeaturesRF.csv')

Imprimiremos os registros dos modelos que apresentaram a melhor acurácia.

In [ ]:
# Imprimindo registros dos modelos que alcançaram a maior acurácia.

bestRF <- impFeaturesRF[impFeaturesRF$accuracy == max(impFeaturesRF$accuracy), ]

bestRF
A data.table: 5 × 3
nodesnTreeaccuracy
<int><int><dbl>
89890.8592593
99910.8592593
99940.8592593
99980.8592593
99990.8592593

Caso mais de um modelo apresente a maior acurácia registrada dentro do dataset, selecionaremos o primeiro que foi listado para ser utilizado nas etapas seguintes.

In [ ]:
# Selecionando o primeiro registro dentro do dataframe.

bestRF <- bestRF[1,]

Recriaremos este modelo e imprimiremos suas estatísticas.

In [ ]:
# Define um seed para permitir que o mesmo resultado do experimento seja reproduzível.

set.seed(100)

# Criando modelo.

model <- randomForest(
    formula    = heart_disease ~ .,
    data       = data, 
    ntree      = bestRF$nTree, 
    nodesize   = bestRF$nodes, 
    importance = T
 )
                      
# Imprimindo o modelo.

model
Call:
 randomForest(formula = heart_disease ~ ., data = data, ntree = bestRF$nTree,      nodesize = bestRF$nodes, importance = T) 
               Type of random forest: classification
                     Number of trees: 89
No. of variables tried at each split: 3

        OOB estimate of  error rate: 14.07%
Confusion matrix:
    1   2 class.error
1 127  23   0.1533333
2  15 105   0.1250000

Agora podemos plotar o modelo em um gráfico e verificar o nível de importância das variáveis do dataset para prever a variável alvo.

In [ ]:
# Plotando gráfico para visualizar o nível de importância de cada variável no processo de predição da variável alvo.

v <- as.data.frame(varImpPlot(model, main = 'Predictor variables'))

Também podemos mensurar numericamente o quanto cada variável auxilia no processo de predição da variável alvo.

In [ ]:
# Captura em ordem decrescente de nível de importância o nome das variáveis.

names <- rownames(v[order(v[, 'MeanDecreaseAccuracy'], decreasing = T),])

# Imprime o resultado.

v[order(v[, 'MeanDecreaseAccuracy'], decreasing = T), ]
A data.frame: 13 × 2
MeanDecreaseAccuracyMeanDecreaseGini
<dbl><dbl>
thal5.9873363811.0332430
number_of_major_vessels5.20768150 6.1614050
chest_pain_type4.89701493 7.2690872
oldpeak3.75437555 3.5279328
exercise_induced_angina3.37432499 3.2947821
max_heart_rate_achieved3.28352898 3.3087980
slope_of_the_peak3.09433613 2.8576053
sex1.73545248 0.5974387
age1.00566577 0.5678321
resting_blood_pressure0.01414144 0.1245467
serum_cholestoral0.00000000 0.2509163
fasting_blood_sugar0.00000000 0.0000000
resting_electro_results0.00000000 0.1117128

5.4 Feature Selection

Bom, vamos considerar a medida MeanDecreaseAccuracy calculada na fase anterior. Sabemos que as variáveis mais importantes para prever a variável alvo são aquelas que apresentam os maiores valores para esta grandeza.

Sendo assim, iremos eliminar as 3 variáveis que apresentaram o menor valor de importância. Como o gráfico anterior nos mostra, o nome destas variáveis preditoras são: serum_cholestoral, fasting_blood_sugar e resting_electro_results.

In [ ]:
# Definindo um vetor com o nome das variáveis que devem ser desconsideras pelo modelo a ser treinado.

d <- c('fasting_blood_sugar', 'serum_cholestoral', 'resting_electro_results', 'heart_disease')

# Definindo fórmula a ser utilizada pelo modelo a ser treinado.

f <- heart_disease ~ . - fasting_blood_sugar - serum_cholestoral - resting_electro_results

5.5 Criando modelos

5.5.1 Modelo Random Forest

In [ ]:
# Gerando diferentes modelos baseados no algoritmo Random Forest e determinando sua acurácia e score para a métrica LogLoss.

featuresRF <- getBetterRandomForestParameters(
    train       = train, 
    test        = test,
    f           = f, 
    positive    = '2',
    statusPrint = T, nTree = 1:200, nNode = 1:200
)
In [ ]:
# Salvando dataframe com os resultados gerados em um arquivo .csv.

fwrite(featuresRF, '/content/outputs/featuresRF.csv')

Caso deseje pular a execução do bloco de código anterior, basta carregar os resultados já processados que estão salvos no arquivo CSV abaixo:

In [ ]:
# Carregando dataframe com os resultados obtidos para cada modelo Random Forest criado.

featuresRF <- fread('/content/outputs/featuresRF.csv')

Imprimiremos os registros dos modelos que apresentaram o menor score para a métrica Logloss.

In [ ]:
# Imprimindo registros dos modelos que alcançaram o menor score para a métrica Logloss.

bestRF <- featuresRF[featuresRF$logloss == min(featuresRF$logloss), ]

bestRF
A data.table: 1 × 4
nodesnTreeaccuracylogloss
<int><int><dbl><dbl>
2850.9629630.2005249

Caso mais de um modelo apresente o menor score para a métrica Logloss registrado dentro do dataset, selecionaremos o primeiro que foi listado para ser utilizado nas etapas seguintes.

In [ ]:
# Selecionando o primeiro registro dentro do dataframe.

bestRF <- bestRF[1,]

Recriaremos este modelo e imprimiremos suas estatísticas.

In [ ]:
# Define um seed para permitir que o mesmo resultado do experimento seja reproduzível.

set.seed(100)

# Criando modelo.

model_rf <- randomForest(
    formula  = f,
    data     = train, 
    ntree    = bestRF$nTree, 
    nodesize = bestRF$nodes
)
                      
# Imprimindo o modelo.

model_rf
Call:
 randomForest(formula = f, data = train, ntree = bestRF$nTree,      nodesize = bestRF$nodes) 
               Type of random forest: classification
                     Number of trees: 5
No. of variables tried at each split: 3

        OOB estimate of  error rate: 23.15%
Confusion matrix:
    1  2 class.error
1 108 12   0.1000000
2  38 58   0.3958333
In [ ]:
# Realizando as previsões com o modelo baseado no algoritmo Random Forest.

pred <- predict(model_rf, test)
In [ ]:
# Criando a Confusion Matrix a partir das previsões.  

cm <- confusionMatrix(table(pred = pred, data = test$heart_disease), positive = '2')

# Visualizando a Confusion Matrix.

cm
Confusion Matrix and Statistics

    data
pred  1  2
   1 15  1
   2  0 11
                                          
               Accuracy : 0.963           
                 95% CI : (0.8103, 0.9991)
    No Information Rate : 0.5556          
    P-Value [Acc > NIR] : 2.896e-06       
                                          
                  Kappa : 0.9244          
                                          
 Mcnemar's Test P-Value : 1               
                                          
            Sensitivity : 0.9167          
            Specificity : 1.0000          
         Pos Pred Value : 1.0000          
         Neg Pred Value : 0.9375          
             Prevalence : 0.4444          
         Detection Rate : 0.4074          
   Detection Prevalence : 0.4074          
      Balanced Accuracy : 0.9583          
                                          
       'Positive' Class : 2               
                                          

Perfeito, nosso primeiro modelo foi criado! Vamos salvar os scores de sua acurácia e de sua métrica Logloss em um dataframe.

In [ ]:
# Salvando scores das métricas analisadas em um dataframe.

scoreModels <- data.frame(
    accuracy  = unname(cm$overall['Accuracy']), 
    logloss   = LogLoss(predict(model_rf, test, type = 'prob')[, '2'], as.integer(test$heart_disease) - 1), 
    row.names = 'randomForest'
)

5.5.2 Modelo C 5.0

In [ ]:
# Definindo a matriz de custos a ser utilizada pelo modelo.

cost <- matrix(c(0, 1, 0.1, 0), nrow = 2, dimnames = list(c("No", "Yes"), c("No", "Yes")))

# Visualizando a matriz.

cost
A matrix: 2 × 2 of type dbl
NoYes
No00.1
Yes10.0
In [ ]:
# Gerando diferentes modelos baseados no algoritmo C 5.0 e determinando sua acurácia e score para a métrica LogLoss.

featuresC50 <- getBetterC50Parameters(
    train       = train, 
    test        = test, 
    cost        = cost, 
    f           = f,
    positive    = '2',
    statusPrint = T
)
In [ ]:
# Salvando dataframe com os resultados gerados em um arquivo .csv.

fwrite(featuresC50, '/content/outputs/featuresC50.csv')

Caso deseje pular a execução do bloco de código anterior, basta carregar os resultados já processados que estão salvos no arquivo CSV abaixo:

In [ ]:
# Carregando dataframe com os resultados obtidos para cada modelo C 5.0 criado.

featuresC50 <- fread('/content/outputs/featuresC50.csv')

Imprimiremos os registros dos modelos que apresentaram o menor score para a métrica Logloss.

In [ ]:
# Imprimindo registros dos modelos que alcançaram o menor score para a métrica Logloss.

bestC50 <- featuresC50[featuresC50$logloss == min(featuresC50$logloss), ]

bestC50
A data.table: 1 × 3
trialsaccuracylogloss
<int><dbl><dbl>
40.85185190.2511706

Caso mais de um modelo apresente o menor score para a métrica Logloss registrado dentro do dataset, selecionaremos o primeiro que foi listado para ser utilizado nas etapas seguintes.

In [ ]:
# Selecionando o primeiro registro dentro do dataframe.

bestC50 <- bestC50[1,]

Recriaremos este modelo e imprimiremos suas estatísticas.

In [ ]:
# Define um seed para permitir que os mesmos resultados dos experimentos sejam reproduzíveis.

set.seed(100)

# Criando o modelo baseado no algoritmo C 5.0.

model_c50 <-  C5.0(
    form   = f, 
    data   = train, 
    trials = bestC50$trials, 
    rules  = T,
    cost   = cost,
    control = C5.0Control(
        winnow          = T,
        noGlobalPruning = T,
        earlyStopping   = T
    )
)
In [ ]:
# Realizando as previsões com o modelo baseado no algoritmo C 5.0.

pred <- predict(model_c50, test)
In [ ]:
# Criando a Confusion Matrix a partir das previsões.  

cm <- confusionMatrix(table(pred = pred, data = test$heart_disease), positive = '2')

# Visualizando a Confusion Matrix.

cm
Confusion Matrix and Statistics

    data
pred  1  2
   1 15  4
   2  0  8
                                          
               Accuracy : 0.8519          
                 95% CI : (0.6627, 0.9581)
    No Information Rate : 0.5556          
    P-Value [Acc > NIR] : 0.001145        
                                          
                  Kappa : 0.6897          
                                          
 Mcnemar's Test P-Value : 0.133614        
                                          
            Sensitivity : 0.6667          
            Specificity : 1.0000          
         Pos Pred Value : 1.0000          
         Neg Pred Value : 0.7895          
             Prevalence : 0.4444          
         Detection Rate : 0.2963          
   Detection Prevalence : 0.2963          
      Balanced Accuracy : 0.8333          
                                          
       'Positive' Class : 2               
                                          
In [ ]:
# Salvando scores das métricas analisadas em um dataframe.

scoreModels <- rbind(scoreModels, data.frame(
    accuracy  = unname(cm$overall['Accuracy']),
    logloss   = LogLoss(predict(model_c50, test, type = 'prob')[ , '2'], as.integer(test$heart_disease) - 1), 
    row.names = 'C5.0'
))

5.5.3 Modelo Adaboost

In [ ]:
# Define um seed para permitir que os mesmos resultados dos experimentos sejam reproduzíveis.

set.seed(100)

# Gerando diferentes modelos baseados no algoritmo Adaboost e determinando sua acurácia e score para a métrica LogLoss.

featuresAdaboost <- getBetterAdaboostParameters(
    train       = train, 
    test        = test, 
    f           = f, 
    positive    = '2',
    statusPrint = T
)
In [ ]:
# Salvando dataframe com os resultados gerados em um arquivo .csv.

fwrite(featuresAdaboost, '/content/outputs/featuresAdaboost.csv')

Caso deseje pular a execução do bloco de código anterior, basta carregar os resultados já processados que estão salvos no arquivo CSV abaixo:

In [ ]:
# Carregando dataframe com os resultados obtidos para cada modelo Adaboost criado.

featuresAdaboost <- fread('/content/outputs/featuresAdaboost.csv')

Imprimiremos os registros dos modelos que apresentaram o menor score para a métrica Logloss.

In [ ]:
# Imprimindo registros dos modelos que alcançaram o menor score para a métrica Logloss.

bestAdaboost <- featuresAdaboost[featuresAdaboost$logloss == min(featuresAdaboost$logloss), ]

bestAdaboost
A data.table: 1 × 4
nItertreeDepthaccuracylogloss
<int><int><dbl><dbl>
130.70370374.266795

Caso mais de um modelo apresente o menor score para a métrica Logloss registrado dentro do dataset, selecionaremos o primeiro que foi listado para ser utilizado nas etapas seguintes.

In [ ]:
# Selecionando o primeiro registro dentro do dataframe.

bestAdaboost <- bestAdaboost[1,]

Recriaremos este modelo e imprimiremos suas estatísticas.

In [ ]:
# Define um seed para permitir que os mesmos resultados dos experimentos sejam reproduzíveis.

set.seed(100)

# Criando o modelo baseado no algoritmo Adaboost.

model_adaboost <-  adaboost(
    formula = f, 
    data    = train, 
    nIter   = bestAdaboost$nIter
)
In [ ]:
# Realizando as previsões com o modelo baseado no algoritmo Adaboost.

pred <- predict(model_adaboost, test, type = 'class')
In [ ]:
# Criando a Confusion Matrix a partir das previsões.  

cm <- confusionMatrix(table(pred = pred$class, data = test$heart_disease), positive = '2')

# Visualizando a Confusion Matrix.

cm
Confusion Matrix and Statistics

    data
pred  1  2
   1 10  3
   2  5  9
                                          
               Accuracy : 0.7037          
                 95% CI : (0.4982, 0.8625)
    No Information Rate : 0.5556          
    P-Value [Acc > NIR] : 0.08606         
                                          
                  Kappa : 0.4098          
                                          
 Mcnemar's Test P-Value : 0.72367         
                                          
            Sensitivity : 0.7500          
            Specificity : 0.6667          
         Pos Pred Value : 0.6429          
         Neg Pred Value : 0.7692          
             Prevalence : 0.4444          
         Detection Rate : 0.3333          
   Detection Prevalence : 0.5185          
      Balanced Accuracy : 0.7083          
                                          
       'Positive' Class : 2               
                                          
In [ ]:
# Salvando scores das métricas analisadas em um dataframe.

scoreModels <- rbind(scoreModels, data.frame(
    accuracy  = unname(cm$overall['Accuracy']),
    logloss   = LogLoss(predict(model_adaboost, test, type = 'raw')$votes[, 2], as.integer(test$heart_disease) - 1),
    row.names = 'adaboost'
))

5.5.4 Modelo XGboost

In [ ]:
# Gerando diferentes modelos baseados no algoritmo Adaboost e determinando sua acurácia e score para a métrica LogLoss.

featuresXGboost <- getBetterXGboostParameters(
    train       = as.matrix(train %>% select(-d)), 
    trainTarget = as.integer(train$heart_disease) - 1, 
    test        = as.matrix(test %>% select(-d)), 
    testTarget  = as.integer(test$heart_disease) - 1, 
    statusPrint = T
)
In [ ]:
# Salvando dataframe com os resultados gerados em um arquivo .csv.

fwrite(featuresXGboost, '/content/outputs/featuresXGboost.csv')

Caso deseje pular a execução do bloco de código anterior, basta carregar os resultados já processados que estão salvos no arquivo CSV abaixo:

In [ ]:
# Carregando dataframe com os resultados obtidos para cada modelo XGboost criado.

featuresXGboost <- fread('/content/outputs/featuresXGboost.csv')

Imprimiremos os registros dos modelos que apresentaram o menor score para a métrica Logloss.

In [ ]:
# Imprimindo registros dos modelos que alcançaram o menor score para a métrica Logloss.

bestXGboost <- featuresXGboost[featuresXGboost$logloss == min(featuresXGboost$logloss), ]

bestXGboost
A data.table: 1 × 6
maxDepththetanRoundsaccuracylogloss
<int><dbl><dbl><int><dbl><dbl>
10.51310.85185190.3528369

Caso mais de um modelo apresente o menor score para a métrica Logloss registrado dentro do dataset, selecionaremos o primeiro que foi listado para ser utilizado nas etapas seguintes.

In [ ]:
# Selecionando o primeiro registro dentro do dataframe.

bestXGboost <- bestXGboost[1,]

Recriaremos este modelo e imprimiremos suas estatísticas.

In [ ]:
# Define um seed para permitir que os mesmos resultados dos experimentos sejam reproduzíveis.

set.seed(100)

# Criando o modelo baseado no algoritmo XGboost.

  model_xgboost <- xgboost(
      data        = as.matrix(train %>% select(-d )) ,   # Define as variáveis preditoras.
      label       = as.integer(train$heart_disease) - 1, # Define a variável target.
      max.depth   = bestXGboost$maxDepth, # Define o tamanho máximo da árvore. 
      eta         = bestXGboost$eta,      # Define a taxa de aprendizado do modelo.
      nthread     = 16,                   # Define o número de threads que devem ser usadas. 
                                          # Quanto maior for esse número, mais rápido será o treinamento.
      nrounds     = bestXGboost$nRounds,  # Define o número de iterações.
      objective   = "binary:logistic",    # Define que o modelo deve ser baseado em uma regressão logistica binária.
      verbose     = T,                    # Define a exibição da queda da taxa de erro durante o treinamento.
      eval_metric = 'logloss',            # Define a função de avaliação a ser utilizada.
      maximize    = F                     # Define que a pontuação da avaliação deve ser minimizada.
)
[1]	train-logloss:0.527098 
[2]	train-logloss:0.456620 
[3]	train-logloss:0.399991 
[4]	train-logloss:0.371496 
[5]	train-logloss:0.357484 
[6]	train-logloss:0.350303 
[7]	train-logloss:0.342190 
[8]	train-logloss:0.335219 
[9]	train-logloss:0.328297 
[10]	train-logloss:0.320737 
[11]	train-logloss:0.315246 
[12]	train-logloss:0.309653 
[13]	train-logloss:0.303510 
[14]	train-logloss:0.299435 
[15]	train-logloss:0.294288 
[16]	train-logloss:0.289795 
[17]	train-logloss:0.286767 
[18]	train-logloss:0.283143 
[19]	train-logloss:0.280407 
[20]	train-logloss:0.278409 
[21]	train-logloss:0.276586 
[22]	train-logloss:0.274621 
[23]	train-logloss:0.272868 
[24]	train-logloss:0.269897 
[25]	train-logloss:0.267716 
[26]	train-logloss:0.266327 
[27]	train-logloss:0.264723 
[28]	train-logloss:0.263101 
[29]	train-logloss:0.261510 
[30]	train-logloss:0.260153 
[31]	train-logloss:0.258840 
In [ ]:
# Realizando as previsões com o modelo baseado no algoritmo XGboost.

pred <- predict(model_xgboost, as.matrix(test %>% select(-d)))
In [ ]:
# Definindo o threshold.

th <- bestXGboost$th

# Classificando os resultados segundo o threshold especificado.

predClass <- ifelse(pred > th, 1, 0)
In [ ]:
# Criando a Confusion Matrix a partir das previsões.  

cm <- confusionMatrix(table(pred = predClass, data = as.integer(test$heart_disease) - 1), positive = '1')

# Visualizando a Confusion Matrix.

cm
Confusion Matrix and Statistics

    data
pred  0  1
   0 12  1
   1  3 11
                                          
               Accuracy : 0.8519          
                 95% CI : (0.6627, 0.9581)
    No Information Rate : 0.5556          
    P-Value [Acc > NIR] : 0.001145        
                                          
                  Kappa : 0.7049          
                                          
 Mcnemar's Test P-Value : 0.617075        
                                          
            Sensitivity : 0.9167          
            Specificity : 0.8000          
         Pos Pred Value : 0.7857          
         Neg Pred Value : 0.9231          
             Prevalence : 0.4444          
         Detection Rate : 0.4074          
   Detection Prevalence : 0.5185          
      Balanced Accuracy : 0.8583          
                                          
       'Positive' Class : 1               
                                          
In [ ]:
# Salvando scores das métricas analisadas em um dataframe.

scoreModels <- rbind(scoreModels, data.frame(
    accuracy  = unname(cm$overall['Accuracy']), 
    logloss   = LogLoss(as.numeric(pred), as.matrix(as.integer(test$heart_disease) - 1)),        
    row.names = 'XGboost'
))

5.5.5 Modelo Neuralnet

In [ ]:
# Gerando diferentes modelos baseados no algoritmo Neuralnet e determinando sua acurácia e score para a métrica LogLoss.

featuresNN <- getBetterNNParameters(
    train       = train %>% mutate(heart_disease = as.integer(train$heart_disease) - 1), 
    test        = test %>% mutate(heart_disease = as.integer(test$heart_disease) - 1), 
    f           = f, 
    statusPrint = T
)
In [ ]:
# Salvando dataframe com os resultados gerados em um arquivo .csv.

fwrite(featuresNN, '/content/outputs/featuresNN.csv')

Caso deseje pular a execução do bloco de código anterior, basta carregar os resultados já processados que estão salvos no arquivo CSV abaixo:

In [ ]:
# Carregando dataframe com os resultados obtidos para cada modelo Neuralnet criado.

featuresNN <- fread('/content/outputs/featuresNN.csv')

Imprimiremos os registros dos modelos que apresentaram o menor score para a métrica Logloss.

In [ ]:
# Imprimindo registros dos modelos que alcançaram o menor score para a métrica Logloss.

bestNN <- featuresNN[featuresNN$logloss == min(featuresNN$logloss), ]

bestNN
A data.table: 16 × 5
nthresholdModelthresholdClassAccuracylogloss
<int><dbl><dbl><dbl><dbl>
290.9250.50.77777780.4308035
290.9300.50.77777780.4308035
290.9350.50.77777780.4308035
290.9400.50.77777780.4308035
290.9450.50.77777780.4308035
290.9500.50.77777780.4308035
290.9550.50.77777780.4308035
290.9600.50.77777780.4308035
290.9650.50.77777780.4308035
290.9700.50.77777780.4308035
290.9750.50.77777780.4308035
290.9800.50.77777780.4308035
290.9850.50.77777780.4308035
290.9900.50.77777780.4308035
290.9950.50.77777780.4308035
291.0000.50.77777780.4308035

Caso mais de um modelo apresente o menor score para a métrica Logloss registrado dentro do dataset, selecionaremos o primeiro que foi listado para ser utilizado nas etapas seguintes.

In [ ]:
# Selecionando o primeiro registro dentro do dataframe.

bestNN <- bestNN[1,]

Recriaremos este modelo e imprimiremos suas estatísticas.

In [ ]:
# Define um seed para permitir que os mesmos resultados dos experimentos sejam reproduzíveis.

set.seed(100)

# Criando o modelo baseado no algoritmo Neuralnet

nn <- neuralnet(
    formula       = f, 
    data          = train %>% mutate(heart_disease = as.integer(train$heart_disease) - 1), 
    hidden        = bestNN$n, 
    act.fct       = "logistic", 
    stepmax       = 1e+08, 
    linear.output = FALSE, 
    threshold     = bestNN$thresholdModel
)
In [ ]:
# Realizando as previsões com o modelo baseado no algoritmo Neuralnet.

pred <- predict(nn, test %>% mutate(heart_disease = as.integer(test$heart_disease) - 1))
In [ ]:
# Definindo o threshold.

th <- bestNN$thresholdClass

# Classificando os resultados segundo o threshold especificado.

predClass <- ifelse(pred > th, 1, 0)
In [ ]:
# Criando a Confusion Matrix a partir das previsões.  

cm <- confusionMatrix(table(
        pred = predClass, 
        data = as.integer(test$heart_disease) - 1
    ),
    positive = '1'
)

# Visualizando a Confusion Matrix.

cm
Confusion Matrix and Statistics

    data
pred  0  1
   0 12  3
   1  3  9
                                          
               Accuracy : 0.7778          
                 95% CI : (0.5774, 0.9138)
    No Information Rate : 0.5556          
    P-Value [Acc > NIR] : 0.01448         
                                          
                  Kappa : 0.55            
                                          
 Mcnemar's Test P-Value : 1.00000         
                                          
            Sensitivity : 0.7500          
            Specificity : 0.8000          
         Pos Pred Value : 0.7500          
         Neg Pred Value : 0.8000          
             Prevalence : 0.4444          
         Detection Rate : 0.3333          
   Detection Prevalence : 0.4444          
      Balanced Accuracy : 0.7750          
                                          
       'Positive' Class : 1               
                                          
In [ ]:
# Salvando scores das métricas analisadas em um dataframe.

scoreModels <- rbind(scoreModels, data.frame(
    accuracy  = unname(cm$overall['Accuracy']), 
    logloss   = LogLoss(pred, as.integer(test$heart_disease) - 1),        
    row.names = 'Neuralnet'
))

5.6 Determinando o melhor modelo

Determinaremos qual modelo teve o melhor desempenho para o conjunto de dados de teste a partir do score da métrica LogLoss.

A métrica LogLoss quantifica a precisão de um classificador penalizando classificações falsas. Quando minimizamos seu score estamos basicamente maximizando a precisão do modelo. Ou seja, o modelo que apresentar o menor valor para esta métrica será aquele que irá classificar corretamente a maior proporção de pacientes que efetivamente apresentam alguma doença cardíaca.

É interessante refletir sobre isso, pois não queremos classificar indivíduos saudáveis como doentes e submetê-los a tramentos desnecessários, caros ou até mesmo prejudiciais a saúde de pessoas sãs.

In [ ]:
# Exibindo dataframe com as métricas de performance dos melhores modelos treinados com cada algoritmo implementado.

scoreModels
A data.frame: 5 × 2
accuracylogloss
<dbl><dbl>
randomForest0.96296300.2005249
C5.00.85185190.2511706
adaboost0.70370374.2667947
XGboost0.85185190.3528369
Neuralnet0.77777780.4308035
In [ ]:
# Determinando o modelo que teve o melhor score para a métrica LogLoss.

scoreModels[scoreModels$logloss == min(scoreModels$logloss), ]
A data.frame: 1 × 2
accuracylogloss
<dbl><dbl>
randomForest0.9629630.2005249

Por fim, concluímos que o algoritmo que obteve a melhor performance para os critérios que adotamos foi o Random Forest.

Entre em contato comigo!

Caso tenha alguma dúvida, sugestão ou apenas queira trocar uma ideia sobre este projeto, não hesite em entrar em contato comigo!