Criando modelos de aprendizado para diagnóstico de saúde fetal

Gabriel Ribeiro Ferreira Lopes
22 min readOct 2, 2023

--

Photo by Isaac Quesada on Unsplash

Muitas mães e bebês correm risco de vida por não terem um acompanhamento durante o período de gestação. Nos EUA, cerca de 1 milhão de bebês não-nascidos morrem, com quase 26 mil mortes sendo de bebês com mais de 20 semanas. No Brasil, a taxa de mortalidade chega a ser de 10 óbitos para cada 1000 nascimentos.

O cuidado com a saúde da mãe e do bebê é algo que necessita da coordenação de diversas áreas, desde o acompanhamento médico, assistência social, infraestrutura de transporte até o desenvolvimento de tecnologias para identificação, prevenção e tratamento das complicações que podem surgir durante a gravidez.

Lidar com esse sério problema da mortalidade maternal e fetal, portanto, é algo que cada segmento da sociedade pode ajudar direta ou indiretamente, para poder compensar as faltas de recursos e acompanhamento médico que ainda é a causa da maioria das mortes.

Salvando as duas vidas com dados

Pensando nisso, este projeto de ciência de dados e aprendizado de máquina busca contribuir analisando dados de exames pré-natais com o objetivo de reduzir a mortalidade fetal e maternal.

O exame a ser analilsado é o CTG, ou cardiotocografia, que é um procedimento que utiliza de pulsos de ultrassom para analisar a frequência cardíaca do bebê, o movimento fetal, contrações uterinas, entre outros indicadores.

O CTG é um exame simples e acessível para assegurar a saúde do bebê, permitindo que médicos e profissionais da saúde identifiquem problemas com antecedência, olhando por doenças congênitas, condições hereditárias e complicações diversas da gestação.

Portanto, espera-se responder com este projeto as seguintes questões:

  • É possível utilizar dados do exame de CTG para diminuir o risco de mortalidade da mãe e do bebê em gestação?
  • É possível providenciar informações a neonatologistas e obstetras sobre os fatores que mais contribuam para a previsão de risco?
  • Quais abordagens podem ser tomadas para implementar essa checagem e facilitar o reconhecimento desses riscos?

A abordagem do problema passará pela criação de modelos supervisionados de classificação, com a construção de pipelines para pré-processamento e ajuste dos dados, buscando minimizar a taxa de erros de falsos negativos, que corresponderiam a casos graves classificados como normais., priorizando, é claro, a detecção de casos de risco.

Iremos utilizar bibliotecas como Scikit-learn para construção desses pipelines, além de uma abordagem baseada em AutoML com o PyCaret.

O conjunto de dados

Foram reunidos dados de exames de CTG de diversas partes do mundo por um grupo de cientistas, visando construir um sistema automatizado de análise de cardiotocografias [1].

O conjunto de dados é composto por 2126 entradas e 22 variáveis, que correspondem a características fisiológicas analisadas pelo exame, como batimentos cardíacos, movimentos e contrações uterinas, entre outros.

A seguir, você pode conferir o dicionário de variáveis:

1. Análise exploratória e visualização dos dados

Na análise exploratória, iremos entender melhor o contexto dos dados e como os diferentes atributos se correlacionam para ajudar na determinação da saúde do bebê.

Numa análise inicial, foi verificado que o conjunto de dados não possui valores ausentes, todos os atributos são numéricos do tipo float64 e a maioria são contínuos.

Variável-alvo

A variável-alvo chama-se fetal_health e retorna as classes de diagnóstico da saúde do bebê: (1) Normal, (2) Suspeito e (3) Patológico. Dentre essas classes, a mais frequente é a normal, com 1655 exemplos, seguido de casos suspeitos com 295 e, por último, os patológicos, com 176 exemplos.

Queremos construir um modelo que identifique casos suspeitos e patológicos independentemente da gravidade implícita nessas classes. Isto significa que estaremos tratando dos bebês e das mães ao primeiro sinal de irregularidades na gestação, com exames de rotina e acompanhamento médico.

Por isso, as duas classes serão reunidas em uma só, denominada Grave., tornando o problema de classificação binário. Para isso, foi criada uma nova variável-alvo, chamada fh_class, enquanto a antiga foi excluída para evitar redundância no conjunto de dados.

