Skip to main content

ncm-classificador

Classificador fiscal NCM: descrição livre de item de NF-e → top-3 códigos NCM com confiança calibrada e abstenção. Roda offline, na sua CPU — nenhum dado sai da máquina.

Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.


Instalação

Via pip (com suporte à API)

pip install ncm-classificador[api]

Sem a flag [api], instala apenas a CLI (sem dependências do servidor FastAPI).


Uso

A CLI resolve o pacote-modelo nesta ordem: --pacote DIR > env NCM_PACOTE > ~/.ncm/pacote.

1. Classificar item único

ncm classificar "PARAFUSO SEXT ZINC M8X40 DIN933"

Saída real (tabela rich — código + confiança calibrada; a descrição textual do NCM não faz parte do pacote exportado):

                    NCM para: PARAFUSO SEXT ZINC M8X40 DIN933
┌───────────────┬─────────────────────┐
│ candidato NCM │ confiança calibrada │
├───────────────┼─────────────────────┤
│ 73181500      │               99.7% │
│ 73181100      │                0.1% │
│ 86079900      │                0.0% │
└───────────────┴─────────────────────┘
Ferramenta de apoio · caráter orientativo · a responsabilidade pela
classificação é do contribuinte/contador.

Quando o item abstém, uma linha extra em destaque aparece antes do disclaimer: ABSTEVE — confiança insuficiente; escale a um contador — ou, quando a posição fecha mesmo sem fechar a folha, RESPOSTA PARCIAL (ver a seção dedicada mais abaixo).

Com --json, a saída é uma linha NDJSON (mesmo shape usado pela API — ver adiante). Repare no campo resposta_parcial: ele está sempre presente, e vem null quando não há posição confiante o bastante para fechar (é o caso deste item, que já responde direto na folha):

ncm classificar "PARAFUSO SEXT ZINC M8X40 DIN933" --json
{"descricao": "PARAFUSO SEXT ZINC M8X40 DIN933", "top3": [{"ncm8": "73181500", "confianca": 0.996774}, {"ncm8": "73181100", "confianca": 0.000645}, {"ncm8": "86079900", "confianca": 0.000174}], "abstem": false, "resposta_parcial": null, "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.", "versao_pacote": "f-1"}

2. Classificar lote (arquivo CSV)

ncm lote itens.csv > resultado.csv

Não existe --saida: o resultado sai no stdout e é redirecionado com >. (--json também é aceito e emite uma linha NDJSON por item, no mesmo formato do item 1.)

Entrada (itens.csv, coluna obrigatória descricao):

descricao
PARAFUSO SEXT ZINC M8X40 DIN933
ARROZ(CLASSIFICAÇÂO SEM CARACTERÍSTICAS)
FEIJÃO(CLASSIFICAÇÂO SEM CARACTERÍSTICAS)
OVINO DOMESTICO SANSIBEL - 190KG

Saída real (resultado.csv) — colunas ncm8_N/confianca_N (não ncm_N), campo abstem (não abstencao), e duas colunas novas no fim, nivel_parcial/codigo_parcial (script que só lê abstem continua funcionando sem alteração):

descricao,ncm8_1,confianca_1,ncm8_2,confianca_2,ncm8_3,confianca_3,abstem,nivel_parcial,codigo_parcial
PARAFUSO SEXT ZINC M8X40 DIN933,73181500,0.996774,73181100,0.000645,86079900,0.000174,false,,
ARROZ(CLASSIFICAÇÂO SEM CARACTERÍSTICAS),10062020,0.301446,10061092,0.141093,10061091,0.107488,true,posicao,1006
FEIJÃO(CLASSIFICAÇÂO SEM CARACTERÍSTICAS),07082000,0.368774,07133399,0.191740,07139090,0.065303,true,,
OVINO DOMESTICO SANSIBEL - 190KG,01041019,0.492764,01042010,0.337461,01041090,0.052320,true,posicao,0104
# Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.

As três últimas linhas abstêm (abstem=true) — a saída real, não um exemplo forjado. O ARROZ e o OVINO fecham a posição (nivel_parcial=posicao, com o código de 4 dígitos em codigo_parcial); o FEIJÃO abstém sem apoio nenhum, e as duas colunas ficam vazias.

3. Consultar informações do modelo

ncm info

Saída real:

                      Pacote de inferência NCM
