Otimização do diagnóstico de câncer com Machine Learning

Daniel Volponi
8 min readApr 17, 2021

--

Photo by Michał Parzuchowski on Unsplash

Depois de Estimar o Consumo de Cerveja com Ciência de Dados (se você ainda não leu, é só clicar aqui!), resolvi apresentar o modelo desenvolvido no Curso “Machine Learning: Lidando com dados de muitas dimensões”, da Alura.

Da mesma forma que o projeto anterior, o texto está dividido nas fases de um projeto de Data Science.

1. Problema de Negócio

Será analisada uma base de dados que contém o diagnóstico de um tumor, que pode ser maligno ou benigno, e os resultados de 33 exames/dimensões que caracterizam esse diagnóstico.

Com o objetivo de tornar mais rápida e efetiva a detecção de determinados tipos de câncer, o estudo se propõe a responder a seguinte questão: É possível reduzir a quantidade de exames realizados e, ainda assim, chegar a um diagnóstico de maneira efetiva?

Ressalto que o presente estudo não tem relevância científica para quaisquer tipos de diagnósticos, servindo apenas para desenvolver técnicas de Data Science.

2. Obtenção e tratamento dos dados

A base de dados utilizada foi criada pela Alura a partir dos diagnósticos de câncer de mama de Wisconsin, Estados Unidos, e pode ser obtida aqui.

Dados:

  • ID — identificador atribuído a cada paciente.
  • diagnostico — resultado do diagnóstico do paciente, atribuindo “M” para maligno e “B” para benigno.
  • exame_1 a exame_33 — resultado de cada exame utilizado para diagnóstico.

2.1 Importação de dados no Jupyter Notebook

import pandas as pd

resultados_exames = pd.read_csv(“dataset/exames.csv”)
resultados_exames

O dataset possui 569 linhas e 35 colunas. Contendo o Id do Paciente, o diagnóstico e o resultado de 33 exames.

2.2. Verificação da existência de valores vazios

resultados_exames.isnull().sum()

O exame_33 possui 419 valores nulos, o que corresponde a cerca de 74% dos dados. Portanto, o resultado deste exame não é relevante para a construção do modelo.

3. Treinamento e Construção do Modelo de Machine Learning

Com os dados preparados, precisamos aplicar um algoritmo com todas as variáveis disponíveis que sirva como base de comparação com outros modelos compostos por um número menor de variáveis.

3.1. Separação dos dados para treino e teste

Por meio da função do sklearn train_test_split, o dataset será dividido de forma aleatória, sendo 70% dos dados para treinar o modelo e o restante para testar o quanto o nosso modelo consegue estimar os diagnósticos como malignos ou benignos.

Além disso, o dataset deve ser separado em X e Y, em que X são todas as colunas de exames e Y é o diagnóstico.

# Importando os pacotes

from sklearn.model_selection import train_test_split
from numpy import random
# Aplicando seed para aleatorizar os dados

SEED = 123143
random.seed(SEED)

