Predição da necessidade de leitos de UTI no Hospital Sírio Libanês

Machine Learning na COVID-19

Letícia Pires
Leti Pires
14 min readMar 23, 2021

--

Esse foi o tema do meu primeiro projeto de Machine Learning que desenvolvi no Bootcamp de Data Science Aplicada da Alura (que orgulho que me deu 🤩). E hoje vim aqui compartilhar um pouco mais sobre o projeto e como foi estruturado.

O livro que utilizei como referência para esse estudo (além das aulas do curso) foi o “Machine Learning: Guia de Referência Prática”. Esse livro simplesmente me salvou! (Vou fazer um post só sobre ele, mas de modo geral ele auxilia a seguir um fluxo de ideias pro projeto, desde a limpeza dos dados até a avaliação dos modelos preditivos).

Introdução

A pandemia de COVID-19 atingiu o mundo inteiro, sobrecarregando os sistemas de saúde — despreparados para uma solicitação tão intensa e demorada de leitos de UTI, profissionais, equipamentos de proteção individual e recursos de saúde. Países como o Brasil, que já possui sistema de saúde superlotados, vem sofrendo com a falta de leitos de Unidade de Terapia Intensiva (UTI) na internação de seus pacientes.

Com base nesses acontecimentos e até mesmo na prevenção de sobrecarga do sistema de saúde das redes privadas, o Hospital Sírio-Libanês, busca prevenir e até mesmo predizer, com base em dados clínicos de seus pacientes, conforme forem sendo admitidos no ambiente hospitalar, a necessidade ou não de internação nas UTIs nas próximas horas. A proposta feita pelo Hospital Sírio Libanês está disposta nesse link do Kaggle.

Sendo assim, o objetivo desse projeto foi de prever quais pacientes precisariam ser admitidos na unidade de terapia intensiva e assim, definir qual a necessidade de leitos de UTI do hospital, a partir dos dados clínicos individuais disponíveis.

A ideia deste artigo é ser um ponto de referência e geração de insights para seus projetos. Você pode ter acesso completo ao notebook desse projeto clicando no link abaixo:

Para contextualizar, o conjunto de dados está dividido em janelas pacientes que fizeram admissão na UTI, como mostra a figura abaixo:

Para realizar o projeto, alguns critérios foram obrigatórios. Não pude utilizar os dados quando o paciente deu entrada na UTI -> ICU = 1, pois estes já terão ido diretamente para a UTI nas 2 primeiras horas, não importando para a predição.

O projeto ficou dividido nas seguintes etapas:

Etapas do projeto.

1. Importações

Para começar, importei algumas bibliotecas importantes para esse trabalho. Dentre elas, utilizei: pandas, numpy, seaborn, matplotlib, sklearn, yellowbrick e xgboost:

2. Coleta de dados

Carreguei o arquivo csv com os atributos das amostras do Covid 19. Esse arquivo contém várias colunas, incluindo uma coluna relacionada à UTI (ICU em inglês).

Para visualizar os dados, apliquei o método head() que nos trazer os 5 primeiros dados do nosso dataset:

O dataset acima não está completo, pois ele possui 231 colunas. Explicando melhor sobre o dataset, temos pacientes positivos para COVID-19 internados no Hospital Sírio Libânes de São Paulo-SP e Brasília-DF. De forma geral, temos até 5 entradas que representam dados de um mesmo paciente. Essas entradas são referentes à diferentes janelas de tempo (0–2, 2–4, 4–6, 6–12, >12) em que os pacientes foram acompanhados. Para cada uma das janelas, foram dosados diversos marcadores biológicos e sinais vitais.

Resumidamente, o layout dos dados estruturados e a estrutura do banco é a seguinte:

Layout do dados e Estrutura de repetição dos dados no banco usado no desafio.

3. Limpeza dos dados

Depois de conhecer melhor os dados, realizei uma limpeza neles.

É importante garantir que os dados estejam em um formato adequado para criar o modelo posteriormente. A maioria dos modelos do scikit learn exige que nossos atributos sejam numéricos. Além disso, muitos modelos falham caso recebam dados ausentes (NaN).

Para isso, inicialmente apliquei a função fill_table (criei funções para facilitar no desenvolvimento do projeto) para agrupar a coluna PATIENT_VISIT_IDENTIFIER e preencher dados faltantes através dos métodos bfill() e ffill().

Em seguida, realizei uma query por todas as colunas/linhas que possuem WINDOW=='0-2' eICU==1, que deverão ser removidas da base de dados, como mencionado lá no início. Em seguida, utilizei o dropna para remover dados NaN.