┌──────────────────────────┬────────────────────────────────────────┐
│ campo                    │ valor                                  │
├──────────────────────────┼────────────────────────────────────────┤
│ versão do pacote         │ f-1                                    │
│ modelo base              │ neuralmind/bert-large-portuguese-cased │
│ corrida / data de export │ f / 2026-07-18                         │
│ temperatura (T)          │ 0.788                                  │
│ pisos de abstenção       │ conf 0.55 · margem 0.1                 │
│ classes (folhas NCM)     │ 9748                                   │
│ integridade sha256       │ ok (conferida ao carregar)             │
└──────────────────────────┴────────────────────────────────────────┘
Ferramenta de apoio · caráter orientativo · a responsabilidade pela
classificação é do contribuinte/contador.

ncm info mostra proveniência e configuração de calibração do pacote carregado — não expõe métricas de acurácia/ECE do conjunto de validação (essas vivem nos artefatos de avaliação do treino, não no pacote de produto).


Resposta parcial de posição

Quando o modelo não tem confiança para fechar os 8 dígitos, mas está bastante seguro sobre os 4 primeiros (a posição da NCM), ele devolve um apoio extra em vez de abstenção muda: o campo resposta_parcial. Isso só acontece com pacotes de modelo que trazem piso_posicao no inferencia.json — o f-1 do Hugging Face traz; pacotes mais antigos (como o d-1) continuam funcionando normalmente, só que sempre com resposta_parcial: null.

abstem continua true nesse caso — a posição não é uma segunda forma de decisão automática, é o modelo estreitando o universo de folhas candidatas para o contador escolher entre elas. Saída real (--json, mesmo shape usado pela API):

{"descricao": "ARROZ(CLASSIFICAÇÂO SEM CARACTERÍSTICAS)", "top3": [{"ncm8": "10062020", "confianca": 0.301446}, {"ncm8": "10061092", "confianca": 0.141093}, {"ncm8": "10061091", "confianca": 0.107488}], "abstem": true, "resposta_parcial": {"nivel": "posicao", "codigo": "1006", "confianca": 0.902203, "candidatas_folha": ["10062020", "10061092", "10061091"], "texto": "Faltam os últimos 4 dígitos — refine entre as folhas candidatas com seu contador."}, "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.", "versao_pacote": "f-1"}

Na CLI interativa, isso aparece como RESPOSTA PARCIAL (destacada em ciano) em vez do ABSTEVE amarelo — o top-3 continua exibido do mesmo jeito nos dois casos.

No ncm lote, o mesmo dado chega em duas colunas no fim do CSV: nivel_parcial (hoje só existe o valor posicao — a camada de capítulo foi medida e arquivada por não bater a barra de precisão pré-registrada, ver o model card) e codigo_parcial (os 4 dígitos). Scripts que já leem só a coluna abstem continuam funcionando sem qualquer mudança — as colunas novas vêm depois, no fim da linha.

O nível posição foi medido em dados reais com 93,8% de acerto (321 itens, barra pré-registrada era 89,9%) e fecha em 5,6% das consultas que, de outra forma, seriam abstenções mudas — na prática, reduz de 25,6% para 19,9% a fatia de consultas reais que ficam sem nenhum código de apoio.


Servidor API

Iniciar o servidor

ncm servir --host 0.0.0.0 --porta 8000

Variáveis de ambiente obrigatórias/opcionais

Variável Tipo Padrão Descrição
NCM_API_KEYS string (obrigatória) Chave(s) de API separadas por , ex: chave1,chave2. Sem isto o servidor não sobe (fail-closed)
NCM_RATE_LIMIT_RPM int 120 Limite de requisições por minuto por chave (token bucket em memória; zera no restart)
NCM_PACOTE path ~/.ncm/pacote Caminho até o diretório do pacote-modelo (no Docker, tipicamente /pacote, ver seção Docker)

Endpoints

POST /v1/classificar

Classifica um item único.

Request:

curl -X POST http://localhost:8000/v1/classificar \
  -H "X-API-Key: sua-chave-api" \
  -H "Content-Type: application/json" \
  -d '{"descricao": "PARAFUSO SEXT ZINC M8X40 DIN933"}'

Response (200) — mesmo shape do --json da CLI (carga real, capturada rodando o servidor local; resposta_parcial sempre presente, null quando não há posição confiante o bastante):