valores_exames = resultados_exames.drop(columns=[‘id’, ‘diagnostico’])
diagnostico = resultados_exames[‘diagnostico’]
# Valores exames_v1 — retirado da coluna exame_33
valores_exames_v1 = valores_exames.drop(columns=’exame_33')

treino_x, teste_x, treino_y, teste_y = train_test_split(valores_exames_v1,
diagnostico,
test_size = 0.3)

3.2. Classificador Random Forest

O algoritmo Random Forest (Floresta Aleatória) é um método de aprendizado de máquina que consiste em treinar várias árvores de decisão a partir dos dados de treino e fazer predições com os dados de teste para identificar se o tumor é benigno ou maligno.

from sklearn.ensemble import RandomForestClassifier

classificador = RandomForestClassifier(n_estimators = 100)
classificador.fit(treino_x, treino_y)

print(“Resultado da classificação Random Forest %.2f%%” %(classificador.score(teste_x, teste_y)*100))

Pelo classificador de randomforest temos uma acurácia de 92,40%, ou seja, baseado no resultado dos exames o modelo conseguiu acertar o diagnóstico em 92,40% dos casos. Apesar de o índice estar acima de 90%, não podemos afirmar se o modelo é bom ou ruim, uma vez que não temos outros valores para usar como comparação.

3.3. Classificador Dummy

Como um primeiro comparativo vamos usar um classificador dummy com a estratégia do mais frequente. Este modelo gera uma previsão “burra”, já que sempre retorna o diagnóstico de maior frequência para todas as previsões.

from sklearn.dummy import DummyClassifier

SEED = 123143
random.seed(SEED)

classificador_dummy = DummyClassifier(strategy=”most_frequent”)
classificador_dummy.fit(treino_x,treino_y)
print(“Resultado da classificação Dummy %.2f%%” %(classificador_dummy.score(teste_x, teste_y)*100))

O resultado do classificador Dummy foi de 66,67%, índice este inferior ao Random Forest. Logo, permanecemos com o valor base de 92.40%.

3.4. Análise Gráfica — Violin Plot

O gráfico de violino (Violin Plot) permite identificar a frequência dos valores de cada exame divididos pelo resultado do diagnóstico. Antes de plotar, é necessário normalizar os dados para manter os valores na mesma escala, lançando mão da função do sklearn StandardScaler.

import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler

padronizador = StandardScaler()
padronizador.fit(valores_exames_v1)
valores_exames_v2 = padronizador.transform(valores_exames_v1)
# Transformando o valores exames_v2 em data Frame
valores_exames_v2 = pd.DataFrame(data = valores_exames_v2,
columns = valores_exames_v1.keys())

dados_plot = pd.concat([diagnostico, valores_exames_v2.iloc[:,0:10]], axis = 1)
dados_plot = pd.melt(dados_plot, id_vars=”diagnostico”, var_name = “exames”, value_name=”valores”)

plt.figure(figsize=(10,10))
sns.violinplot(x = “exames”, y = “valores”, hue= “diagnostico”, data = dados_plot, split = True)
plt.xticks(rotation = 90)
plt.show()

O exame 4 chama atenção porque não se modifica de acordo com a variável diagnostico, enquanto que os outros já apresentam diferenças. O exame_4 só exibe o valor 103.78 para todos os Ids. Por se tratar de uma constante, não faz diferença utilizá-la ou não no modelo, visto que o resultado é o mesmo tanto para os tumores malignos quanto para os benignos.

O exame 15 tem um alto pico para os benignos e um valor constante para os malignos.

O exame 29 só apresenta valores constantes e, tal qual o 4, pode ser retirado do modelo.

3.5. Recálculo sem as variáveis constantes

# Criando o valores_exames_v3 com o drop das colunas que são constantes
valores_exames_v3 = valores_exames_v2.drop(columns=[“exame_4”, “exame_29”])

def classificar(valores):
SEED = 123143
random.seed(SEED)
treino_x, teste_x, treino_y, teste_y = train_test_split(valores, diagnostico, test_size = 0.3)

classificador = RandomForestClassifier(n_estimators = 100)
classificador.fit(treino_x, treino_y)

print(“Resultado da classificação Random Forest %.2f%%” %(classificador.score(teste_x, teste_y)*100))

classificar(valores_exames_v3)

Resultado do Modelo: 91,81%.

Mesmo com o número reduzido de variáveis constantes, a acurácia foi próxima ao primeiro modelo de random forest.

4. Teste de outros algoritmos para selecionar as variáveis

Determinada a acurácia base, empregaremos outros algoritmos de classificação que selecionam um número diferente de variáveis.

4.1. Modelo K-Best

O método K-Best seleciona, a partir do número de variáveis (k), quais são as melhores para estimar o modelo. Por exemplo: Se fixarmos o k-best em 5, o algoritmo calcula um score para cada exame e escolhe os cinco melhores para o modelo utilizando a função qui quadrado.

from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2

selecionar_kmelhores = SelectKBest(chi2, k = 5)

SEED = 123143
random.seed(SEED)
treino_x, teste_x, treino_y, teste_y = train_test_split(valores_exames_v6,
diagnostico,
test_size = 0.3)

selecionar_kmelhores.fit(treino_x, treino_y)
treino_kbest = selecionar_kmelhores.transform(treino_x)
teste_kbest = selecionar_kmelhores.transform(teste_x)

classificador = RandomForestClassifier(n_estimators= 1000, random_state= 123143)
classificador.fit(treino_kbest, treino_y)
print(“Resultado da classificação com 5 KBest %.2f%%” %(classificador.score(teste_kbest, teste_y)*100))

Resultado da classificação com K-Best: 95.32%

Com o resultado de 95,32% (superior ao índice base 92,40%), obtido a partir de apenas 5 features, é possível alcançar uma redução de 85% na quantidade de exames necessários para o diagnóstico.

Contudo, essa diminuição acarreta a perda de um volume muito grande de informações importantes para o diagnóstico médico, o que pode impactar sua precisão: Será que estamos acertando mais quando o câncer é do tipo benigno? Ou errando mais quando ele é maligno?

Para responder, nada melhor que uma matriz de confusão, representada no SKlearn pela função confusion_matrix() e visualizada em um gráfico de calor (heatmap).

plt.figure(figsize= (10, 8))
sns.set(font_scale = 2)

sns.heatmap(matriz_confusao, annot = True, fmt = “d”).set(xlabel = “Predição”,
ylabel = “Real”)

De 114 casos de câncer benignos (0), o modelo estimou 110 como benignos e 4 como malignos. Quanto ao câncer maligno (1), dos 57 casos, 4 foram classificados como benignos e 53 como malignos. Em alguns segmentos, como na área da saúde, é essencial saber qual classificação atinge mais acertos. Imagine, por exemplo, uma pessoa que realmente tem câncer, mas recebe o diagnóstico de que não tem. O diagnóstico equivocado atrasa o tratamento, afinal, quanto mais precoce ele for, maiores as chances de cura.

4.2. Modelo Recursive Feature Elimination (RFE)

Consiste em treinar o modelo utilizando todo o conjunto inicial de variáveis. Após o primeiro treino, o RFE irá verificar a importância das features e, recursivamente, remover as menos relevantes até chegar ao número de features determinado. (Conceito retirado do texto: Como selecionar as melhores features para seu modelo de Machine Learning

from sklearn.feature_selection import RFE

selecionador_rfe = RFE(estimator= classificador, n_features_to_select = 5, step = 1)
selecionador_rfe.fit(treino_x, treino_y)
treino_rfe = selecionador_rfe.transform(treino_x)
teste_rfe = selecionador_rfe.transform(teste_x)

classificador = RandomForestClassifier(n_estimators= 1000, random_state= 123143)
classificador.fit(treino_rfe, treino_y)
print(“Resultado da classificação com 5 KBest %.2f%%” %(classificador.score(teste_rfe, teste_y)*100))

matriz_confusao = confusion_matrix(teste_y, classificador.predict(teste_rfe))

sns.heatmap(matriz_confusao, annot = True, fmt = “d”).set(xlabel = “Predição”,
ylabel = “Real”)

Resultado da classificação com RFE 92.98%.

O modelo RFE apresentou um desempenho inferior ao K-best no diagnóstico do câncer maligno.

Até o momento, elegemos algumas features com base em visualizações, como o Violin Plot, e com base em algoritmos mais automatizados, como o SelectKBest e o RFE. No caso desses algoritmos, determinamos quantas features gostaríamos que fossem selecionadas — no caso 5, mas poderiam ser 10, 15 ou qualquer outro número, dependendo da necessidade.

4.3. Modelo Recursive Feature Elimination Cross Validation (RFECV)

O RFECV divide o banco de dados em blocos e aplica o algoritmo RFE em cada um desses blocos, gerando diferentes resultados. Dessa forma, O RFECV não só nos informa quantas features precisamos ter para gerar o melhor resultado possível, como também quais features são essas.

from sklearn.feature_selection import RFECV

SEED= 1234
random.seed(SEED)

treino_x, teste_x, treino_y, teste_y = train_test_split(valores_exames_v6,
diagnostico,
test_size = 0.3)

classificador = RandomForestClassifier(n_estimators=100, random_state=1234)
classificador.fit(treino_x, treino_y)
selecionador_rfecv = RFECV(estimator = classificador, cv = 5, step = 1, scoring=”accuracy”)
selecionador_rfecv.fit(treino_x, treino_y)
treino_rfecv = selecionador_rfecv.transform(treino_x)
teste_rfecv = selecionador_rfecv.transform(teste_x)
classificador.fit(treino_rfecv, treino_y)

matriz_confusao = confusion_matrix(teste_y,classificador.predict(teste_rfecv))
plt.figure(figsize = (10, 8))
sns.set(font_scale= 2)
sns.heatmap(matriz_confusao, annot = True, fmt = “d”).set(xlabel = “Predição”, ylabel= “Real”)

print(“Resultado da classificação %.2f%%” %(classificador.score(teste_rfecv,teste_y)*100))

Resultado da classificação com RFECV 94.15%.

O método Cross Validation obteve um resultado superior ao RFE, selecionando 19 exames diferentes. Nesse contexto, os métodos n_features e grid scores indicam quais features o modelo utilizou.

O modelo grid_scores aponta, ainda, o quanto o número de exames afeta a acurácia do modelo:

import matplotlib.pyplot as plt

plt.figure(figsize = (14,8))
plt.xlabel(“Número de Exames”)
plt.ylabel(“Acurácia”)
plt.plot(range(1, len(selecionador_rfecv.grid_scores_)+1), selecionador_rfecv.grid_scores_)

5. Conclusão

Nesse problema de Machine Learning, recomenda-se tomar a decisão conservadora de adotar o modelo RFECV no processo de diagnóstico, a fim de economizar recursos e tempo por meio da redução de 14 exames em relação à quantidade do dataset original.

Por mais que quase todos os outros algoritmos apresentem uma acurácia maior, eles implicam queda muito grande de informações, ameaçando a precisão e qualidade do trabalho médico.

--

--