Filtro de Kalman em Python: algoritmo de predição e suavização de dados ruidosos com representação geométrica

Filtro de Kalman em Python Puro: O Algoritmo Que Prevê o Futuro Combinando Predições com Medições Ruidosas (Sem numpy, Sem scipy, Sem Dependências)

Você já olhou para uma planilha de métricas pessoais — produtividade, humor, horas de sono — e sentiu que os dados estavam gritando algo, mas o ruído era tanto que você desistiu de interpretar? Eu já. Várias vezes. A média móvel ajuda, mas ela é burra: trata o dado de ontem e o de 30 dias atrás com o mesmo peso. E se existisse um algoritmo que soubesse exatamente quanto confiar na sua medição e quanto confiar na sua predição? Esse algoritmo existe há 60 anos, foi usado para levar o homem à Lua, e hoje eu vou implementá-lo do zero em Python puro.

Estamos falando do Filtro de Kalman — o estimador ótimo que combina predições teóricas com medições ruidosas para encontrar o estado verdadeiro de um sistema. E o melhor: sem numpy, sem scipy, sem biblioteca nenhuma. Só Python na unha.

O Que É o Filtro de Kalman (e Por Que Você Deveria Se Importar)

O Filtro de Kalman é um algoritmo recursivo que estima o estado interno de um sistema dinâmico a partir de uma série de medições imprecisas. Ele foi desenvolvido por Rudolf Kálmán em 1960 e foi fundamental para o programa Apollo da NASA. Hoje está dentro do seu celular (sensor de rotação), do GPS do carro, de drones autônomos e de qualquer sistema que precisa tomar decisões com dados imperfeitos.

A genialidade do Kalman está na Ganho de Kalman — um número entre 0 e 1 que decide, a cada passo, se o algoritmo deve confiar mais na predição ou na medição. Se o sensor é barulhento, o ganho diminui e o filtro confia mais no modelo. Se o modelo é impreciso, o ganho aumenta e o filtro abraça a medição.

Analogia Para Fixar

Imagine que você está medindo sua produtividade diária numa escala de 0 a 10. Na segunda-feira você anota 7. Mas sabe que seu humor influencia a nota — ou seja, a medição tem ruído. O Filtro de Kalman faz duas coisas simultaneamente:

  1. Prediz qual deveria ser sua produtividade amanhã baseado no histórico (modelo).
  2. Corrige essa predição quando a medição real chega, ponderando a confiança em cada fonte.

O resultado? Uma curva suave que revela a tendência real, livre de ruído emocional.

Lupa sobre documentos com análise de dados — o Filtro de Kalman inspeciona cada medição e decide quanto confiar nela
Lupa sobre dados: o Filtro de Kalman inspeciona cada medição e decide quanto confiar nela. (Foto: Hanna Pad / Pexels)

A Matemática Por Trás (Simplificada Para Quem Não É Físico)

O algoritmo tem duas fases que se repetem indefinidamente:

Fase 1: Predição (Time Update)

x_pred = F * x_est + B * u
P_pred = F * P_est * F_T + Q

Onde:

  • x_est = estimativa anterior do estado
  • F = matriz de transição (como o estado evolui)
  • P_est = incerteza anterior
  • Q = ruído do processo (o quanto o modelo é imperfeito)

Fase 2: Correção (Measurement Update)

K = P_pred * H_T / (H * P_pred * H_T + R)
x_est = x_pred + K * (z - H * x_pred)
P_est = (1 - K * H) * P_pred

Onde:

  • K = Ganho de Kalman (o peso entre predição e medição)
  • z = medição real
  • H = matriz de observação (como o estado se traduz em medição)
  • R = ruído do sensor (o quanto a medição é imprecisa)

Implementação Completa em Python Puro

Chega de teoria. Vamos ao código. Esta implementação funciona para sistemas 1D (uma variável de estado) sem nenhuma dependência externa:

class KalmanFilter1D:
    """Filtro de Kalman unidimensional em Python puro."""
    
    def __init__(self, x0=0.0, P0=1.0, Q=0.1, R=1.0, F=1.0, H=1.0):
        """
        Args:
            x0: estimativa inicial do estado
            P0: incerteza inicial
            Q:  ruído do processo (confiança no modelo)
            R:  ruído do sensor (confiança na medição)
            F:  fator de transição (como o estado evolui)
            H:  fator de observação (como o estado vira medição)
        """
        self.x = x0    # estado estimado
        self.P = P0    # incerteza
        self.Q = Q     # ruído do processo
        self.R = R     # ruído do sensor
        self.F = F     # transição
        self.H = H     # observação
        self.history = []
    
    def predict(self, u=0.0, B=0.0):
        """Fase de predição: estima o próximo estado."""
        self.x = self.F * self.x + B * u
        self.P = self.F * self.P * self.F + self.Q
        return self.x, self.P
    
    def update(self, z):
        """Fase de correção: ajusta com a medição real."""
        # Ganho de Kalman
        S = self.H * self.P * self.H + self.R
        K = self.P * self.H / S
        
        # Inovação (surpresa)
        y = z - self.H * self.x
        
        # Atualiza estado e incerteza
        self.x = self.x + K * y
        self.P = (1 - K * self.H) * self.P
        
        self.history.append({
            "measurement": z,
            "estimate": self.x,
            "uncertainty": self.P,
            "kalman_gain": K,
            "innovation": y
        })
        return self.x, self.P
    
    def step(self, z, u=0.0, B=0.0):
        """Ciclo completo: prediz e corrige."""
        self.predict(u, B)
        return self.update(z)

Aplicação Prática: Suavizando Métricas de Produtividade

Agora vamos usar o filtro com dados reais. Imagine que você anota sua produtividade diária (0-10) num caderno. Os dados são naturalmente ruidosos — humor, sono, café, distrações. O Filtro de Kalman revela a tendência real:

import random

# Dados simulados de produtividade (30 dias)
random.seed(42)
true_productivity = 6.0  # tendência real
measurements = []

for day in range(30):
    # Tendência real com leve crescimento
    true_productivity += random.uniform(-0.1, 0.15)
    # Medição com ruído (humor, sono, etc.)
    noise = random.gauss(0, 1.5)
    measurements.append(max(0, min(10, true_productivity + noise)))

# Aplica o Filtro de Kalman
kf = KalmanFilter1D(
    x0=5.0,   # chute inicial
    P0=10.0,  # alta incerteza inicial
    Q=0.3,    # processo moderadamente estável
    R=2.25,   # sensor ruidoso (variância de 1.5²)
    F=1.0,    # estado não muda sozinho
    H=1.0     # medição direta
)

results = []
for z in measurements:
    est, unc = kf.step(z)
    results.append((z, est, unc))

# Imprime comparação
print(f"{'Dia':>3} | {'Medição':>8} | {'Estimativa':>10} | {'Incerteza':>9} | {'Ganho K':>7}")
print("-" * 55)
for i, (z, est, unc) in enumerate(results):
    rec = kf.history[i]
    print(f"{i+1:>3} | {z:>8.2f} | {est:>10.2f} | {unc:>9.4f} | {rec['kalman_gain']:>7.4f}")

O Que Observar nos Resultados

Nos primeiros dias, o ganho de Kalman é alto (~0.3-0.5) porque o filtro está aprendendo. A incerteza cai rapidamente. A partir do dia 10, o ganho se estabiliza em torno de 0.1-0.15, e a estimativa é suave mas responsiva. Se um dia a medição for absurdamente fora (tipo 2.0 quando a tendência é 7.0), o filtro não entra em pânico — ele sabe que provavelmente é ruído.