{
  "descricao": "PARAFUSO SEXT ZINC M8X40 DIN933",
  "top3": [
    {"ncm8": "73181500", "confianca": 0.996774},
    {"ncm8": "73181100", "confianca": 0.000645},
    {"ncm8": "86079900", "confianca": 0.000174}
  ],
  "abstem": false,
  "resposta_parcial": null,
  "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
  "versao_pacote": "f-1"
}

descricao aceita 1 a 2000 caracteres. Corpo fora desse contrato (campo errado, string vazia/longa demais) devolve 422 no formato problem+json (RFC 9457).

POST /v1/classificar-lote

Classifica múltiplos itens (máx. 200 por requisição). O corpo é {"descricoes": [...]} — uma lista de strings, não uma lista de objetos {"descricao": ...}.

Request:

curl -X POST http://localhost:8000/v1/classificar-lote \
  -H "X-API-Key: sua-chave-api" \
  -H "Content-Type: application/json" \
  -d '{
    "descricoes": [
      "PARAFUSO SEXT ZINC M8X40 DIN933",
      "ARROZ(CLASSIFICAÇÂO SEM CARACTERÍSTICAS)"
    ]
  }'

Response (200) — envelope com disclaimer/versao_pacote no topo, e cada resultado com o mesmo shape do endpoint acima (também carregando disclaimer/versao_pacote). O segundo item mostra resposta_parcial preenchido — o mesmo dado que a CLI devolve para essa descrição, só que dentro do envelope de lote:

{
  "resultados": [
    {
      "descricao": "PARAFUSO SEXT ZINC M8X40 DIN933",
      "top3": [
        {"ncm8": "73181500", "confianca": 0.996774},
        {"ncm8": "73181100", "confianca": 0.000645},
        {"ncm8": "86079900", "confianca": 0.000174}
      ],
      "abstem": false,
      "resposta_parcial": null,
      "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
      "versao_pacote": "f-1"
    },
    {
      "descricao": "ARROZ(CLASSIFICAÇÂO SEM CARACTERÍSTICAS)",
      "top3": [
        {"ncm8": "10062020", "confianca": 0.301446},
        {"ncm8": "10061092", "confianca": 0.141093},
        {"ncm8": "10061091", "confianca": 0.107488}
      ],
      "abstem": true,
      "resposta_parcial": {
        "nivel": "posicao",
        "codigo": "1006",
        "confianca": 0.902203,
        "candidatas_folha": ["10062020", "10061092", "10061091"],
        "texto": "Faltam os últimos 4 dígitos — refine entre as folhas candidatas com seu contador."
      },
      "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
      "versao_pacote": "f-1"
    }
  ],
  "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
  "versao_pacote": "f-1"
}

O custo no rate limit é len(descricoes) tokens do balde — um lote de 50 itens custa 50, não 1.

GET /v1/info

Retorna proveniência e configuração de calibração do pacote carregado. Não expõe métricas de acurácia/ECE (não fazem parte do pacote de produto — ver seção ncm info acima).

Request:

curl -H "X-API-Key: sua-chave-api" http://localhost:8000/v1/info

Response (200) — capturada rodando o servidor local com o pacote f-1. O campo piso_posicao só aparece (não-nulo) em pacotes que trazem a calibração da resposta parcial; pacotes antigos devolvem piso_posicao: null aqui:

{
  "versao_pacote": "f-1",
  "modelo_base": "neuralmind/bert-large-portuguese-cased",
  "corrida": "f",
  "data_export": "2026-07-18",
  "temperatura": 0.7876692056028095,
  "piso_confianca": 0.55,
  "piso_margem": 0.1,
  "piso_posicao": 0.7793302624741045,
  "classes": 9748,
  "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador."
}

GET /healthz

Verifica saúde do servidor (usado pelo Docker HEALTHCHECK). Não exige X-API-Key.

Request:

curl http://localhost:8000/healthz

Response (200):

{"status": "ok"}

Docker

Build

Deve ser executado na raiz do repositório (o Dockerfile copia o wheel de dist/):

# 1. Construir o wheel
uv build packages/ncm-classificador

# 2. Construir a imagem Docker
docker build -f packages/ncm-classificador/Dockerfile -t ncm-classificador .

Run

O pacote-modelo (1,3 GB) não é embarcado na imagem — entra como volume:

docker run \
  -p 8000:8000 \
  -v /caminho/local/do/pacote:/pacote:ro \
  -e NCM_API_KEYS=sua-chave-api \
  -e NCM_RATE_LIMIT_RPM=120 \
  ncm-classificador