Nessa nova distribuição, teremos 1655 exemplos normais e 471 exemplos graves no conjunto de dados. A Figura 1.1 abaixo mostra essas distribuições.

Figura 1.1 — Distribuição das classes na variável-alvo. (a) Original e (b) reunindo as classes de casos suspeitos e patológicos numa classe só.

Nota-se que o problema é bastante desbalanceado, como esperado.

Correlações

Vamos analisar as correlações que cada um dos atributos tem com a variável-alvo, buscando compreender quais variáveis podem influenciar mais ou menos para a determinação de um caso grave.

Definimos as correlações como segue:

# definindo as correlações para todas as variáveis
corr = df_new.corr()

# definindo uma variável para as correlações com o alvo apenas
corr_target = corr['fh_class'][:-1].sort_values(ascending = False)

Em seguida, o gráfico foi construído a partir do da biblioteca Matplotlib, resultando na Figura 1.2 abaixo:

Figura 1.2 — Correlações dos atributos com a variável-alvo.

No gráfico de correlações, temos dispostas o quão relacionadas estão as variáveis independentes com o alvo, tanto positivamente quanto negativamente. Valores positivos da correlação apontam para uma maior probabilidade daquela variável prever a classe 1 (Grave), ao passo que valores negativos apontam para a probabilidade das variáveis ajudarem na previsão da classe 0 (Normal).

É importante lembrar que correlação não implica necessariamente causalidade, ou seja, não é porque duas variáveis estão correlacionadas que uma seja a causa da outra. Com isso em mente, as variáveis que mais se correlacionam positivamente com o alvo são:

  1. Variabilidade anormal de curto prazo (abnormal_short_term_variability) (+ 0.493)
  2. Variabilidade anormal de longo prazo (abnormal_long_term_variability) (+ 0.489)
  3. Desacelerações prolongadas (prolongued_decelerations) (+ 0.340)
  4. Valor de base (baseline_value) (+ 0.252)

As variáveis mais correlacionadas negativamente são:

  1. Acelerações (accelerations) (- 0.395)
  2. Contrações uterinas (uterine_contractions) (- 0.264)
  3. Valor médio da variabilidade de curto prazo (mean_value_short_term_variability) (- 0.208)
  4. Valor médio da variabilidade de longo prazo (mean_value_long_term_variability) (- 0.172)

Como é possível constatar, as variabilidades anormais se correlacionam mais fortemente com o alvo, de modo que convém visualizar a dispersão dos dados com relação a essas variáveis.

A seguir, iremos visualizar como estão distribuídos esses dados, buscando construir uma intuição mais robusta acerca de como cada atributo é capaz de dividir os dados entre as classes.

O que são as medidas de variabilidades anormais?

Antes de seguir, vamos esclarecer alguns termos técnicos importantes, como os de variabilidades anormais.

Variabilidade anormal de curto prazo refere-se às flutuações dos batimentos cardíacos do bebê, que acontecem num período de segundos.

Pequenas variações são consideradas sinais positivos, visto que mostra atividade do sistema nervoso autônomo do bebê. Em um bebê saudável, é esperado que a taxa de batimentos cardíacos varie ligeiramente em comparação com os valores de base.

As variações se tornam motivo de preocupação quando indicam redução ou inexistência de batimentos com maior frequência e por períodos mais longos. Elas podem indicar que o bebê sofre de hipóxia (falta de oxigênio), ou acidemia (aumento da acidez do sangue), entre outras condições que podem afetar seu sistema nervoso.

De maneira análoga, a variabilidade anormal de longo prazo se refere a variações na taxa de batimentos cardiácos do bebê que ocorrem em intervalos de minutos. Elas representam desvios significativos dos padrões de base para os batimentos cardíacos por longos períodos.

Essa condição pode indicar falta de oxigênio ou compressão do bebê pelo cordão umbilical.

A medida de variabilidade está associada também às desacelerações prolongadas, quando a taxa de batimentos do bebê cai para valores muito abaixo dos padrões por longos períodos de tempo.

Contrações uterinas e variabilidades anormais

A Figura 1.3 mostra a relação entre as variabilidades de curto e longo prazo e as contrações uterinas.

Figura 1.3 — Relação entre os atributos de contrações uterinas e variabilidade de (a) curto e (b) longo prazo.

Para variações anormais de curto prazo, note que é normal por alguns segundos não haver contrações uterinas, sem que isso signifique que o bebê esteja em risco. Se o bebê está a mais de 60% do tempo com variações anormais de curto prazo, e sem contrações uterinas, então muito provavelmente ele está em risco.

