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:
- Prediz qual deveria ser sua produtividade amanhã baseado no histórico (modelo).
- 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.

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 estadoF= matriz de transição (como o estado evolui)P_est= incerteza anteriorQ= 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 realH= 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.

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.