Em seguida, identifiquei que o dataset ainda apresentava colunas do tipo string, AGE_PERCENTIL e WINDOW . Como mencionei, essas colunas podem atrapalhar no modelo depois. Então para resolver, utilizei uma solução encontrada no livro de Matt Harrison. Criei colunas dummy a partir de colunas de string, e aplicando o get_dummies, foi criada uma coluna pra cada valor, sendo preenchido por 0 e 1.

Além disso, inicialmente também foi aplicado a funçãoprepare_window para ficar somente com o quadro de pacientes que entram nas primeiras 2 horas.

Depois de fazer a limpeza inicial dos dados, fiz um novo tratamento, pois talvez nem todas as colunas sejam necessárias, podendo atrapalhar nas predições.

Segundo Harrison (2020), “removemos qualquer coluna com uma correlação perfeita ou com uma correlação positiva ou negativa bem alta. A multicolinearidade pode causar impactos na interpretação da importância dos atributos e dos coeficientes em alguns modelos”.

Então removi todas as colunas com correlação perfeita ou alta correlação. Para fazer isso utilizei a técnica elaborado pelos instrutores do Bootcamp, Thiago Gonçalves e Alan Spadinni nas aulas do Módulo 6, que consiste na criação de correlação de todas as linhas a partir da quarta coluna e antepenúltima (sem considerar WINDOW e ICU). Em seguida é gerado uma correlação e transformação dos valores para absoluto (não negativos).

“A correlação vai de -1 a 1. Altas correlações aproximam de -1 e +1. Quando é próxima de -1 ocorre o inverso, então uma variável cresce e outra sobre. Quando é próxima de +1, é proporcional, quando uma sobre a outra também sobe.”

Em seguida o any busca dentro dentro da matriz de correlação variáveis com alta correlação (valores acima do valor_corte) e através do laço for, isso é repetido pra todas as colunas. E caso seja verdadeiro, fica armazenado dentro de uma lista.

No entanto, fazendo somente essas ações não é possível, visto que, todas as colunas seriam excluídas. Por isso, podemos observar através da figura abaixo que a coluna na diagonal é igual a 1, e o triângulo superior é um espelho do inferior. Sendo assim, aplicando o where, np.ones(matriz com 1s) e np.triu(elimina triângulo abaixo da diagonal igual a 1).

4. Separação das amostras

Segundo Harrison (2020) sempre devemos fazer treinamento e testes em dados distintos. Caso contrário, você não saberá realmente quão bem seu modelo poderá ser generalizado para dados que ainda não tenham sido vistos antes.

Utilizei o scikit-learn para fazer essa separação dos dados através do método train_test_split :

5. Criando o modelo

Para o projeto decidi fazer a análise utilizando 4 modelos de classificação: Regressão Logística, Árvore de Decisão, Floresta Aleatória e o XGBoost.

No entanto, para exemplificar aqui, vou utilizar somente o modelo de Floresta Aleatória (ou Random Forest), que aparentemente se apresentou melhor na análise.

Uma floresta aleatória é um conjunto de árvores de decisão. Ao criar árvores treinadas com sub-amostras e atributos aleatórios de dados, a variância é reduzida. A ideia das florestas aleatórias é criar uma “floresta” de árvores de decisão treinadas em diferentes colunas de dados de treinamento.

Então, inicialmente passei a biblioteca RandomForestClassifier . Apliquei um n_estimators = 100 , que representa a adição de mais árvores, evitando uma superadequação.

Em seguida, para treinar o modelo, é necessário passar o método .fit com os dados de treino e para fazer a predição aplicar o método .predict com o x_test.

Como comentei anteriormente, automatizei esse processo dentro de funções. Então apliquei a separação dos dados de treino e teste, treinamento e predição em cada função de métrica, como mostra o tópico seguinte.

6. Métricas e avaliação de classificação

A fim de avaliar o comportamento do modelo, utilizei as seguintes métricas: acurácia, matriz de confusão, recall, precisão, F1 e curva ROC.

6.1. Acurácia

A acurácia representa a porcentagem de classificações corretas do modelo. Ou seja, quanto maior a acurácia, mais próximo da referência ou valor real. Através da função abaixo, separei meus dados de treino e teste, treinei o modelo, fiz a predição e calculei a acurácia.

Passando os dados e o modelo de Floresta Aleatória na função, tem-se o seguinte:

O valor de acurácia obtido do modelo foi de 65,91%. Ou seja, de todas as classificações, somente 65,91% foram corretas.

6.2. Matriz de Confusão

Outra métrica importante para avaliar os modelos de Machine Learning é a Matriz de Confusão. Ela pode ajudar a compreender o desempenho de um classificador. Um classificador binário pode ter quatro resultados de classificação:

  • VP: verdadeiro positivo (a pessoa precisa de UTI e o modelo acertou)
  • VN: verdadeiro negativo (acerto para quando a pessoa não precisa de UTI)
  • FP: falso positivo (O modelo diz que é 1, mas a label é 0)
  • FN: falso negativo (o modelo diz que é 0, mas a label é 1)

As duas primeiras classificações representam a maneira correta.

Para entender mais sobre matriz de confusão, clique nesse link.

A função abaixo separa os dados de treino e teste, treina o modelo, faz a predição e cria o gráfico de matriz de confusão.

O resultado da matriz gerada é o seguinte:

Em resumo, o modelo acertou 24 vezes para pacientes que devem ser internados na UTI (verdadeiro positivo) e 34 vezes para pacientes que não devem ser internados (verdadeiro negativo).

No entanto, o modelo previu 13 vezes para pacientes que devem ser internados (falso positivo), que na realidade não devem. E previu 17 vezes para pacientes que não deveriam ser internados (falso negativo), sendo que deveriam ser.

É insano prever que uma pessoa não precisa de leito, sendo que precisa. Assim como também, superlotar leitos de UTI com pessoas que não precisariam estar ali.

6.3. Precisão, Recall e F1

Aprofundando mais as métricas, podemos utilizar o ClassificationReport para encontrar a precisão, recall e F1. A função abaixo roda_modelo gera essas métricas.

O resultado obtido foi o seguinte:

Para valores igual a 0 (pacientes que não precisam de UTI), possuimos uma precisão de 73% e para valores igual a 1 (pacientes que precisam de UTI) a precisão é de 67%. Segundo este artigo, a PRECISÃO pode ser usada em situação em que os falsos positivos são considerados mais prejudiciais que os falsos negativos, ou seja, será importante para o caso de estarmos internando mais pessoas do que o necessário, superlotando o hospital e fazendo com que pessoas que precisem de fato, fiquem sem leitos de UTI.

Já o RECALL pode ser usada em uma situação em que os Falsos Negativos são considerados mais prejudiciais que os Falsos Positivos. Por exemplo, o modelo deve de qualquer maneira encontrar todos os pacientes que precisam de UTI, mesmo que classifique os que não precise (situação de Falso Positivo). Ou seja, o modelo deve ter alto recall ou alta sensibilidade, pois imagina classificar pacientes que precisam de UTI como pacientes que não precisam, seria um problema muito grave.

No caso, nosso recall apresentou para 0, um desempenho alto de 70%, enquanto para a classe 1, 71%.

O f1 score nos mostra uma média harmônica entre precisão e recall. Para 0, apresentou 72% e para 1, 69% (representa que ou recall ou precisão estão baixos).

6.4. Curva ROC

Outra métrica utilizada é o roc_auc, ou área sob a curva ROC (Receiver Operator Characteristic). A curva ROC mostra o desempenho do classificador, exibindo taxa de verdadeiros positivos à medida que a taxa de falsos positivos muda. A regra geral é que o gráfico deve ter uma protuberância em direção ao canto superior esquerdo. Um traçado que esteja à esquerda e acima de outro sinaliza um desempenho melhor.

A função abaixo plota a curva ROC:

Chamando a função…

O valor AUC ficou em 0.79, que pode ser considerado um bom desempenho. Modelos com AUC igual a 0 representam previsões 100% erradas. Quanto mais próximo de 1, melhor o modelo é.

Comparando com os outros modelos utilizados no projeto, este apresentou um valor de AUC melhor, mas o modelo ainda depende de hiperparâmetros para melhorar seu desempenho. Pra otimizar o modelo, utilizamos a curva de validação.

8. Seleção do modelo