Para variações anormais de longo prazo, a tolerância é bem menor para contrações uterinas nulas. Se o bebê está a mais de 15% do tempo com variações anormais de longo prazo e sem contrações uterinas, significa que ele está em risco.

Há maior probabilidade de bebês com saúde normal apresentarem contrações uterinas medidas entre 0.002 e 0.011, como pode ser visto nos gráficos ao observar a densidade de pontos relacionados à classe normal nesse intervalo.

Movimentos fetais e variabilidades anormais

A Figura 1.4 mostra como o movimento fetal se relaciona com as variabilidades.

Figura 1.4 —Movimento fetal relacionado às variações de (a) curto prazo e (b) longo prazo.

Para variações de curto prazo, nota-se que:

  • Pouco movimento fetal para até 50% de variação é considerado normal. A partir de 60% do tempo com variações anormais e sem movimento, maiores as chances de risco.
  • Movimento fetal acima de 0.3 é um sinal de risco, independente de haver ou não muitas variações de curto prazo.

Para variações de longo prazo, nota-se que:

  • Nenhum movimento fetal até 20% de variações anormais são, em sua maioria, casos normais.
  • Se o bebê não se move e apresenta variações anormais dos batimentos cardíacos por longos períodos, então ele está em risco.
  • Novamente, vemos que se o bebê está muito agitado, com medidas de movimento fetal acima de 0.3, então ele provavelmente está em risco.

Acelerações e variabilidades anormais

As acelerações se referem a aumentos momentâneos da taxa de batimentos cardíacos do bebê acima dos valores padrões. Elas são consideradas um sinal positivo de que o bebê está responsivo e recebendo oxigênio e nutrientes suficientes. A presença das acelerações frequentemente indicam, portanto, que o bebê não está sob estresse no momento.

No exame de CTG, as acelerações são medidas observando-se o aumento e o retorno dos batimentos para o valor básico. Usualmente, essas acelerações acontecem por um curto período, tipicamente 15 segundos, mas podem variar.

A Figura 1.5 mostra a relação entre as variáveis.

Figura 1.5 — Relação entre as variáveis de acelerações e variabilidades de (a) curto e (b) longo prazo.

Como vimos acima, as acelerações indicam que o bebê é saudável e, quanto mais acelerações, maiores são as chances da saúde do bebê estar boa. De fato, é o atributo que mais se correlaciona negativamente com a classe de risco.

As incidências de risco para a saúde do bebê ocorrem justamente para valores de acelerações baixas (< 0.005) ou nulas.

Como as variabilidades se relacionam entre si

A Figura 1.6 mostra a correlação entre as duas variabilidade anormais de curto e longo prazo.

Figura 1.6 — Relação entre as variabilidades anormais.

Note que todos os casos de risco se encontram na região de alta porcentagem de tempo com variabilidades anormais, tanto no curto (> 60%) quanto no longo prazo (> 20%). Estes dois atributos são de fato bem informativos para nossa análise e distinguem bem entre os casos normais e graves.

2. Pré-processamento dos dados

Seguindo da análise exploratória, o pré-processamento dos dados envolveu a criação de uma cópia de backup do Dataframe e a divisão entre matriz de atributos e variável-alvo.

Com isso, pôde-se dividir os conjuntos de treino e teste, utilizando o método train_test_split do Scikit-learn. Para os testes, foram separados 30% dos dados, restando 70% dos exemplos para treino.

df_proc = df_new.copy()

# Matriz de atributos X e a variável-alvo y
X = df_proc.drop('fh_class', axis = 1)
y = df_proc['fh_class']

from sklearn.model_selection import train_test_split

# Separação dos conjuntos de treino e teste
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .3, stratify = y, random_state = 42)

3. Modelo de base

O modelo de base fornecerá a performance de referência de um classificador básico sem otimizações, que no caso será o de árvore de decisão. A partir dele, iremos testar novos modelos e comparar os respectivos desempenhos.

Temos duas situações, uma com dados desbalanceados para a variável-alvo, havendo uma proporção maior de exemplos da classe que indica a saúde normal do bebê, e outra com dados balanceados. Para este último caso, iremos utilizar o argumento class_weight = 'balanced' para assegurar o balanceamento feito pelo algoritmo do classificador.

