Metodologia
Como as despesas são coletadas, quais regras marcam um gasto como atípico e o código-fonte de cada uma. As mesmas regras, os mesmos limites e o mesmo texto valem para todos os parlamentares, partidos e estados.
De onde vêm os dados
Todos os dias o robô baixa os arquivos oficiais da Cota para o Exercício da Atividade Parlamentar da Câmara dos Deputadose do Senado Federal. Cada linha recebe uma impressão digital (hash), o que impede duplicar registros quando o mesmo arquivo é baixado de novo. Deputados são identificados pelo número de cadastro da Câmara; senadores, pelo nome, comparado à lista oficial do Senado sem acentos e sem diferenciar maiúsculas.
Despesas de lideranças partidárias e órgãos da Câmara (sem um deputado associado) não entram na análise. No Senado, alimentação vem somada a locomoção e hospedagem, por isso a regra de refeição só se aplica à Câmara. O partido mostrado é o informado pela Câmara na própria despesa e, no Senado, o do cadastro oficial (quando disponível).
Período coberto: 2026-01 a 2026-11 · 138.215 despesas analisadas.
Pontuação
Cada alerta tem uma pontuação de 0 a 100, usada só para ordenar a fila de publicação nas redes sociais: pontuação = 100 × peso da regra × intensidade. A intensidade vai de 0 a 1. Nas regras com limite, é o valor dividido pelo limite, com teto em 5 vezes, dividido por 5; nas demais, é 1. A pontuação não é uma medida de gravidade.
Regras
R01_refeicao_alta
Refeição acima do limite
Marca refeições do próprio parlamentar com valor acima de um limite fixo, igual para todos. Só se aplica às categorias que identificam refeição (na Câmara, 'Fornecimento de alimentação do parlamentar'; o Senado publica alimentação junto com locomoção e hospedagem, por isso não há como separar).
- peso
- 0.6
- limite
- 250
- categorias
- Fornecimento de alimentação do parlamentar
- alertas ativos
- 103
Código-fonte (pipeline/regras/r01_refeicao_alta.py)
from typing import Any
import polars as pl
from pipeline import formatacao as fmt
from pipeline.regras.base import Contexto, Regra, intensidade_por_razao, na_lista, resultado
def aplicar(despesas: pl.DataFrame, params: dict[str, Any], contexto: Contexto) -> pl.DataFrame:
limite = params["limite"]
df = despesas.filter(na_lista("categoria", params["categorias"]) & (pl.col("valor") > limite))
return resultado(
df.select(
pl.col("id").alias("despesa_id"),
intensidade_por_razao(pl.col("valor") / limite).alias("intensidade"),
pl.col("valor")
.map_elements(
lambda v: (
f"Refeição de {fmt.reais(v)}; o limite configurado é {fmt.reais(limite)} "
f"({fmt.numero(v / limite)} vezes o limite)."
),
pl.String,
)
.alias("detalhe"),
)
)
REGRA = Regra(
id="R01_refeicao_alta",
nome="Refeição acima do limite",
descricao=(
"Marca refeições do próprio parlamentar com valor acima de um limite fixo, igual "
"para todos. Só se aplica às categorias que identificam refeição (na Câmara, "
"'Fornecimento de alimentação do parlamentar'; o Senado publica alimentação junto "
"com locomoção e hospedagem, por isso não há como separar)."
),
aplicar=aplicar,
modulo=__name__,
)
R02_duplicidade
Possível duplicidade
Marca despesas do mesmo parlamentar com o mesmo fornecedor, o mesmo valor e a mesma data de outra despesa já registrada. A primeira delas não é marcada; as repetições sim. Valores pequenos e categorias em que a repetição é comum (como várias diárias de hotel na mesma noite) ficam de fora, conforme a configuração.
- peso
- 1
- valor minimo
- 100
- categorias excluidas
- Hospedagem, exceto do parlamentar no Distrito Federal
- alertas ativos
- 1.528
Código-fonte (pipeline/regras/r02_duplicidade.py)
from typing import Any
import polars as pl
from pipeline import formatacao as fmt
from pipeline.regras.base import Contexto, Regra, na_lista, resultado
CHAVE = ["parlamentar_id", "fornecedor", "valor", "data"]
def aplicar(despesas: pl.DataFrame, params: dict[str, Any], contexto: Contexto) -> pl.DataFrame:
df = despesas.filter(
pl.col("data").is_not_null()
& (pl.col("valor") >= params["valor_minimo"])
& ~na_lista("categoria", params.get("categorias_excluidas", []))
).with_columns(
pl.coalesce("fornecedor_cnpj_cpf", pl.col("fornecedor_nome").str.to_uppercase()).alias(
"fornecedor"
)
)
# A primeira despesa do grupo (pela ordem do id) é a referência; as demais disparam.
df = (
df.sort("id")
.with_columns(
pl.len().over(CHAVE).alias("ocorrencias"),
pl.int_range(1, pl.len() + 1).over(CHAVE).alias("ordem"),
)
.filter(pl.col("ordem") > 1)
)
return resultado(
df.select(
pl.col("id").alias("despesa_id"),
pl.lit(1.0).alias("intensidade"),
pl.struct("ocorrencias", "valor", "data")
.map_elements(
lambda s: (
f"Há {s['ocorrencias']} despesas deste parlamentar com o mesmo fornecedor, "
f"o mesmo valor ({fmt.reais(s['valor'])}) e a mesma data "
f"({fmt.data_curta(s['data'])})."
),
pl.String,
)
.alias("detalhe"),
)
)
REGRA = Regra(
id="R02_duplicidade",
nome="Possível duplicidade",
descricao=(
"Marca despesas do mesmo parlamentar com o mesmo fornecedor, o mesmo valor e a mesma "
"data de outra despesa já registrada. A primeira delas não é marcada; as repetições "
"sim. Valores pequenos e categorias em que a repetição é comum (como várias diárias "
"de hotel na mesma noite) ficam de fora, conforme a configuração."
),
aplicar=aplicar,
modulo=__name__,
)
R03_outlier_categoria
Valor muito acima do comum na categoria
Para cada categoria de despesa e cada ano, calcula o valor abaixo do qual estão 99% das despesas (percentil 99) de todos os parlamentares. Marca as despesas acima desse valor. Categorias com poucas despesas no ano não são avaliadas.
- peso
- 0.8
- percentil
- 0.99
- amostra minima
- 100
- alertas ativos
- 1.356
Código-fonte (pipeline/regras/r03_outlier_categoria.py)
from typing import Any
import polars as pl
from pipeline import formatacao as fmt
from pipeline.regras.base import Contexto, Regra, intensidade_por_razao, resultado
GRUPO = ["categoria", "ano"]
def aplicar(despesas: pl.DataFrame, params: dict[str, Any], contexto: Contexto) -> pl.DataFrame:
percentil = params["percentil"]
df = (
despesas.filter(pl.col("valor") > 0)
.with_columns(pl.col("competencia").str.slice(0, 4).alias("ano"))
.with_columns(
pl.len().over(GRUPO).alias("amostra"),
pl.col("valor").quantile(percentil, "linear").over(GRUPO).alias("limiar"),
)
.filter(
(pl.col("amostra") >= params["amostra_minima"]) & (pl.col("valor") > pl.col("limiar"))
)
)
rotulo = f"percentil {fmt.numero(percentil * 100, 0)}"
return resultado(
df.select(
pl.col("id").alias("despesa_id"),
intensidade_por_razao(pl.col("valor") / pl.col("limiar")).alias("intensidade"),
pl.struct("valor", "limiar", "ano", "amostra")
.map_elements(
lambda s: (
f"Valor de {fmt.reais(s['valor'])}, acima do {rotulo} da categoria em "
f"{s['ano']} ({fmt.reais(s['limiar'])}, calculado sobre "
f"{fmt.numero(s['amostra'], 0)} despesas)."
),
pl.String,
)
.alias("detalhe"),
)
)
REGRA = Regra(
id="R03_outlier_categoria",
nome="Valor muito acima do comum na categoria",
descricao=(
"Para cada categoria de despesa e cada ano, calcula o valor abaixo do qual estão 99% "
"das despesas (percentil 99) de todos os parlamentares. Marca as despesas acima "
"desse valor. Categorias com poucas despesas no ano não são avaliadas."
),
aplicar=aplicar,
modulo=__name__,
)
R04_cnpj_situacao
Fornecedor com CNPJ irregular na data
Consulta o cadastro do CNPJ do fornecedor na Receita Federal (via BrasilAPI). Marca a despesa se, na data dela, a empresa ainda não tinha sido aberta ou já estava com situação diferente de 'ativa' (baixada, inapta, suspensa ou nula). Empresas que fecharam depois da despesa não são marcadas.
- peso
- 1
- alertas ativos
- 34
Código-fonte (pipeline/regras/r04_cnpj_situacao.py)
from typing import Any
import polars as pl
from pipeline import formatacao as fmt
from pipeline.regras.base import Contexto, Regra, resultado
def _detalhe(s: dict[str, Any]) -> str:
if s["aberto_depois"]:
return (
f"Segundo a Receita Federal, o CNPJ do fornecedor começou a funcionar em "
f"{fmt.data_curta(s['data_inicio'])}, depois da data da despesa "
f"({fmt.data_curta(s['data'])})."
)
return (
f"Segundo a Receita Federal, o CNPJ do fornecedor está com situação "
f"'{s['situacao']}' desde {fmt.data_curta(s['data_situacao'])}, antes da data da "
f"despesa ({fmt.data_curta(s['data'])})."
)
def aplicar(despesas: pl.DataFrame, params: dict[str, Any], contexto: Contexto) -> pl.DataFrame:
df = (
despesas.filter(
pl.col("data").is_not_null() & (pl.col("fornecedor_cnpj_cpf").str.len_chars() == 14)
)
.join(contexto.cnpjs, left_on="fornecedor_cnpj_cpf", right_on="cnpj", how="inner")
.with_columns(
(pl.col("data_inicio") > pl.col("data")).fill_null(False).alias("aberto_depois"),
(
pl.col("situacao").is_not_null()
& (pl.col("situacao").str.to_uppercase() != "ATIVA")
& (pl.col("data_situacao") <= pl.col("data"))
)
.fill_null(False)
.alias("inativo"),
)
.filter(pl.col("aberto_depois") | pl.col("inativo"))
)
return resultado(
df.select(
pl.col("id").alias("despesa_id"),
pl.lit(1.0).alias("intensidade"),
pl.struct("aberto_depois", "data_inicio", "data", "situacao", "data_situacao")
.map_elements(_detalhe, pl.String)
.alias("detalhe"),
)
)
REGRA = Regra(
id="R04_cnpj_situacao",
nome="Fornecedor com CNPJ irregular na data",
descricao=(
"Consulta o cadastro do CNPJ do fornecedor na Receita Federal (via BrasilAPI). Marca a "
"despesa se, na data dela, a empresa ainda não tinha sido aberta ou já estava com "
"situação diferente de 'ativa' (baixada, inapta, suspensa ou nula). Empresas que "
"fecharam depois da despesa não são marcadas."
),
aplicar=aplicar,
modulo=__name__,
)
R05_fim_de_semana
Despesa de expediente em fim de semana ou feriado
Marca despesas de categorias típicas de expediente de escritório (manutenção de escritório, aluguel, material de consumo, consultorias, divulgação, publicações e serviços postais) cujo documento foi emitido num sábado, domingo ou feriado nacional.
- peso
- 0.4
- valor minimo
- 100
- categorias
- Manutenção de escritório de apoio à atividade parlamentar; Aluguel de imóveis para escritório político, compreendendo despesas concernentes a eles; Aquisição de material de consumo para uso no escritório político, inclusive aquisição ou locação de software, despesas postais, aquisição de publicações, locação de móveis e de equipamentos; Contratação de consultorias, assessorias, pesquisas, trabalhos técnicos e outros serviços de apoio ao exercício do mandato parlamentar; Consultorias, pesquisas e trabalhos técnicos; Divulgação da atividade parlamentar; Assinatura de publicações; Serviços postais
- alertas ativos
- 2.115
Código-fonte (pipeline/regras/r05_fim_de_semana.py)
from datetime import date
from typing import Any
import polars as pl
from pipeline import formatacao as fmt
from pipeline.regras.base import Contexto, Regra, na_lista, resultado
from pipeline.regras.feriados import feriado
def motivo(d: date) -> str | None:
"""'sábado', 'domingo', 'feriado nacional (Tiradentes)' ou None em dia útil."""
if nome := feriado(d):
return f"feriado nacional ({nome})"
if d.weekday() >= 5:
return fmt.DIAS_DA_SEMANA[d.weekday()]
return None
def aplicar(despesas: pl.DataFrame, params: dict[str, Any], contexto: Contexto) -> pl.DataFrame:
df = despesas.filter(
pl.col("data").is_not_null()
& (pl.col("valor") >= params["valor_minimo"])
& na_lista("categoria", params["categorias"])
).with_columns(pl.col("data").map_elements(motivo, pl.String).alias("motivo"))
df = df.filter(pl.col("motivo").is_not_null())
return resultado(
df.select(
pl.col("id").alias("despesa_id"),
pl.lit(1.0).alias("intensidade"),
pl.struct("data", "motivo")
.map_elements(
lambda s: (
f"Documento de despesa de expediente emitido em "
f"{fmt.data_curta(s['data'])}, {s['motivo']}."
),
pl.String,
)
.alias("detalhe"),
)
)
REGRA = Regra(
id="R05_fim_de_semana",
nome="Despesa de expediente em fim de semana ou feriado",
descricao=(
"Marca despesas de categorias típicas de expediente de escritório (manutenção de "
"escritório, aluguel, material de consumo, consultorias, divulgação, publicações e "
"serviços postais) cujo documento foi emitido num sábado, domingo ou feriado "
"nacional."
),
aplicar=aplicar,
modulo=__name__,
)
O que um alerta significa
Um alerta é um gasto atípico segundo uma regra estatística ou cadastral. Não é acusação. Ele indica uma despesa que merece um segundo olhar e mostra o documento oficial para quem quiser conferir. Os limites estão no arquivoconfig.toml e o código completo está no repositório público.
Aviso: Atipicidade estatística não é prova de irregularidade. O parlamentar pode ter justificativa.