Existem duas formas possíveis de avaliar um modelo. Através da Curva de Validação e Curva de Aprendizagem.

  • Curva de Validação: é uma forma de determinar um valor apropriado para um hiperparâmetro. Uma curva de validação é um gráfico que mostra como o desempenho do modelo responde a mudanças no valor do hiperparâmetro. O gráfico mostra tanto os dados de treinamento como de validação. As pontuações dos dados de validação nos permitem inferir como o modelo responderia a dados não vistos anteriormente. Em geral, escolhemos um hiperparâmetro que maximize a pontuação dos dados de validação.
  • Curva de Aprendizagem: pode nos ajudar a seguinte pergunta: Quantos dados sserão necessários para selecionar o melhor modelo para o projeto? O gráfico mostra as instâncias de treinamento e a pontuação para validação cruzada a medida que criamos modelos com mais amostras. Se a pontuação da validação cruzada continuar a subir, por exemplo, poderá ser sinal de que mais dados ajudariam o modelo a ter melhor desempenho. Se houver variabilidade (uma área sombreada grande) na pontuação de dados de treinamento, é sinal de que o modelo sofre de erros de viés e é simples demais (há subadequação). Se houver variabilidade na pontuação para validação cruzada, é porque o modelo está sujeito a erros de variância e é complicado demais (há superadequação).
Curva de Validação e Curva de Aprendizagem, respectivamente.

Através da função abaixo, criei o gráfico de Curva de Validação para o modelo citado:

Aplicando a função…

O resultado obtido para a Curva de Validação foi o seguinte:

A linha azul representa os dados de treino. Essa linha mostra, que a cada valor max_depth, a pontuação se aproxima de 1.00, que pode levar a um overfitting. O overfitting ocorre quando o modelo se adaptou muito bem aos dados com os quais está sendo treinado, porém, não generaliza bem para novos dados. Ou seja, o modelo decorou o conjunto de dados de treino, mas não aprendeu de fato o que diferencia aqueles dados para quando enfrentar novos testes. E isso, podemos perceber através da linha verde (que representa os dados de teste), que fica sempre no intervalo de 0.60 a 0.85.

E a função abaixo representa o gráfico da Curva de Aprendizagem para o modelo citado:

Aplicando a função…

O resultado obtido para a Curva de Aprendizagem foi o seguinte:

Na Curva de Aprendizagem do modelo, percebe-se que a curva de Validação Cruzada (verde) começa a subir, o que pode ser um sinal de que mais dados ajudariam o modelo a ter um desempenho melhor. Mas parece manter constante depois mesmo adicionando dados.

A pontuação da validação cruzada (curva verde) apresenta uma variabilidade (área sombreada grande), indicando que o modelo está sujeito a erros de variância e é complicado demais, ou seja, há superadequação.

E os dados de treinamento (linha azul) apresenta um desempenho constante. Adicionar mais dados, não vai influenciar no modelo.

7. Validação Cruzada

A validação cruzada é uma técnica para avaliar a capacidade de generalização de um modelo, a partir de um conjunto de dados. Ou seja, busca-se estimar o quão preciso é este modelo na prática, o seu desempenho para um novo conjunto de dados.

Uma das maneiras de fazer a divisão desses dados é usando o método holdout, ele consiste em dividir os dados em 70–30 de maneira aleatória. Ou seja, 70% dos dados para treino e 30% para teste.

Para fazer isso, é utilizado o cross_validate, que tem como principal objetivo evitar problemas de aleatoriedade.

O cross_validate pega os dados, separa em porções de treino e teste, embaralha os dados e divide em k números de dobras (grupos), para assim termos conjuntos diferentes de dados para treino e teste. O resultado do nosso modelo vai ser a média dos valores de cada um desses grupos.

* A única desvantagem de utilizar o cross_validate é o desempenho, se você tiver uma base de dados muito grande rodar um cross validation vai ser muito custoso computacionalmente.

Utilizei a função abaixo para calcular a Validação Cruzada.

Aplicando a função no modelo…

O resultado obtido foi um AUC de 0.7955–1.0, mostrando um desempenho melhor comparado aos outros modelos.

Conclusões

Considerando a situação emergencial do COVID-19 no Brasil, a necessidade de uma solução rápida, todo esse processo de criação de um modelo deve ser tratado da forma mais séria possível. Trabalhar com dados reais pode ser algo muito complexo nesse caso.

Este projeto está finalizado no momento, no entanto, ao longo dos próximos meses será aperfeiçoado, conforme a nova remessa de dados e meus novos aprendizados sobre Machine Learning.

Para conferir mais sobre meu projeto, só acessar o link do meu repositório no Github: https://github.com/letpires/ICU_prediction_sirio_libanes

--

--

Letícia Pires
Leti Pires

Jr. Data Scientist at Sauter 💜 & Civil Engineer . Content creator in @letispires. Fond of Python, Machine Learning and AI.