# Baseline desbalanceado
dt = DecisionTreeClassifier(criterion = 'gini',
max_depth = 5, random_state = 42)

dt.fit(X_train, y_train)

# Baseline balanceado
dt_bal = DecisionTreeClassifier(max_depth = 5, class_weight = 'balanced',
random_state = 42)

dt_bal.fit(X_train, y_train)

A performance dos dois modelos pode ser visualizada a partir da matriz de confusão na Figura 3.1.

Figura 3.1 — Matrizes de confusão dos modelos básicos de árvore de decisão com dados (a) desbalanceados e (b) balanceados.

Obtivemos um recall maior para o modelo de base balanceado, de 84%. A precisão, contudo, para a classe 1 foi de 0.89 para 0.78, o que é de se esperar sempre que se faz a troca pela diminuição da taxa de falsos negativos.

A Figura 3.2 mostra a importância de cada atributo para a previsão das classes. Note que, assim como pudemos inferir a partir da análise exploratória dos dados, as variáveis relacionadas com variabilidades anormais de curto prazo, acelerações e desacelerações prolongadas são muito informativas na previsão dos casos normais e de risco.

Figura 3.2 — Feature importances para o modelo de base balanceado.

Veja como esse gráfico foi construído acessando o notebook do projeto.

O modelo de baseline de árvore de decisão mostra que a abordagem com dados balanceados é a indicada para melhorar a métrica de recall, enquanto mantemos um nível aceitável de acurácia e precisão. Porém, ainda há 16% de casos falsos negativos, ou seja, que são de risco mas foram classificados como normais.

É preciso diminuir a taxa erros do tipo II para o mínimo possível e, para isso, vamos utilizar das técnicas de busca em grade (Grid Search) e validação cruzada (Cross Validation), visando construir novos modelos e aumentar as performances baseadas na métrica recall.

4. Construção de modelos com Grid Search e Validação Cruzada

Seguindo agora para a construção dos modelos e as comparações de performance de cada um, iremos definir algumas funções que nos ajudarão em três etapas. São elas: (I) a criação de um pipeline para padronização dos dados e implementação do classificador, (II) uma função para implementar o objeto de Grid Search CV voltado para maximizar a métrica recall e (III) uma função para ajustar os dados de treino e teste, retornando os melhores desempenhos.

4.1 Pipeline

A primeira função é bem simples, trata-se da função para criar um pipeline com passos de padronização dos dados e implementação do modelo com classes balanceadas.

A função recebe o modelo do classificador que se quer trabalhar, por exemplo, RandomForestClassifiere retorna o pipeline.

Iremos testar 4 modelos. São eles: Logistic Regression, Random Forest, SVM e Extra Trees Classifier.

def create_pipeline(classifier):

'''
Cria um pipeline com etapas de normalização e implementação do classificador
'''

pipe = Pipeline([('std', StandardScaler()),
('clf', classifier(class_weight = 'balanced', random_state = 42))])

return pipe

4.2 GridSearchCV

Após implementar o modelo via pipeline, iremos aplicar a função que instancia o objeto de GridSearchCV para realizar a otimização com a técnica de busca em grade e validação cruzada. Em vista disso, para cada modelo foi montado um dicionário de parâmetros a serem otimizados.

Para cada um dos modelos, os dicionários foram os seguintes:

param_range = np.arange(1, 11)
param_small = [1.0, .5, .1, .01]

# Regressão Logística
param_lr = [{'clf__penalty': ['l1', 'l2'],
'clf__C': param_small,
'clf__solver': ['lbgfs', 'liblinear']}]

# Random Forest
param_rf = [{
'clf__criterion': ['gini', 'entropy'],
'clf__max_depth': param_range
}]

# SVM
param_svm = [{
'clf__C': param_range,
'clf__kernel': ['linear', 'rbf'],
}]

# Extra Trees Classifier
param_et = [{
'clf__criterion': ['gini', 'entropy'],
'clf__max_depth': param_range,
'clf__min_samples_split': param_range[1:],
'clf__min_samples_leaf': param_range
}]

A função recebe como estimador o pipeline criado anteriormente, juntamente com o dicionário de parâmetros, e realiza a busca em grade visando maximizar a métrica recall em 10 dobras de validação cruzada.

def make_grid(pipeline, params):

