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.
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.
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.
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.
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:
![]()
onde
é 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.
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.
É 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.
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_cleanremove 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
. 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.

