Painel de controle com filtros e ajustes finos — calibrar parâmetros do Filtro de Kalman é como ajustar knobs de um mixer
Calibrar Q e R no Filtro de Kalman é como ajustar knobs de um mixer: sensibilidade demais gera ruído, de menos gera atraso. (Foto: Egor Komarov / Pexels)

Calibrando os Parâmetros Q e R (O Segredo Que Ninguém Conta)

A maioria dos tutoriais de Kalman mostra o código mas não ensina a calibrar Q e R. E é aí que 90% das implementações falham. Vamos resolver isso:

Estimando R (Ruído do Sensor)

Se você tem dados estacionários (uma variável que não deveria mudar), calcule a variância das medições. Isso é R.

def estimate_R(stationary_measurements):
    """Estima o ruído do sensor a partir de medições estáveis."""
    n = len(stationary_measurements)
    mean = sum(stationary_measurements) / n
    variance = sum((x - mean) ** 2 for x in stationary_measurements) / (n - 1)
    return variance

Estimando Q (Ruído do Processo)

Q representa o quanto o sistema real pode mudar inesperadamente. Se sua produtividade pode variar bastante de um dia para outro, Q deve ser maior. Uma heurística prática: comece com Q = R / 10 e ajuste.

def auto_calibrate(measurements, initial_R_factor=0.1):
    """Calibração automática ingênua para começar."""
    R_est = estimate_R(measurements[:10])  # usa os 10 primeiros
    Q_est = R_est * initial_R_factor
    return Q_est, R_est

Versão Multivariada: Rastreando Posição e Velocidade

O filtro 1D é útil, mas o poder real do Kalman aparece quando rastreamos múltiplas variáveis simultaneamente. Aqui está uma versão 2D que estima posição e velocidade ao mesmo tempo — útil para rastrear qualquer coisa que se move (um hábito, uma métrica financeira, um KPI):

class KalmanFilter2D:
    """Filtro de Kalman 2D: estima posição e velocidade."""
    
    def __init__(self, dt=1.0, process_noise=0.1, measurement_noise=1.0):
        self.dt = dt
        # Estado: [posição, velocidade]
        self.x = [0.0, 0.0]
        # Matriz de covariância (lista de listas)
        self.P = [[100.0, 0.0], [0.0, 100.0]]
        # Transição: posição += velocidade * dt
        self.F = [[1.0, dt], [0.0, 1.0]]
        # Observação: vemos apenas posição
        self.H = [[1.0, 0.0]]
        # Ruídos
        self.Q = [[process_noise, 0.0], [0.0, process_noise]]
        self.R = [[measurement_noise]]
    
    def mat_mult(self, A, B):
        """Multiplicação de matrizes genérica."""
        rows_a, cols_a = len(A), len(A[0])
        rows_b, cols_b = len(B), len(B[0])
        C = [[0.0]*cols_b for _ in range(rows_a)]
        for i in range(rows_a):
            for j in range(cols_b):
                for k in range(cols_a):
                    C[i][j] += A[i][k] * B[k][j]
        return C
    
    def predict(self):
        """Predição: x = F * x, P = F * P * F' + Q"""
        new_x = [
            self.F[0][0]*self.x[0] + self.F[0][1]*self.x[1],
            self.F[1][0]*self.x[0] + self.F[1][1]*self.x[1]
        ]
        self.x = new_x
        
        FP = self.mat_mult(self.F, self.P)
        FT = [[self.F[j][i] for j in range(2)] for i in range(2)]
        FPFt = self.mat_mult(FP, FT)
        self.P = [[FPFt[i][j] + self.Q[i][j] for j in range(2)] for i in range(2)]
    
    def update(self, z):
        """Correção com medição de posição."""
        # Inovação: y = z - H*x
        y = z - (self.H[0][0]*self.x[0] + self.H[0][1]*self.x[1])
        
        # S = H * P * H' + R
        PHt_0 = self.P[0][0]*self.H[0][0] + self.P[0][1]*self.H[0][1]
        PHt_1 = self.P[1][0]*self.H[0][0] + self.P[1][1]*self.H[0][1]
        S = self.H[0][0]*PHt_0 + self.H[0][1]*PHt_1 + self.R[0][0]
        
        # K = P * H' / S
        K = [PHt_0 / S, PHt_1 / S]
        
        # Atualiza estado
        self.x[0] += K[0] * y
        self.x[1] += K[1] * y
        
        # Atualiza covariância: P = (I - K*H) * P
        KH = [[K[i]*self.H[0][j] for j in range(2)] for i in range(2)]
        I_KH = [[(1.0 if i==j else 0.0) - KH[i][j] for j in range(2)] for i in range(2)]
        self.P = self.mat_mult(I_KH, self.P)
        
        return self.x[:]