Notas:

  • /caminho/local/do/pacote é o diretório local com o pacote-modelo baixado do Hugging Face (ver seção "Obtendo o pacote-modelo" abaixo)
  • -v ... :ro monta em modo somente-leitura (segurança)
  • Porta exposta: 8000 (ajuste com -p HOST:8000 conforme necessário)
  • O servidor está pronto quando logs mostram Application startup complete

Obtendo o pacote-modelo

O modelo treinado é distribuído separadamente, no Hugging Face: https://huggingface.co/DominuZ/ncm-classificador-bertimbau (CC BY 4.0). O pacote de inferência são estes 5 itens do repositório: modelo.onnx (~1,3 GB), tokenizer/, labels.json, inferencia.json e manifest.json — a integridade (SHA-256 do manifesto) é conferida automaticamente antes de qualquer resposta.

pip install huggingface_hub
hf download DominuZ/ncm-classificador-bertimbau \
  modelo.onnx labels.json inferencia.json manifest.json tokenizer/tokenizer.json tokenizer/tokenizer_config.json \
  --local-dir ~/.ncm/pacote

Uma vez baixado, aponte a variável de ambiente NCM_PACOTE para o diretório (ou use o padrão ~/.ncm/pacote, que dispensa a variável):

export NCM_PACOTE=~/.ncm/pacote
ncm classificar "PARAFUSO SEXT ZINC M8X40 DIN933"
ncm servir

Ou via Docker:

docker run -v /seu/caminho/pacote:/pacote -e NCM_PACOTE=/pacote ...

Limitações e abstenção

O modelo abstém em casos ambíguos ou com baixa confiança na predição (atualmente 48,8% dos itens na validação agregada). A abstenção é um recurso, não um defeito: itens abstidos devem ser revisados manualmente pela equipe de conformidade ou contador. Em dados reais, boa parte dessas abstenções ganha um apoio extra — a resposta parcial de posição (seção acima) — o que reduz de 25,6% para 19,9% a fatia de consultas reais que ficam sem nenhum código de apoio.

Exemplos reais de abstenção (saída do ncm lote, ver seção de uso acima): o item FEIJÃO(CLASSIFICAÇÂO SEM CARACTERÍSTICAS) recebeu abstem=true porque o 1º candidato (07082000) ficou em 36,9% de confiança, abaixo do piso de 55% configurado no pacote f-1 — e a posição também não fechou, então é uma abstenção sem apoio nenhum (resposta_parcial: null). Já o item ARROZ(CLASSIFICAÇÂO SEM CARACTERÍSTICAS) também abstém na folha, mas fecha a posição (1006, 90,2% de confiança) — abstenção com apoio parcial. Na CLI interativa (ncm classificar), a primeira aparece como a linha ABSTEVE — confiança insuficiente; escale a um contador logo abaixo da tabela; a segunda aparece como RESPOSTA PARCIAL, com a posição sugerida.


Links e suporte

Licença: código MIT · pesos do modelo CC BY 4.0 Versão: 0.2.0 Última atualização: 2026-07-18

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ncm_classificador-0.2.0.tar.gz (20.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ncm_classificador-0.2.0-py3-none-any.whl (23.0 kB view details)

Uploaded Python 3

File details

Details for the file ncm_classificador-0.2.0.tar.gz.

File metadata

  • Download URL: ncm_classificador-0.2.0.tar.gz
  • Upload date:
  • Size: 20.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for ncm_classificador-0.2.0.tar.gz
Algorithm Hash digest
SHA256 559c0eb5b62fee1faacae35853620d5f24f49120fabeb926e2f603ee2b7464ae
MD5 e9f1b453c9da6594072e7efa9b507012
BLAKE2b-256 425320120d65a6c3a2e908bc7f518645fcc506ce88a75af4a606a6a31984eaf4

See more details on using hashes here.

File details

Details for the file ncm_classificador-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: ncm_classificador-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 23.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for ncm_classificador-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 261456f0f3f50c311f9f92562be8e3bef3cd0b6317db3e3cefc3490b0c687965
MD5 91bb18bb0bb23659c8eb3aed6facbc1b
BLAKE2b-256 37d8d9b354e3d02b517294c36d5c59c3f589acdc7472e04c36cfd327101105b9

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page