'''
Função que recebe o classificador e implementa o objeto de busca em grade
com validação cruzada utilizando a função GridSearchCV, otimizada
para a métrica recall.

clf: classificador
params: dicionário de hiperparâmetros
'''

return GridSearchCV(
estimator = pipeline,
param_grid = params,
scoring = 'recall',
cv = 10,
n_jobs = -1
)

4.3 Encontrar a melhor configuração para a métrica recall

Com todas as etapas de preparação feitas, a função abaixo realiza o ajuste dos dados a partir do objeto de busca em grade, otimizando para a métrica de recall. Ela então imprime o conjunto de hiperparâmetros que resulta no melhor desempenho, juntamente com a pontuação no conjunto de treino.

A partir disso, a função ajusta o modelo nos dados de teste, imprime as métricas no relatório de classificação e, por fim, cria a matriz de confusão.

def find_best_recall(grid_search):

'''
Realiza o Grid Search com os dados balanceados e retorna a
melhor configuração de hiperparâmetros para ajustar o modelo
com base na métrica de recall.

Passos:
1. Ajusta os dados de treino para realizar o GridSearch
2. Imprime os melhores hiperparâmetros
3. Imprime o melhor recall para os dados de treino
4. Realiza previsões nos dados de teste
5. Imprime o melhor recall nos dados de teste
6. Imprime o relatório de classificação
7. Imprime o AUC
8. Plota a matriz de confusão

grid_search: Objeto de GridSearchCV para o classificador
'''

# Ajuste dos dados de treino balanceados para GridSearch
grid_search.fit(X_train, y_train)

# Imprimir os melhores hiperparâmetros
print(f'Melhores parâmetros: {grid_search.best_params_}')

# Imprime o melhor recall de treino
best_recall = grid_search.best_score_
print(f'\nMelhor recall de treino: {best_recall:.3f}')

# Previsões nos dados de teste
y_pred = grid_search.predict(X_test)

# Melhor recall para os dados de teste
best_recall = recall_score(y_test, y_pred)
print(f'\nMelhor recall de teste: {best_recall:.3f}')

#I Imprimir o relatório de classificação
print('\nRelatório de classificação',
classification_report(y_test, y_pred),
f'\nAUC: \t{roc_auc_score(y_test, y_pred):.3f}')

# Plotar matriz de confusão
plot_confusion_matrix(y_pred)

Esse procedimento todo é repetido para os 4 modelos testados no projeto. As Figura 4.1 e 4.2 mostram as matrizes de confusão para cada classificador testado. Nelas, note os valores de recall para cada modelo.

Figura 4.1 — Matrizes de confusão (a) de regressão logistica e (b) random forest.

Observando a matriz de confusão para o modelo de Regressão Logística, note que o recall foi de 0.95, com uma taxa baixa de falsos negativos de 5%. Um ótimo resultado, considerando um algoritmo relativamente simples. Contudo, a precisão do modelo foi muito baixa, de 0.49, o que significa que o custo para minimizar o erro do tipo II é ter um modelo que não performa bem para a classe negativa, tampouco consegue distinguir satisfatoriamente entre as classes.

Já para o segundo modelo de Random Forest, vemos um resultado de recall um pouco pior do que o de regressão logística, igual a 0.92 e uma taxa de falsos negativos de 0.08. O custo de maximizar a métrica recall foi uma precisão de 0.82, o que é significativamente melhor do que a precisão do primeiro modelo.

De fato, se notarmos a métrica de AUC, o modelo de Random Forest obteve 0.93, apontando o fato de conseguir distinguir melhor entre as classes.

Entretanto, estamos lidando com as vidas de mães e bebês que dependem da identificação da classe positiva pelo modelo. O custo para a taxa de falsos negativos é muito alta, pois põe em risco as duas vidas, de modo que não estamos nem um pouco interessados em equilibrar todas as métricas, mas sim em maximizar a sensibilidade, ou recall.

Figura 4.2 — Matrizes de confusão para os modelos de (a) SVM e (b) Extra Trees Classifier.

Seguindo para os últimos dois modelos, o SVM otimizado atingiu um recall de 0.94 e uma taxa de falsos negativos de aproximadamente 6%. A precisão neste caso foi de 0.69, que é uma troca um pouco maior do que aquela observada no modelo de floresta aleatória. A AUC atingiu o valor de 0.91, o que mostra que o modelo é muito bom em distinguir entre as classes.

