Não existe projeto simples
No segundo semestre de 2024 pude concluir o MBA de Jornalismo de Dados do Instituto Brasileiro de Ensino, Desenvolvimento e Pesquisa (IDP). Parte dos requisitos para a finalização do curso era a criação de uma reportagem jornalística, em equipe, a partir da orientação de um professor orientador.
A equipe da qual eu fazia parte optou, após outras tentativas — fracassadas por falta de tempo, dados compatíveis, ou ambos —, em tentar obter informações sobre possíveis privilégios que certos grupos empresariais teriam recebido após realizarem doações para campanhas de prefeituras do país em 2020.
“É fácil, é só...”
A lógica da proposta era relativamente direta. Utilizando Python deveríamos obter:
- Os dados de doações eleitorais abertos pelo Tribunal Superior Eleitoral
- Os dados de sociedades empresariais abertos, do mesmo modo, pela Receita Federal.
E em seguida, de posse de ambas as bases, imaginamos, é possível cruzá-las e:
- Identificar quem são as pessoas que compõem sociedades empresariais
- Que foram declaradas como doadores em campanhas de candidatos que se tornaram vitoriosos nos pleitos municipais.
- Em resumo: identificar os indivíduos que estão em ambas as bases ao mesmo tempo.
- Através de pesquisas em reportagens e documentos públicos, publicados entre 2020 e a data de finalização do trabalho, analisar o relacionamento dos entes públicos e privados que selecionamos.
Infelizmente cometi o erro de também considerar esse processo todo como simples.
O destino aparentemente se viu obrigado, assim como faz com todas as pessoas que se consideram diante de um problema de rápida solução, a me colocar no meu lugar de humildade novamente. Esse post explica o porquê de eu estar errado no meu primeiro momento e os passos necessários para encontrar uma solução.
Não é bem assim
Os dados das principais bases que foram usadas no desenvolvimento deste projeto são disponibilizadas em formatos de arquivo de texto, com valores separados por ;. No caso dos arquivos de sócios de empresas disponibilizado pela Receita Federal o arquivo ainda conta com uma modificação da extensão do arquivo, mas que em nada muda o processo de leitura.
Amigos não deixarem amigos exportarem para csv. Infelizmente não sou amigo de nenhum servidor destes órgãos, logo tive que lidar com os arquivos em sua forma original.
Caso fosse um projeto a ser mantido em produção constante, este seria um momento propício para inserir estes dados em um sistema de gerenciamento de bases de dados mais robusto, como o PostgreSQL, ou até mesmo utilizar soluções mais simples, como o Sqlite. Essa decisão, entretanto, demandaria mais tempo e linhas de código a serem escritas, logo foi descartada.
Do HD para RAM
Este momento contou com a preocupação de nosso orientador, que temia pela quantidade de memória RAM da máquina que poderia ser consumida ao carregar todas as bases ao mesmo tempo para que fizesse o cruzamento.
Meu computador pessoal provavelmente não sofreria com este problema, mas considerando que a metodologia deveria ser útil para que outros jornalistas e pesquisadores conseguissem replicar as instruções, a eficiência do código deveria ser levada em consideração também.
Para contornar estes problemas, alguns artifícios foram adotados:
Seleção de colunas
Após uma breve análise do conteúdo de cada coluna é possível escolher somente aquelas que serão úteis para a análise que se deseja realizar. O Pandas — biblioteca do Python que me acompanha em 9 de 10 casos necessários para manipular dados tabulares — permite essa construção facilmente utilizando o parâmetro usecols dentro da função read_csv().
Fazendo um paralelo com SQL seria o mesmo que escrever uma consulta utilizando
SELECT A, B, C
FROM table;
Assim obtemos:
df_socios = pd.read_csv(
...
names=[
"cnpj_basico", "identificador_socio", "nome_socio",
"cpf_socio", "qualificacao_socio", "data_entrada_sociedade",
"pais", "cpf_representante_legal", "nome_representante_legal",
"qualificacao_representante", "faixa_etaria"
],
usecols=[
"cnpj_basico", "cpf_socio",
"data_entrada_sociedade", "nome_socio"
])
e também
df_extratos_candidatos = pd.read_csv(
...
usecols=[
"NR_CNPJ_PRESTADOR_CONTA", "DS_CARGO_PRESTADOR_CONTA", "SG_PARTIDO",
"NM_PRESTADOR_CONTA", "TP_PESSOA", "DT_LANCAMENTO", "NR_DOCUMENTO",
"VR_LANCAMENTO", "NR_CPF_CNPJ_CONTRAPARTE", "NM_CONTRAPARTE"])
E neste ponto vale um acender um alerta: a base de sócios disponibilizada pela Receita Federal não possui cabeçalhos, razão pela qual é necessário utilizar também o parâmetro names com os nomes dos campos como descrito pelo dicionário de dados para a função.
Dividir para conquistar
Ler o arquivo em partes e processar pequenos pedaços de informações antes de retornar ao escopo geral pode ser uma boa ideia caso o arquivo original seja suficientemente grande a ponto de ocupar grande parte da memória RAM e impossibilitar manipular os dados nos passos seguintes.
Um fator crucial na nossa análise foi o de que estávamos interessados em analisar somente grupos já pré estabelecidos: 1. candidatos à prefeitura (excluem-se, portanto, candidatos à vereadores) 2. sócios de empresas que fizeram doações à campanhas daqueles.
Desse modo foi possível diminuir consideravelmente a quantidade de linhas que compõem a tabela final de prestação de contas.
Novamente, se em SQL teríamos algo parecido com
SELECT (
NR_CNPJ_PRESTADOR_CONTA,
DS_CARGO_PRESTADOR_CONTA,
SG_PARTIDO,
NM_PRESTADOR_CONTA,
TP_PESSOA,
DT_LANCAMENTO,
NR_DOCUMENTO,
VR_LANCAMENTO,
NR_CPF_CNPJ_CONTRAPARTE,
NM_CONTRAPARTE
)
FROM extratos_candidatos
WHERE (TP_PESSOA = "1") AND (DS_CARGO_PRESTADOR_CONTA = "PREFEITO")
;
... para que sejam retornados somente os dados de extratos que nos interessa.
Convertendo para a sintaxe do Pandas chegamos em:
colunas = [
"NR_CNPJ_PRESTADOR_CONTA", "DS_CARGO_PRESTADOR_CONTA", "SG_PARTIDO",
"NM_PRESTADOR_CONTA", "TP_PESSOA", "DT_LANCAMENTO",
"NR_DOCUMENTO","VR_LANCAMENTO", "NR_CPF_CNPJ_CONTRAPARTE", "NM_CONTRAPARTE"
]
selecionados = []
# Neste ponto o pandas irá gerar "mini dataframes" com a quantidade de linhas estipuladas no parâmetro chunksize
# Esses mini dataframes (nomeados "chunks" neste exemplo) passarão por uma filtragem simples
# O resultado de cada uma dessas filtragens é, enfim, adicionado à lista "selecionados"."""
for chunk in df_extratos_candidatos = pd.read_csv(usecols=colunas, chunksize=500_000):
filtrado = chunk[
(chunk[TP_PESSOA == "1"])
&
(chunk[DS_CARGO_PRESTADOR_CONTA == "PREFEITO"])
]
selecionados.append(filtrado)
df_extrados_filtrados = pd.concat(selecionados)
Dados preenchidos por humanos e dados omitidos
As informações usadas nesta análise são derivadas de dados oficiais, visto que foram carregadas diretamente dos repositórios de órgãos públicos federais. Mesmo assim possuem características que demandam cautela durante seu tratamento.
Partindo-se da pressuposto de que os dados societários têm uma boa confiabilidade — já que são derivados dos registros de empresas — eles foram considerados o equivalente ao padrão ouro dentro desse contexto: os dados de doadores seriam submetidos à verificação perante a eles, e não o contrário. Isso pela simples razão de que a maneira mais fácil de ter resultados inconsistentes em uma análise é introduzir o fator humano.
Inserir dados em determinado sistema é uma etapa delicada da alimentação de um banco de dados, e quando feito por humanos, os motivos das possíveis catástrofes que podem ser encontradas variam desde o simples erro de digitação até a aplicação de regras pouco definidas e documentadas — e que resultam em variações entre equipes ou de tomadas de decisão entre o dia atual e o anterior.
Isso será ilustrado com os os dados de doadores das campanhas mais à frente.
Apesar da alta qualidade dos dados da Receita Federal, por razões de privacidade, o órgão omite parte do Cadastro de Pessoa Física (CPF) resultando em um dado no formato ***NNNNNN**. É importante ressaltar esse fato porque, em função do espaço limitado de possibilidades para gerar um número de CPF válido, aliado com a quantidade de dígitos ocultados, torna possível que surjam “colisões” durante o cruzamento entre as duas bases. Em outras palavras: o analista deve ficar atento para que duas pessoas diferentes, — cuja única semelhaça entre si seja a mera coincidência de que os mesmos números componham a parte interna de seus respectivos CPFs — não sejam consideradas como um mesmo indivíduo.
Duas medidas em relação aos nomes das pessoas foram tomadas para garantir que a variabilidade de dados fosse reduzida: a normalização dos nomes e o cálculo da similaridade.
A normalização de valores de nomes foi feita a partir de uma função simples:
def normalize_str(string: str) -> str:
"""
Função auxiliar para remover os acentos e outros caracteres de nomes de prefeitos.
Adaptado do livro Python Fluente (Luciano Ramalho).
"""
import unicodedata
try:
normalized = unicodedata.normalize("NFKD", string)
string = "".join([c for c in normalized if not unicodedata.combining(c)])
string = string.casefold()
return string
except TypeError:
return "error"
df_socios['nome_socio'] = df_socios['nome_socio'].apply(normalize_str)
O cálculo de similaridade, por outro lado, foi feita a partir de outra função presente na biblioteca padrão do Python. Como é uma função simples, a aplicação via lambda foi suficiente:
from difflib import SequenceMatcher
df_socios_extratos['distance'] = df_socios_extratos.apply(
lambda x: SequenceMatcher(None, x['NM_CONTRAPARTE'], x['nome_socio']).ratio(),
axis=1)
A documentação oficial pode ser consultada para maiores detalhes sobre o cálculo utilizado.
Neste momento foi possível obter uma ideia melhor de como a base se comportou após o cruzamento, assim como a escala do problema das colisões entre pessoas com partes internas do CPF semelhantes.
![Comparação entre nomes nas diferentes bases Tabela que mostra alguns registros após o cruzamento das bases. São registros de nomes que não são iguais entre si, mas provavelmente se referem a mesma pessoa e se diferenciam somente por uma abreviação indevida ou omissão de sobrenomes.]](https://i.imgur.com/UpMTQoB.png)
![Comparação entre nomes nas diferentes bases A distribuição da similaridade entre os nomes das duas bases é ilustrada por uma curva bastante semelhante à uma curva de sino. Há um um pico substancial nos valores que indicam correspondência exata entre nomes.]](https://i.imgur.com/7s35ADc.png)
Ficou claro que um cruzamento simples não seria suficiente para gerar resultados satisfatórios nesta análise, visto que a maior parte dos registros resultaram em colisões entre indivíduos que possuem nomes bastante diferentes. O histograma acima ilustra bem o problema: O cruzamento dos banco de dados com base nos CPFs — parcialmente omitidos — dos indivíduos resultou em uma grande parcela de relacionamentos criados entre sujeitos com pouca similaridade entre seus nomes.
A partir de análises de amostragens de diferentes percentis de similaridade decidiu-se que o índice de semelhança entre nomes acima de 0.8 foi suficiente para, com boa margem de segurança, admitir que os resultados correspondiam à mesma pessoa. Assim: – As diferenças a partir deste ponto (similaridade > 0.80) são, em sua absoluta maioria, frutos de abreviações ou omissões de partes do nome dentro da base de doações do TSE. – Os resultados abaixo deste patamar (similaridade < 0.80) foram, por outro lado, considerados como pouco confiáveis e fruto de colisões indesejadas e, portanto, descartados.
Conclusões
Utilizando a biblioteca memory_profiler foi possível medir o impacto das decisões tomadas em direção à melhorar ao gerenciamento de memória.
Avaliando os algoritmos na minha máquina com 64GB de memória RAM os resultados obtidos são:
O pico de maior utilização de memória ocorreu ao carregar o arquivo de extratos dos candidatos.
- O consumo de memória atingiu o pico de 1213 MiB (cerca de 1.3 GB).
- Considerando que o arquivo original tem 1.42 GB em disco e não foram possíveis fazer filtros significativos em relação a ele, este já era um resultado esperado.
Em relação a base de dados societários da Receita Federal, o pico de consumo ocorreu em 724.2 MiB (cerca de 759 MB).
- Considerando que a base em seu formato original em disco possui 2.32 GB, houve um consumo de memória pouco menor que 1/3 da base original.
Em ambos os casos o programa parece ser capaz de ser executado em basicamente qualquer máquina minimamente moderna. Assim, considero que as preocupações do orientador — que se tornaram as minhas também — foram satisfeitas.
A partir deste momento os dados foram enviados para os colegas repórteres que produziram a reportagem apresentada para a banca avaliadora. A ferramenta CruzaGrafos, mantida pela Abraji, também foi utilizada para enriquecer a análise nesta etapa.
Finalmente, todos os arquivos foram disponibilizados em um repositório, assim como a reportagem final.