Método · Extrator de balanços
O que é o extrator de balanços, e como ele foi feito.
Empresa de capital aberto é obrigada a publicar o balanço em jornal. Sai em PDF, cada uma com um layout, muitas escaneadas. O extrator lê esses arquivos e devolve uma planilha com uma linha por empresa. A parte difícil não é ler o número: é saber, com precisão, quando não se conseguiu ler.
O que entra e o que sai
Entra um PDF de balanço publicado. Sai uma linha com razão social, CNPJ, exercício, escala, escopo, valor e status. O status é o campo que faz a base valer alguma coisa, e vou chegar nele.
Foi construído e calibrado contra 15 balanços reais de empresas capixabas, não contra documento sintético. Cada regra descrita abaixo existe porque um arquivo específico quebrou sem ela.
Como funciona, em quatro etapas
- Localizar. Achar, dentro do PDF, o retângulo onde está a demonstração de resultado. Usa a camada de texto quando ela existe e presta; quando não presta, faz OCR da página. Um jornal traz várias demonstrações lado a lado, então a busca é por palavra e não por linha.
- Recortar. Renderizar só aquele retângulo em imagem de boa resolução. Recorte pequeno é barato de processar e não carrega o resto da página junto.
- Extrair. Onde a camada de texto é confiável, a leitura é determinística e custa zero. Só o que sobra vai para um modelo de visão. No corpus de teste, a maioria dos arquivos se resolve sem gastar um token.
- Validar. Conferir o número contra a aritmética do próprio documento, e atribuir o status conforme o resultado.
Duas regras de roteamento cortaram um lote de 30 arquivos de cerca de 20 minutos para 4m53s, sem mudar um valor sequer: a varredura da camada de texto passou a rodar primeiro no documento inteiro, em vez de alternar com OCR página a página, e a barra para dispensar o OCR virou um escore mínimo em vez de qualquer escore positivo.
Por que não é RAG, e por que não é fine-tuning
RAG serve para achar informação num corpus grande. Aqui o problema é o inverso: dado um documento, achar uma seção conhecida. Embedding e banco vetorial seriam custo sem retorno.
Fine-tuning não entra primeiro porque nenhum dos erros observados neste corpus foi erro de ler número. Foram erros de escala, de coluna, de escopo e de layout. Isso é regra de negócio, e regra de negócio precisa morar em código que alguém pode auditar, não em pesos que ninguém inspeciona.
O status, que é o ponto inteiro
Se "não consegui extrair" e "a empresa faturou zero" virarem o mesmo campo vazio, quem usar a base vai somar os dois e concluir por cima da mistura, sem nunca saber. Por isso cada registro carrega o próprio estado:
| Status | Significado |
|---|---|
| ok | extraído e validado |
| receita_zero | 0,00 impresso: dado válido |
| sem_movimento | traço explícito à direita do rótulo |
| dre_ausente | não há demonstração no documento |
| receita_nao_localizada | a demonstração existe, o valor não foi localizado |
| revisar | extraiu, mas a validação não fecha |
| erro | falha técnica |
Erro de leitura sai como erro, nunca como zero.
Três desses são resposta e entram na base como fato:
receita_zero, sem_movimento e
dre_ausente. Os outros dizem, com precisão, o que falta.
Um hífen que apagou R$ 202,7 milhões
Vale um exemplo de por que essas distinções custam trabalho.
sem_movimento exige um traço explícito à direita do
rótulo. Só que hífen não é traço, e a diferença é tipográfica. Um
hospital publica assim:
RECEITA DE SERVIÇOS DE SAÚDE - 19 180.969.410 170.503.540 CONTRATO DE GESTÃO
O - ali não é ausência de valor: é o começo de "-
CONTRATO DE GESTÃO", que quebrou para a linha seguinte. O registro saiu
como empresa sem faturamento, quando a receita era R$ 202,7 milhões.
O que separa os dois é o espaçamento, comparado com o da própria linha.
Traço de valor fica na coluna, longe do texto. Hífen de rótulo fica
colado na palavra anterior. Medido em todos os
sem_movimento de um lote de 150 arquivos:
| Caso | Multiplicador |
|---|---|
| Hífen de rótulo | 1,0× |
| Traços de valor legítimos | 4,2× a 122,5× |
O limiar de 3× cai no meio dessa folga, e a medida se calibra sozinha pela mediana dos espaços da linha, então não depende de corpo de fonte nem de resolução de OCR.
Como a validação funciona
A confiabilidade não vem do modelo, vem da aritmética. Demonstração contábil tem redundância embutida, e ela pega quase tudo:
| Conferência | Erro que ela detecta |
|---|---|
| bruta − deduções = líquida | linha ou coluna errada |
| líquida ≤ bruta | inversão de campos |
| o resultado consta na DMPL | coluna ou ano errados |
| ativo / receita fora de faixa | erro de escala |
| valor em reais ≤ R$ 20 bi | escala declarada errada no documento |
| o trecho literal existe no texto | número inventado pelo modelo |
A quinta linha existe por um motivo específico. As outras comparam dois
números do mesmo documento, então não enxergam rótulo de escala errado:
se o documento inteiro está mil vezes fora, a razão entre ativo e
receita continua perfeita. Uma construtora declara "(Em milhares de
Reais)" num quadro cujos valores estão em reais, e passava como
ok com R$ 145 bilhões de receita. O teto é alto de
propósito: empresa brasileira acima de R$ 20 bi se conta nos dedos, e
nenhuma publica balanço em jornal do interior.
O limite que nenhuma conferência atravessa
Aqui está a parte que interessa a quem vai construir algo parecido. O cache de OCR era indexado pelo endereço de memória do documento. Quando um documento era liberado e o próximo alocado no mesmo endereço, a chave colidia e o cache devolvia as palavras do documento anterior. Medido: o mesmo valor, 29.766.643,66, em quatro empresas diferentes numa rodada de 30.
Nenhuma conferência pega isso. Bruta menos deduções dá líquida, o resultado aparece na DMPL, a magnitude é plausível. Tudo fecha, porque o documento lido é internamente coerente. Ele só não é o documento certo.
A validação verifica consistência interna, não identidade. São coisas diferentes, e a segunda não se deduz da primeira. Todo pipeline desse tipo tem esse limite; o que varia é se ele está escrito em algum lugar.
A correção passou a usar o nome do documento como chave. Isso também colide, e o teste do caminho de imagem pegou na primeira execução: um PDF aberto a partir de bytes tem sempre o mesmo nome. Em produção nunca mordeu, porque os arquivos vêm do disco com caminho próprio, mas o buraco estava exatamente dentro da guarda contra o bug mais caro do projeto. Hoje a chave é um contador gravado no próprio documento: não recicla e não colide.
O placar, e por que ele mede pouco
No corpus onde as regras foram encontradas, o resultado é 20 de 20
corretos, com zero erros silenciosos: nenhum arquivo entrou na
base com número errado se dizendo ok. Esse é o único
critério que importa numa base que vai alimentar decisão.
E ele não diz quase nada sobre o próximo arquivo. Medido nesta mesma base: 20 de 20 no corpus conhecido, 0 valores em 9 arquivos cegos, e 10 de 10 depois de consertar. Cada rodada de consertos sobe o número do corpus conhecido e não prevê o desempenho fora dele.
Por isso o número que vale reportar é sempre o primeiro, medido em arquivo nunca visto, antes de qualquer conserto. Quem só mostra o placar depois das correções está mostrando o quanto ajustou ao corpus, não o quanto o sistema funciona.
O que perguntar a uma base extraída de documento
- Como o vazio é representado? Se há um único campo vazio para tudo, "não li" e "é zero" já se misturaram, e não há como separá-los depois.
- Qual foi o placar no primeiro contato com arquivo novo? Antes de qualquer conserto.
- O que a validação não consegue ver? Consistência interna não é identidade.
Onde isso está
O extrator é motor proprietário e o repositório é privado, então esta página é a documentação pública dele. Os números vieram do próprio corpus e das rodadas registradas. Se algum não bater, é bug, e eu quero saber.