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.

Data scientist analyzing confusion matrix and accuracy metrics on a laptop screen for fall detection model Technical Structure Concept

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.

Wearable sensor and webcam setup for real-time fall detection using MediaPipe pose estimation Programming Illustration

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 fall mostravam 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étricaValor
Quedas detectadas121 / 126 (96,0%)
Latência mediana do alarme0,58 s do início da queda
Vídeos com falso alarme3–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:

ModeloQuedas detectadasVídeos com falso alarmeDeitar de propósito
labels "person down"96,0%7/60dispara
labels "fall motion"89,7%3/60silencioso

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.

Cloud backend architecture diagram with FastAPI and PostgreSQL for fall detection alert system Algorithm Concept Visual

O checklist

Se você levar só uma coisa dos meus 25 pontos desperdiçados, leve o primeiro item:

  1. 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.
  2. 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.
  3. Meça eventos inteiros, como o usuário faria: quantos incidentes pegou, com que rapidez, e com que frequência disparou quando nada aconteceu.
  4. 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.
  5. 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).

Este conteúdo foi elaborado com o auxílio de ferramentas de IA, com base em fontes confiáveis, e revisado pela nossa equipe editorial antes da publicação. Não substitui o aconselhamento de um profissional especializado.