Quando NÃO Usar o Filtro de Kalman

Nem tudo são flores. O Filtro de Kalman tem premissas fortes que, quando violadas, destroem a estimativa:

  • Linearidade: O Kalman clássico assume que as relações são lineares. Para sistemas não-lineares (como a maioria dos sistemas reais), use o Extended Kalman Filter (EKF) ou o Unscented Kalman Filter (UKF).
  • Ruído Gaussiano: O filtro assume que o ruído segue distribuição normal. Se seus dados têm outliers pesados (distribuição de cauda longa), considere um filtro de partículas.
  • Estacionariedade dos parâmetros: Q e R constantes assumem que o nível de ruído não muda ao longo do tempo. Para sistemas adaptativos, use Adaptive Kalman Filter.

Dica de ouro: Se seus dados têm muitos outliers, combine o Filtro de Kalman com um detector de anomalias (como o Z-Score que já mostramos neste post). Rejeite medições que estão além de 3 desvios-padrão antes de alimentar o filtro.

Casos de Uso Práticos Para o Seu Dia a Dia

O Filtro de Kalman não serve só para foguetes. Veja aplicações reais para desenvolvedores e profissionais de tech:

1. Suavização de Métricas de API

Latência de resposta, taxa de erro, throughput — tudo isso é ruidoso. Um KalmanFilter1D em cima de métricas de API revela tendências reais e gera alertas mais confiáveis que thresholds estáticos.

2. Estimativa de Progresso em Projetos

Se você estima que uma tarefa leva X horas e a cada dia mede o progresso real, o Kalman combina sua estimativa inicial com as medições para dar uma previsão cada vez mais precisa de quando o projeto termina.

3. Tracking de Hábitos

Anote seu nível de energia, foco ou humor diariamente. O Kalman revela a tendência subjacente e te mostra se você está realmente melhorando ou se foram só dois dias bons seguidos.

4. Filtro para Dados de Sensores IoT

Temperatura, umidade, luminosidade — sensores baratos são barulhentos. Um KalmanFilter1D rodando no Raspberry Pi ou no ESP32 (via MicroPython) limpa os dados antes de enviar para o dashboard.

Performance e Limitações

Nossa implementação 1D roda em O(1) por step — constante. A versão 2D é O(n³) onde n é a dimensão do estado (matrizes pequenas). Para estados com mais de 10 dimensões, considere usar numpy para as operações matriciais.

Aqui vai um benchmark rápido:

import time

kf = KalmanFilter1D(x0=0, P0=1, Q=0.1, R=1.0)
N = 1_000_000
start = time.perf_counter()
for i in range(N):
    kf.step(float(i % 100))
elapsed = time.perf_counter() - start
print(f"{N} steps em {elapsed:.2f}s ({N/elapsed:.0f} steps/s)")
# Resultado típico: ~1M steps em 0.5-1.0s

Conexões: Kalman e Outros Algoritmos da Série

