Sigmoidal
  • Home
  • Pós-Graduação
  • Blog
  • Sobre Mim
  • Contato
Sem Resultado
Ver Todos Resultados
  • English
  • Home
  • Pós-Graduação
  • Blog
  • Sobre Mim
  • Contato
Sem Resultado
Ver Todos Resultados
Sigmoidal
Sem Resultado
Ver Todos Resultados

Análise de ECG com Python: do sinal ao diagnóstico automático

Carlos Melo por Carlos Melo
julho 13, 2026
em Data Science, Python, Sinais Biomédicos
0
13
VIEWS
Publicar no LinkedInCompartilhar no FacebookCompartilhar no Whatsapp

Como um algoritmo aprende a ler um eletrocardiograma e distinguir um coração acelerado de um coração em repouso, batimento a batimento, sem que nenhum cardiologista toque no traçado?

O eletrocardiograma é um dos exames mais difundidos da medicina, e também um dos mais ricos para quem trabalha com dados. Por trás daquele traçado que sobe e desce está um sinal com estrutura precisa, o mesmo tipo de objeto que a ciência de dados manipula todos os dias: uma série temporal ruidosa da qual precisamos extrair padrões confiáveis. Neste artigo você vai percorrer a cadeia completa de análise de ECG com Python, da simulação do sinal até um classificador que separa três padrões de ritmo cardíaco.

Para reproduzir cada etapa com os mesmos números apresentados aqui, o notebook executado acompanha o artigo.

💻

Código do Artigo

Acesse o código-fonte deste artigo gratuitamente.

Informe seu email para acessar o código:

✓ Seu código está pronto!

Abrir no Google Colab →

O que o traçado de um ECG realmente registra?

O ECG mede a atividade elétrica do coração ao longo do tempo. Cada batimento imprime no papel, ou na tela, uma sequência característica de deflexões. Primeiro vem a onda P, associada à despolarização dos átrios. Em seguida aparece o complexo QRS, o pico estreito e mais alto do traçado, que corresponde à despolarização dos ventrículos. Por fim, a onda T marca a repolarização ventricular.

Reconhecer essa assinatura é o ponto de partida de qualquer leitura automática. Para trabalhar sem depender de uma base clínica, usamos o pacote neurokit2, que simula sinais fisiológicos realistas. Começamos com um traçado ideal de 10 segundos, amostrado a 250 Hz, com frequência média de 70 batimentos por minuto.

import numpy as np
import matplotlib.pyplot as plt
import neurokit2 as nk

SEED = 42
np.random.seed(SEED)

sampling_rate = 250
ecg_limpo = nk.ecg_simulate(
    duration=10,
    sampling_rate=sampling_rate,
    heart_rate=70,
    random_state=SEED,
)

O recorte de dois segundos abaixo deixa visível o padrão que se repete a cada batimento: a pequena elevação inicial da onda P, o pico agudo do complexo QRS e a ondulação seguinte da onda T. É essa forma que os algoritmos aprendem a localizar.

Forma de onda de um ECG simulado com ondas P, QRS e T
ECG simulado a 70 bpm: a assinatura P-QRS-T se repete a cada batimento.

Por que o sinal real nunca chega limpo?

Um traçado adquirido na prática carrega interferências que o sinal ideal não tem. Duas são especialmente comuns. A flutuação da linha de base (_baseline wander_) é uma oscilação lenta causada pela respiração e pelo movimento dos eletrodos sobre a pele. O ruído de rede elétrica, em torno de 50 a 60 Hz, aparece como uma textura fina sobreposta a todo o sinal. Somamos as duas componentes, além de um ruído branco de baixa amplitude, ao traçado ideal.

tempo = np.arange(len(ecg_limpo)) / sampling_rate

baseline_wander = 0.4 * np.sin(2 * np.pi * 0.3 * tempo)   # ~0,3 Hz
ruido_rede = 0.15 * np.sin(2 * np.pi * 60 * tempo)         # 60 Hz
ruido_branco = np.random.normal(0, 0.05, size=len(ecg_limpo))

ecg_ruidoso = ecg_limpo + baseline_wander + ruido_rede + ruido_branco

O resultado é revelador. A flutuação da linha de base desloca todo o traçado para cima e para baixo, enquanto o ruído de rede adiciona a textura de alta frequência sobre as ondas. Sem tratamento, essas distorções comprometem tanto a inspeção visual quanto qualquer detecção automática de batimentos.

Comparação entre ECG limpo e ECG com ruído e flutuação de linha de base
O mesmo sinal, antes e depois de receber flutuação de linha de base e ruído de rede.