Por fim, o modelo de Extra Trees foi o que melhor performou no pipeline construído, atingindo um recall de 96% com uma precisão de 0.70. O AUC desse classificador foi de 0.92, o que é um ótimo resultado. Não obtivemos uma taxa de erros falsos positivos muito alta, havendo muitos acertos na classe negativa enquanto maximizamos os acertos também na positiva, que era o nosso objetivo.

O algoritmo do Extra Trees é similar ao da floresta aleatória, no sentido de que é criado um conjunto de árvores de decisão cujas variáveis e dobras dos dados são escolhidos aleatoriamente. A diferença entre os dois, contudo, é que adiciona-se mais um fator de aleatoriedade no Extra Trees ao lidar com os limiares (thresholds) para split dos dados. Esse limiar define se determinado exemplo fará parte de uma classe ou de outra.

Enquanto no algoritmo de floresta aleatória temos um cálculo para encontrar o melhor limiar, a estratégia do Extra Trees consiste em testar diversos limiares diferentes aleatoriamente, criando diversas árvores fracas e enviesadas para, ao final, somar as contribuições de cada uma e retornar um modelo mais generalizável.

4.4 Desempenho do Extra Trees a partir da curva de precisão-recall

As curvas de precisão-recall fornecem uma relação entre o poder preditivo do modelo para a classe positiva (precisão) e a proporção de positivos que o modelo previu corretamente (recall). A Figura 4.3 mostra a curva de precisão-recall para o modelo de Extra Trees.

Figura 4.3 — Curva de precisão-recall para o modelo de Extra Trees.

O classificador sem habilidade (no skill) é aquele que não consegue discriminar entre as classes, de modo que só seria capaz de prever aleatoriamente, caso o problema fosse balanceado, ou de forma constante, fornecendo apenas a proporção da classe positiva no conjunto de teste. Portanto, a linha do classificador no skill muda de acordo com a distribuição da classe positiva, que no nosso caso é de 22%.

Analisando a curva para o modelo de Extra Trees, vemos claramente a troca da precisão pelo recall, de modo que obtemos uma precisão menor que 70% para um recall acima de 95%.

Como estamos buscando minimizar a taxa de falsos negativos, de modo a não cometer erros que custem a vida da mãe e do bebê, essa troca se faz necessária e a curva mostra que o modelo está se comportando bem na sua capacidade preditiva.

4.5 Resumo das performances

A Tabela 4.1 abaixo resume todas as performances dos modelos discutidos na seção anterior.

Tabela 4.1 — Resumo das performances dos classificadores.

5. Abordagem com Auto Machine Learning

Depois de ter construído 4 modelos manualmente desde a análise exploratória até o pré-processamento e ajuste dos classificadores, vamos usar agora uma abordagem de aprendizado automatizado com o PyCaret.

O PyCaret permite configurar o treinamento de modelos, realizando todo o processo de análise e pré-processamento dos dados. Com os diversos algoritmos contidos na biblioteca, é possível comparar os modelos de base e otimizar os melhores com relação a algum critério.

5.1 Configuração do ambiente

Neste caso, iremos buscar por classificadores que maximizem a métrica recall, como feito anteriormente. Inicialmente, vamos utilizar o método setup para configurar o ambiente. A partir do Dataframe original, escolhemos uma amostra de 30% dos dados para servirem como um conjunto de teste. O restante será usado como conjunto de treino e validação na construção dos modelos automatizados.

test_df = df_new.sample(frac = .3, random_state = 42)
train_df = df_new.drop(test_df.index)
clf = setup(
data = train_df,
target = 'fh_class',
train_size = .7,
fix_imbalance = True,
fix_imbalance_method = 'ADASYN',
normalize = True,
normalize_method = 'robust',
session_id = 42,
experiment_name = 'FHC_09_2023'
)

Note que, na configuração, selecionamos a mesma proporção para os conjuntos de treino e validação, i.e 70/30, respectivamente. Também foi necessário realizar o balanceamento dos dados e, mediante testes com diversos métodos de under e oversampling, o ADASYN foi o que melhor performou para maximizar o recall.

Além disso, configuramos a normalização dos dados com o método robusto, que lida melhor com outliers. Isso se faz necessário pois muitas variáveis se encontram em diferentes escalas, e também porque a maioria é fortemente enviesada para valores pequenos.