O Filtro de Kalman tem relações profundas com vários algoritmos que já exploramos aqui no Mente Binária:

  • HyperLogLog: Ambos são estimadores probabilísticos. O Kalman estima estado, o HLL estima cardinalidade. Ambos sacrificam precisão absoluta por eficiência.
  • SimHash: O fingerprint do SimHash e a estimativa do Kalman compartilham a ideia de compressão com perda controlada — manter o sinal, descartar o ruído.
  • Bloom Filter: Assim como o Bloom Filter diz “provavelmente existe” com alta confiança, o Kalman diz “provavelmente está aqui” com incerteza quantificada.

Código Completo Para Copiar e Colar

Para quem quer o pacote completo — filtro 1D + calibração automática + visualização ASCII:

#!/usr/bin/env python3
"""Filtro de Kalman 1D completo — copiar e rodar."""

class KalmanFilter1D:
    def __init__(self, x0=0.0, P0=1.0, Q=0.1, R=1.0, F=1.0, H=1.0):
        self.x, self.P, self.Q, self.R, self.F, self.H = x0, P0, Q, R, F, H
        self.history = []

    def step(self, z):
        # Predict
        self.x = self.F * self.x
        self.P = self.F * self.P * self.F + self.Q
        # Update
        S = self.H * self.P * self.H + self.R
        K = self.P * self.H / S
        self.x += K * (z - self.H * self.x)
        self.P = (1 - K * self.H) * self.P
        self.history.append((z, self.x, K))
        return self.x

def ascii_chart(values, width=60, height=15):
    """Gráfico ASCII simples para visualização rápida."""
    mn, mx = min(values), max(values)
    rng = mx - mn or 1
    grid = [[" "]*width for _ in range(height)]
    step = max(1, len(values)//width)
    for i in range(0, len(values), step):
        col = i // step
        if col >= width: break
        row = int((values[i] - mn) / rng * (height - 1))
        grid[height - 1 - row][col] = "█"
    for row in grid:
        print("".join(row))

if __name__ == "__main__":
    import random
    random.seed(42)
    
    # Gera dados ruidosos
    data = [5 + i*0.1 + random.gauss(0, 1.5) for i in range(50)]
    
    # Filtra
    kf = KalmanFilter1D(x0=5, P0=10, Q=0.2, R=2.25)
    estimates = [kf.step(z) for z in data]
    
    print("=== Medições (ruidosas) ===")
    ascii_chart(data)
    print("\n=== Estimativas Kalman (suaves) ===")
    ascii_chart(estimates)
    print(f"\nGanho de Kalman final: {kf.history[-1][2]:.4f}")
    print(f"Incerteza final: {kf.P:.4f}")

Próximos Passos: Onde Isso Vai Dar

O Filtro de Kalman é a porta de entrada para estimadores bayesianos mais sofisticados. Se você gostou dessa abordagem, os próximos passos naturais são:

  • Extended Kalman Filter (EKF): Para sistemas não-lineares — usa linearização por Jacobiano.
  • Unscented Kalman Filter (UKF): Para sistemas altamente não-lineares — usa sigma points em vez de linearização.
  • Filtro de Partículas: Para distribuições não-Gaussianas — usa amostragem Monte Carlo.
  • Kalman Smoother: Versão backward que usa dados futuros para refinar estimativas passadas.

E tudo isso pode ser implementado em Python puro, sem dependências. A matemática é acessível, e o ganho de compreensão vale cada hora investida.


O Filtro de Kalman é uma daquelas ferramentas que muda como você enxerga dados. Depois que você entende o princípio — predizer, medir, ponderar, corrigir — começa a ver esse padrão em todo lugar: na forma como seu cérebro atualiza crenças, como o mercado ajusta preços, como um GPS calcula sua posição.

Me conta nos comentários: qual métrica da sua vida ou do seu trabalho você gostaria de suavizar com um Kalman? Produtividade? Latência de API? Nível de estresse? Sono? Manda aí que no próximo post eu posso implementar o caso de uso mais votado.

Posts Similares

Deixe um comentário

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