Análise de ECG com Python: como filtrar o sinal?

A limpeza do ECG é uma etapa de processamento de sinais anterior a qualquer medida. A função nk.ecg_clean aplica uma cadeia de filtros que remove a componente lenta da linha de base e atenua as altas frequências, preservando a morfologia das ondas.

ecg_filtrado = nk.ecg_clean(ecg_ruidoso, sampling_rate=sampling_rate)

O traçado filtrado volta a oscilar em torno de uma linha de base estável e recupera os complexos QRS com nitidez. É sobre esse sinal, e não sobre o bruto, que executamos todas as etapas seguintes.

ECG antes e depois da filtragem com nk.ecg_clean
Antes e depois da filtragem: a linha de base estabiliza e os batimentos ficam nítidos.

Onde estão os batimentos?

O ponto de referência de cada batimento é o pico R, o vértice do complexo QRS. Localizá-lo é o passo que sustenta praticamente todas as medidas clínicas derivadas do ECG. A função nk.ecg_peaks implementa um detector herdeiro da abordagem clássica de análise do QRS proposta por Pan e Tompkins, amplamente adotada na literatura de processamento de sinais biomédicos.

sinais, info = nk.ecg_peaks(ecg_filtrado, sampling_rate=sampling_rate)
picos_r = info["ECG_R_Peaks"]

print(f"Número de picos R detectados: {len(picos_r)}")
# Número de picos R detectados: 11

No nosso registro de 10 segundos a 70 bpm, o detector encontra 11 picos R, coerente com o ritmo esperado. Cada marcador na figura corresponde a um batimento identificado.

ECG filtrado com os picos R marcados sobre cada complexo QRS
Os 11 picos R detectados, um por batimento, sobre o sinal filtrado.

Da distância entre picos à frequência cardíaca

A contagem de picos já dá uma estimativa grosseira do ritmo, mas o cálculo formal vem dos intervalos entre picos consecutivos. O intervalo RR é o tempo entre dois picos R sucessivos, e a frequência cardíaca instantânea, em batimentos por minuto, é o inverso desse intervalo multiplicado por 60:

    \[ \text{HR} = \frac{60}{\text{RR}} \]

onde \text{RR} é medido em segundos. Convertendo as posições dos picos de amostras para segundos, obtemos a série de intervalos RR e, a partir dela, a frequência instantânea.

rr_segundos = np.diff(picos_r) / sampling_rate
hr_instantanea = 60 / rr_segundos

print(f"Intervalo RR médio: {rr_segundos.mean():.3f} s")
print(f"Frequência cardíaca média: {hr_instantanea.mean():.1f} bpm")
# Intervalo RR médio: 0.855 s
# Frequência cardíaca média: 70.2 bpm

O intervalo RR médio de 0,855 s corresponde a uma frequência cardíaca média de 70,2 bpm, praticamente idêntica ao valor que pedimos na simulação. A frequência oscila levemente ao redor dessa média, refletindo a variação natural entre batimentos.

Frequência cardíaca instantânea calculada a partir dos intervalos RR
Frequência cardíaca instantânea ao longo dos 10 segundos, com a média em destaque.

Quanto o ritmo varia, e por que isso importa?

Aquela pequena flutuação da frequência não é imperfeição da medida: ela carrega informação fisiológica. A variabilidade da frequência cardíaca (_Heart Rate Variability_, HRV) quantifica como os intervalos RR mudam ao longo do tempo. Dois índices no domínio do tempo são de uso corrente. O SDNN é o desvio-padrão de todos os intervalos RR normais, uma medida global da variabilidade. O RMSSD é a raiz quadrada da média dos quadrados das diferenças entre intervalos sucessivos, mais sensível às variações rápidas de curto prazo.

hrv = nk.hrv_time(picos_r, sampling_rate=sampling_rate, show=False)

print(f"SDNN:  {hrv['HRV_SDNN'].iloc[0]:.2f} ms")
print(f"RMSSD: {hrv['HRV_RMSSD'].iloc[0]:.2f} ms")
# SDNN:  14.70 ms
# RMSSD: 17.54 ms

Para este registro, o SDNN é de 14,70 ms e o RMSSD, de 17,54 ms. O gráfico de Poincaré, que dispõe cada intervalo RR contra o seguinte, traduz visualmente essa dispersão: pontos próximos à diagonal indicam intervalos sucessivos parecidos entre si, ou seja, um ritmo estável.

Gráfico de Poincaré dos intervalos RR para leitura visual da HRV
Gráfico de Poincaré: cada intervalo RR contra o seguinte. A dispersão em torno da diagonal resume a variabilidade.