Com o ambiente configurado, seguimos para a comparação de modelos de base, com a função compare_models, que retornou o melhor algoritmo para a métrica recall, que no caso foi o LDA (Linear Discriminant Analysis).

Tabela 5.1 — Os 5 melhores modelos de base para classificação de saúde fetal, segundo o PyCaret.

O LDA é uma técnica que combina redução de dimensionalidade com classificação ao encontrar o subespaço linear que maximiza a separação de classes. Nesse sentido, ele busca uma combinação linear de atributos que conseguem fazer essa divisão da melhor maneira possível.

O algoritmo, entretanto, assume que as variáveis seguem uma distribuição normal e necessita que as classes estejam balanceadas para um melhor desempenho. Além disso, por ser um algoritmo que trata combinações lineares, ele pode falhar em identificar relações não-lineares complexas nos dados.

No caso dos dados de treino em questão, eles não seguem distribuições normais, mas estão balanceados. Alguma perda de performance deve haver, porém vamos testar o modelo otimizado, uma vez que o básico obteve um resultado muito bom para o recall.

5.2 Criação e otimização do modelo

Utilizando as funções create_model e tune_model, iremos ajustar os dados de treino e utilizar da validação cruzada para ter uma média da performance do modelo.

Os resultados em 10 dobras foram de 0.952 para o recall do modelo base e 0.957 para o otimizado. A precisão foi de 0.625 para o primeiro e 0.631 para o segundo, ou seja, uma melhora discreta nas duas métricas.

5.3 Previsão no conjunto de validação

Com o modelo otimizado criado, seguimos para as previsões no conjunto de validação utilizando a função predict_model. Os resultados são dispostos na Tabela 5.2 abaixo:

Tabela 5.2 — Resultados da previsão do modelo otimizado no conjunto de validação.

Obtivemos um recall bem alto, de aproximadamente 97%, e uma precisão de 60%, o que não é um mal resultado. Basta agora testarmos o modelo finalizado no conjunto de treino com dados nunca vistos pelo algoritmo para termos ideia da capacidade de generalização do modelo.

A matriz de confusão no conjunto de validação é mostrada na Figura 5.1. Note que de 99 casos graves, o algoritmo previu 96 corretamente.

Figura 5.1 — Matriz de confusão do modelo de LDA para o conjunto de validação.

A Figura 5.2 mostra a curva de precisão-recall para o LDA, evidenciando a troca de precisão pelo aumento do recall. O comportamento é similar ao observado para o modelo de Extra Trees, porém a queda na precisão é maior no LDA.

Figura 5.2 — Curva de precisão-recall para o LDA.

5.4 Finalização do modelo

Para finalizar o modelo, iremos utilizar a função finalize_model. Em seguida, para testá-lo em dados nunca antes vistos pelo modelo, iremos aplicar o método predict_model sobre os dados do conjunto de testes reservados no início da seção. Assim, teremos uma noção do comportamento do classificador mediante novos exemplos, notando a variação de performance com relação ao obtido durante o treino.

final_lda = finalize_model(tuned_lda)

'''
Previsão no conjunto test_df separado no início da seção
'''
unseen_pred = predict_model(final_lda, data = test_df, round = 3)

A Tabela 5.3 mostra os resultados do desempenho do modelo de LDA no conjunto de teste.

Note que obtivemos uma performance um pouco pior para o conjunto de teste em exemplos novos. O recall caiu de 97% para ficar em torno de 94%. Da mesma forma ocorreu para as outras métricas, como acurácia, AUC e precisão, principalmente. Este tipo de variação na performance não é um sinal ruim, mas mostra que o modelo conseguiu generalizar e ter capacidade de classificar exemplos de dados diversos num conjunto menor.

Tabela 5.3 — Desempenho do modelo LDA nos dados inéditos de treino.

O desempenho nos dados de teste é mais realista pois trata de dados novos, que refletem o desbalanço original entre as classes e não passou pelas etapas de otimização, que podem fazer o modelo superestimar resultados durante o treino. Contudo, o custo de precisão foi maior para o modelo gerado com AutoML se comparado ao modelo de Extra Trees, sem ainda atingir um recall tão alto quanto deste último.

Conclusão

Neste projeto, investigamos como o exame de CTG pode ter seus dados utilizados para a classificação da saúde fetal, buscando salvaguardar a vida da mãe e do bebê através de uma abordagem com machine learning.

