Os 94% que nunca existiram
Eu ainda lembro da saída do terminal: 94,3% de acurácia. Conferi a matriz de confusão duas vezes. O número foi pro README e pro currículo. Também estava errado — não por erro de arredondamento, mas por 25 pontos inteiros. O valor honesto era 69%.
Ninguém pegou isso por mim. Só apareceu porque decidi construir ferramentas capazes de verificar meus próprios resultados. Corrigir esse bug me ensinou mais sobre ML de produção do que construir o sistema original. Se você avalia modelos em dados sequenciais — vídeo, áudio, sensores vestíveis, séries temporais — tem uma boa chance de o mesmo bug estar no seu pipeline agora.
Olha só, esse é um post-mortem de um vazamento de dados (data leakage) aparentemente banal, e da reconstrução da avaliação que veio depois. Pra entender como engenharia assistida por IA está mudando o fluxo de MVP, dá uma olhada no nosso artigo sobre Go como a linguagem inesperada de MVP para engenharia assistida por IA.

O vazamento: splits aleatórios em amostras dependentes
O sistema era um detector de quedas em tempo real para monitoramento assistivo. Uma webcam roda pelo MediaPipe Pose pra extrair landmarks esqueléticos. A partir deles eu calculo features biomecânicas — ângulo do torso, alturas normalizadas de quadril e ombro, geometria da bounding box. Uma Random Forest classifica cada frame, e uma janela de suavização sobre as últimas predições decide se dispara o alarme. Tudo roda em CPU. Nenhum vídeo sai do dispositivo.
Treinei no dataset Le2i de detecção de quedas e usei a linha mais inocente do mundo:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
O problema: esse split é aleatório sobre frames individuais, e frames do mesmo vídeo não são independentes. Dois frames capturados com 80 milissegundos de diferença são quase idênticos — mesma pessoa, mesma pose, mesma iluminação. Quando você embaralha tudo e separa 20% pra teste, quase todo frame de teste tem um gêmeo próximo no treino.
Ou seja: o teste não mede generalização pra pessoas e salas novas. Ele mede memorização, e uma Random Forest memoriza quase tudo. Esse é o data leakage na sua forma mais comum: as amostras não são independentes, então o split aleatório entrega as respostas de bandeja pro conjunto de teste.
A correção é simples de enunciar: divida pela coisa que torna suas amostras dependentes. Pra vídeo, divida por vídeo.
from sklearn.model_selection import GroupKFold
# Agrupa por video_id para que nenhuma gravação apareça dos dois lados do fold
gkf = GroupKFold(n_splits=5)
for train_idx, test_idx in gkf.split(X, y, groups=video_ids):
model.fit(X[train_idx], y[train_idx])
score = model.score(X[test_idx], y[test_idx])
O score agrupado voltou em 69,1%. Um dos folds, com o mesmo código dividido honestamente, caiu pra 31%. Esse é o número real.