É possível classificar padrões de ritmo automaticamente?

As medidas anteriores descrevem um único registro. O passo natural em ciência de dados é usá-las como atributos para um modelo capaz de distinguir padrões de ritmo. Simulamos 30 registros de 10 segundos em cada uma de três classes fisiológicas: bradicardia em torno de 48 bpm, ritmo normal em torno de 72 bpm e taquicardia em torno de 110 bpm. Cada registro recebe uma variação aleatória de frequência para que as classes não sejam triviais.

De cada registro extraímos seis atributos: frequência cardíaca média, SDNN, RMSSD e os intervalos RR médio, mínimo e máximo. O resultado é uma matriz de 90 registros por 6 atributos.

def extrair_atributos(ecg, sampling_rate):
    limpo = nk.ecg_clean(ecg, sampling_rate=sampling_rate)
    _, info = nk.ecg_peaks(limpo, sampling_rate=sampling_rate)
    picos = info["ECG_R_Peaks"]

    rr = np.diff(picos) / sampling_rate
    hr = 60 / rr
    rr_ms = rr * 1000

    return {
        "hr_media": hr.mean(),
        "sdnn": np.std(rr_ms, ddof=1),
        "rmssd": np.sqrt(np.mean(np.diff(rr_ms) ** 2)),
        "rr_medio": rr.mean(),
        "rr_min": rr.min(),
        "rr_max": rr.max(),
    }

Com a matriz montada, separamos os dados em treino e teste de forma estratificada e treinamos um Random Forest. Esse modelo lida bem com poucos atributos numéricos e não exige normalização, o que o torna um ponto de partida sólido.

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

X_treino, X_teste, y_treino, y_teste = train_test_split(
    X, y, test_size=0.3, random_state=SEED, stratify=y
)

modelo = RandomForestClassifier(n_estimators=200, random_state=SEED)
modelo.fit(X_treino, y_treino)
y_pred = modelo.predict(X_teste)

print(classification_report(y_teste, y_pred))

No conjunto de teste, com 27 registros, o modelo alcança acurácia de 1,00, acertando todas as três classes com precisão e revocação plenas. A matriz de confusão confirma o resultado: nenhum registro é atribuído à classe errada.

Matriz de confusão da classificação de ritmo cardíaco com Random Forest
Matriz de confusão: os 27 registros de teste são classificados corretamente entre as três classes.

Vale um cuidado de interpretação. A separação perfeita reflete o desenho controlado do experimento: bradicardia, ritmo normal e taquicardia ocupam faixas de frequência bem distintas, e os atributos de frequência e variabilidade traçam uma fronteira de decisão nítida entre elas. Em sinais clínicos reais, com arritmias, artefatos de movimento e sobreposição de padrões, o problema é substancialmente mais difícil, e é justamente aí que entram bases anotadas como a MIT-BIH Arrhythmia Database e modelos mais expressivos. O pipeline, porém, permanece o mesmo: filtrar, detectar, medir e classificar.

Takeaways

  • O ECG é um sinal estruturado: as ondas P, QRS e T se repetem a cada batimento e servem de âncora para toda medida derivada.
  • Filtrar vem antes de medir: nk.ecg_clean remove a flutuação de linha de base e o ruído de rede, condição para uma detecção de picos confiável.
  • O pico R é o ponto de partida: localizado o complexo QRS, os intervalos RR levam diretamente à frequência cardíaca pela relação \text{HR} = 60 / \text{RR}. No sinal de exemplo, os intervalos entre os 11 picos R detectados em 10 segundos resultam em 70,2 bpm.
  • A variabilidade carrega informação: SDNN e RMSSD resumem, respectivamente, a flutuação global e a de curto prazo dos intervalos RR, aqui 14,70 ms e 17,54 ms.
  • Atributos simples sustentam classificação: seis medidas de frequência e variabilidade bastaram para um Random Forest separar bradicardia, ritmo normal e taquicardia com acurácia de 1,00 em um cenário controlado.
  • O salto para o clínico é de dados, não de método: a mesma cadeia de filtrar, detectar, medir e classificar escala para bases reais anotadas, onde mora a real dificuldade do problema.
CompartilharCompartilharEnviar
Post Anterior

Imputação de Dados Ausentes com Scikit-Learn

Carlos Melo

Carlos Melo

Engenheiro de Visão Computacional graduado em Ciências Aeronáuticas pela Academia da Força Aérea (AFA) e Mestre em Engenharia Aeroespacial pelo Instituto Tecnológico de Aeronáutica (ITA).

Relacionado Artigos

Data Science

Imputação de Dados Ausentes com Scikit-Learn