De fato, foi observado que o CTG fornece as informações necessárias para realizar as previsões sobre a saúde do bebê, destacando atributos informativos como variações anormais de curto e longo prazo, acelerações, desacelerações prolongadas, contrações uterinas e movimentos fetais. Durante a análise exploratória e visualização dos dados, pôde-se observar como essas variáveis se correlacionam entre si, separando bem os casos normais dos graves.

Por exemplo, vimos que contrações uterinas e movimentos fetais podem indicar risco do bebê caso ele esteja imóvel por algum tempo e com variações anormais nos batimentos cardíacos. Por outro lado, bebês que estejam muito agitados tem também maiores chances de estarem correndo risco.

Além desses fatores, as desacelerações prolongadas provaram ser muito informativas quanto ao risco, pois a maioria dos casos com desacelerações ligeiramente maiores do que zero já indicavam um caso grave. Todas essas informações puderam ser inferidas a partir da análise exploratória dos dados do exame de CTG.

Para a construção dos modelos de machine learning, duas abordagens foram utilizadas.

A primeira consistiu na construção de um pipeline para pré-processamento e ajuste dos modelos, com a utilização de técnicas como a busca em grade com validação cruzada (GridSearchCV) e análise de desempenho através da curva de precisão-recall. Foram testados os modelos de regressão logística, random forest, SVM e extra trees classifier, buscando minimizar a taxa de falsos negativos. O melhor desempenho, com maior recall, foi do modelo de Extra Trees, que alcançou uma taxa de falsos negativos de aproximadamente 3% em dados nunca antes vistos.

A segunda abordagem consistiu na aplicação de machine learning automatizado através da biblioteca PyCaret. Neste caso, buscou-se configurar o pipeline automatizado para o balanceamento e normalização dos dados, resultando na construção de diversos classificadores e posterior comparação entre eles. Foi escolhido aquele que obteve o maior recall, que no caso foi o algoritmo de Análise de Discriminante Linear, ou Linear Discriminant Analysis (LDA). Na validação, o modelo otimizado atingiu 97% de recall, enquanto que nos testes de desempenho com dados nunca antes vistos, obteve um recall de aproximadamente 94%.

Como houve um custo maior na precisão para o modelo construído via AutoML — não atingindo ainda um recall tão alto quanto o obtido na primeira abordagem — escolheu-se o modelo de Extra Trees como sendo o melhor na classificação de saúde fetal.

A tabela abaixo mostra a relação das métricas entre os modelos construídos:

O projeto como um todo ajudou a clarificar pontos importantes de atenção para o diagnóstico da saúde fetal que podem ser usados por médicos obstetras e neonatologistas. Alguns atributos do exame de CTG foram destacados como sendo informativos para a previsão de casos graves, como aceleração e movimentos do bebê, contrações uterinas, variações anormais de curto e longo prazo e desacelerações severas.

Além disso, a recomendação é que o exame de CTG seja feito o mais frequente possível durante o período de gestação, principalmente por sua simplicidade e facilidade de interpretação. Os exames de CTG são capazes de prever a saúde fetal e reduzir a mortalidade tanto materna quanto do bebê, tornando-se uma opção de custo efetivo baixo para evitar fatalidades.

Da parte de inteligência de dados, modelos de classificação como o Extra Trees fornecem uma boa capacidade preditiva e um nível de personalização que permite identificar casos suspeitos ou patológicos com o mesmo nível de precisão, para que o tratamento comece o mais rápido possível. É recomendada uma abordagem que diminua o limiar para a classe positiva, visando que, ao menor sinal de suspeita de risco, os pacientes sejam checados.

Referências

  1. Ayres-de Campos, D.; Bernardes, J.; Garrido, A.; Marques-de Sa, J.; Pereira-Leite, L. SisPorto 2.0: A program for automated analysis of cardiotocograms. J. Matern.-Fetal Med. 2000, 9, 311–318.
  2. Elizabeth, C.W.G et al. Fetal Mortality in the United States: Final 2019–2020 and 2020–Provisional 2021. (2021)
  3. Elizabeth, C.W.G et al. Fetal Mortality: United States, 2020. (2020)
  4. Tenorio, D. S. et al. High maternal mortality rates in Brazil: Inequalities and the struggle for justice. (2021)

Obrigado pela leitura!

Me acompanhe nas redes sociais e confira outros projetos no meu portfólio do GitHub, ou através dos artigos aqui no Medium.

--

--