As labels eram piores que o split
Depois de auditar o split, comecei a auditar o resto. Minhas labels de treino vinham de nomes de pasta — se o vídeo estava em falls/, todo frame era rotulado fall. Mas uma queda encenada dura de um a dois segundos; o resto do clipe é atividade comum. O dataset na verdade fornece anotações frame a frame marcando o início e o fim exatos de cada queda, então cruzei com elas. O resultado foi vergonhoso:
- 76% dos frames com label
fallmostravam a pessoa apenas andando normalmente. - Só 6% capturavam uma queda de verdade.
- Os 18% restantes mostravam alguém já caído no chão depois.
- Um vídeo de 78 segundos contribuía com 44% de toda a classe positiva, e 1,5 segundo dele era queda.
Minhas labels tinham mudado a tarefa sem eu perceber. O modelo resolvia o problema mais fácil que elas descreviam, não o que eu queria propor. E é exatamente esse tipo de falha que o leakage esconde: uma avaliação vazada não consegue te dizer que suas labels estão erradas, porque ela só mede memorização — e um modelo memoriza labels erradas com a mesma facilidade que as certas.
Reconstruindo a partir do ground truth
Reconstruí o conjunto de treino a partir das anotações:
- Um frame é positivo se está dentro da janela anotada de queda ou nos dois segundos deitado que vêm depois.
- Tudo antes da queda é negativo.
- Frames ambíguos tarde demais depois da queda (pessoa pode estar deitada ainda ou levantando, sem ground truth) foram excluídos, não chutados.
Também usei o dataset inteiro — 186 de 190 vídeos, em vez dos 28 originais. Duas cenas não tinham arquivo de anotação, então assisti 59 vídeos frame a frame e anotei 29 janelas de queda na mão, com um buffer de segurança em volta das minhas estimativas pra que minha imprecisão não contaminasse a classe negativa.
Meça o que o usuário experiencia
Um cuidador nunca vê um frame. Ele recebe um alerta ou não. Então a avaliação agora simula a lógica exata do alarme de produção — uma janela de votação sobre probabilidades por frame — em vídeos segurados, e reporta resultados em nível de evento:
| Métrica | Valor |
|---|---|
| Quedas detectadas | 121 / 126 (96,0%) |
| Latência mediana do alarme | 0,58 s do início da queda |
| Vídeos com falso alarme | 3–7 de 60, dependendo do modelo |
Todo miss teve a mesma causa: o estimador de pose perdia a pessoa no meio da queda — oclusão por móvel, motion blur ou imagem em resolução baixa demais pra rastrear. Se o estimador de pose não vê a queda, o classificador nunca tem a vez dele. Você só descobre isso avaliando eventos inteiros.
O trade-off que uma métrica única esconde
A avaliação honesta revelou uma decisão que eu nunca tinha percebido que estava tomando:
| Modelo | Quedas detectadas | Vídeos com falso alarme | Deitar de propósito |
|---|---|---|---|
| labels "person down" | 96,0% | 7/60 | dispara |
| labels "fall motion" | 89,7% | 3/60 | silencioso |
Nenhum dos dois está errado. Em cuidado de idosos, você provavelmente quer um alarme sempre que alguém está no chão. Se o objetivo é distinguir queda de deitar, o modelo de movimento ganha. O comportamento é controlado pela política de labels, não pela arquitetura — e uma única acurácia teria escondido o trade-off inteiro. Eu entrego os dois e documento a diferença.
Pra ver como ferramentas de feature flag estão evoluindo junto com esse tipo de ML de produção, confere Vercel Flags GA: um mergulho no gerenciamento de feature flags.
![]()
O checklist
Se você levar só uma coisa dos meus 25 pontos desperdiçados, leve o primeiro item:
- Agrupe seus splits pelo que torna as amostras dependentes — vídeo, paciente, sujeito, sessão, dispositivo. Se trocar pra split aleatório faz seu score pular, o número maior é o mentiroso.
- Compare suas labels com as anotações reais em vez de confiar no nome das pastas. Depois olhe dentro da sua classe positiva e veja o que ela realmente contém.
- Meça eventos inteiros, como o usuário faria: quantos incidentes pegou, com que rapidez, e com que frequência disparou quando nada aconteceu.
- Mantenha um teste comportamental segurado pro modo de falha que mais te assusta. O meu é um deitar de propósito que não pode disparar alarme.
- Publique a correção. Um sistema corrigido em que as pessoas podem acreditar vale mais do que um impressionante em que não podem.
Só gravei o demo ao vivo de 55 segundos — queda real, pipeline rodando, dashboard na nuvem ficando vermelho, alerta reconhecido — porque finalmente confiava no sistema por trás. Nunca teria coragem de filmar a versão de 94,3%.
Limitações e ressalvas
- O dataset Le2i tem quedas encenadas em salas residenciais, não emergências reais. O shift de distribuição no mundo real será maior do que qualquer coisa medida aqui.
- A taxa de 96% é em vídeos segurados do mesmo dataset. Generalização entre datasets não foi testada.
- Estimação de pose falha em oclusão, motion blur e baixa resolução — esse é o modo de falha dominante que resta, e é um problema de percepção, não de classificador.
- Uma métrica única nunca é suficiente pra sistemas que tangenciam segurança. Reporte métricas em nível de evento, latência e taxa de falso alarme juntas.
Próximos passos
Se você está construindo algo parecido, a lista de prioridades é: (1) conserte os splits, (2) conserte as labels, (3) migre pra avaliação em nível de evento, (4) só então — e só então — ajuste o modelo. A maior parte do ganho está nos passos 1–3. Minha próxima rodada de trabalho é em percepção, não em tuning de classificador.
Fonte: My Fall-Detection Model Scored 94%, and It Was Lying to Me — apenas métricas agregadas, dataset usado com atribuição a Charfi et al. (2013).