por Carlos Melo
julho 9, 2026
Data Science

Binary Cross-Entropy e Regressão Logística do Zero

por Carlos Melo
junho 1, 2026
Gradient Descent e Loss Function: a Origem do MSE
Data Science

Gradient Descent e Loss Function: a Origem do MSE

por Carlos Melo
maio 27, 2026
Regressão Linear e Aprendizado Supervisionado
Data Science

Regressão Linear e Aprendizado Supervisionado

por Carlos Melo
maio 26, 2026
Introdução ao Pandas: capa do guia prático para análise de dados com Python
Data Science

Introdução ao Pandas: Guia Prático para Análise de Dados

por Carlos Melo
abril 27, 2026

Deixe um comentário Cancelar resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Pós em Visão Computacional Pós em Visão Computacional Pós em Visão Computacional

Mais Populares

  • Cientista de Dados: Salário em 2026 (Júnior, Pleno e Sênior)

    92 compartilhamentos
    Compartilhar 37 Tweet 23
  • Geometria da Formação de Imagens: Matrizes, Transformações e Sistemas de Coordenadas

    439 compartilhamentos
    Compartilhar 176 Tweet 110
  • Introdução ao MediaPipe e Pose Estimation

    571 compartilhamentos
    Compartilhar 228 Tweet 143
  • 6 Livros de Visão Computacional e Deep Learning

    26 compartilhamentos
    Compartilhar 10 Tweet 7
  • ORB-SLAM 3: Tutorial Completo para Mapeamento 3D e Localização em Tempo Real

    480 compartilhamentos
    Compartilhar 192 Tweet 120
  • Em Alta
  • Comentários
  • Mais Recente
Como Tratar Dados Ausentes com Pandas

Como Tratar Dados Ausentes com Pandas

agosto 13, 2019
Introdução ao MediaPipe e Pose Estimation

Introdução ao MediaPipe e Pose Estimation

julho 15, 2023
Como usar o DALL-E 2 para gerar imagens a partir de textos

Como usar o DALL-E 2 para gerar imagens a partir de textos

dezembro 25, 2022

ORB-SLAM 3: Tutorial Completo para Mapeamento 3D e Localização em Tempo Real

abril 10, 2023
Como Analisar Ações da Bolsa com Python

Como Analisar Ações da Bolsa com Python

15
Setembro Amarelo: Análise do Suicídio no Brasil, com Data Science

Setembro Amarelo: Análise do Suicídio no Brasil, com Data Science

13
Como Aprender Data Science?

Como Aprender Data Science?

9
Qual o Cenário de Data Science no Brasil hoje?

Qual o Cenário de Data Science no Brasil hoje?

8

Análise de ECG com Python: do sinal ao diagnóstico automático

julho 13, 2026

Imputação de Dados Ausentes com Scikit-Learn

julho 9, 2026

Pipelines no Scikit-Learn: Transformações sem Data Leakage

julho 6, 2026

Binary Cross-Entropy e Regressão Logística do Zero

junho 1, 2026
Instagram Youtube LinkedIn Twitter
Sigmoidal

O melhor conteúdo técnico de Data Science, com projetos práticos e exemplos do mundo real.

Seguir no Instagram

Categorias

  • Aeroespacial
  • Artigos
  • Blog
  • Carreira
  • Cursos
  • Data Science
  • Deep Learning
  • Destaques
  • Entrevistas
  • IA Generativa
  • Livros
  • Machine Learning
  • Notícias
  • Python
  • Sinais Biomédicos
  • Teoria
  • Tutoriais
  • Visão Computacional
  • Youtube

Navegar por Tags

algebra-linear camera calibration carreira chatgpt cientista de dados cnn computer vision Cursos dados desbalanceados data science data science na prática decision tree deep learning deploy gpt-3 gradient descent IA generativa inteligência artificial jupyter kaggle keras livros machine learning matplotlib nft openai opencv pandas profissão python pytorch reconstrução 3d redes neurais redes neurais convolucionais regressão linear regressão logística salário scikit-learn sklearn tensorflow tutorial visão computacional vídeo youtube árvore de decisão

© 2024 Sigmoidal - Aprenda Data Science, Visão Computacional e Python na prática.

Welcome Back!

Login to your account below

Forgotten Password?

Retrieve your password

Please enter your username or email address to reset your password.

Log In

Add New Playlist

Sem Resultado
Ver Todos Resultados
  • Home
  • Pós-Graduação
  • Blog
  • Sobre Mim
  • Contato
  • English

© 2024 Sigmoidal - Aprenda Data Science, Visão Computacional e Python na prática.