Refactor: Remove old scripts and consolidate functionality into helper modules

- Deleted `bench.py`, `importar_csv.py`, and `scraper.py` as they are no longer needed.
- Moved the functionality of `bench.py` into `helpers/python/bench.py`.
- Moved the functionality of `importar_csv.py` into `helpers/python/importar_csv.py`.
- Moved the functionality of `scraper.py` into `helpers/python/scraper.py`.
- Added a new `matcher.py` script for enhanced matching functionality with detailed logging and error handling.
- Improved CSV loading and processing with better encoding detection and error management.
- Enhanced multi-core processing for matching SISU and ENEM data.
This commit is contained in:
Murilo Scolari 2025-10-14 04:19:46 -03:00
parent 4ab340fb5a
commit b2b32e8a89
5 changed files with 411 additions and 1 deletions

File diff suppressed because one or more lines are too long

411
helpers python/matcher.py Normal file
View file

@ -0,0 +1,411 @@
import pandas as pd
import numpy as np
from datetime import datetime
import warnings
import csv
import os
import multiprocessing as mp
from functools import partial
# Tenta importar o tqdm, mas não quebra se não tiver
try:
from tqdm import tqdm
TQD_INSTALADO = True
except ImportError:
TQD_INSTALADO = False
warnings.filterwarnings('ignore')
# ============================================================================
# CONFIGURAÇÕES INICIAIS
# ============================================================================
print("="*80)
print("SCRIPT DE MATCHING SISU x ENEM - ALTA PRECISÃO (OTIMIZADO P/ MULTI-CORE)")
print("="*80)
print(f"\nInício: {datetime.now().strftime('%H:%M:%S')}\n")
# Caminhos dos arquivos
ARQUIVO_SISU = 'base_sisu.csv'
ARQUIVO_ENEM = 'base_enem.csv'
ARQUIVO_SAIDA = 'base_consolidada.csv'
ARQUIVO_RELATORIO = 'relatorio_matching.txt'
# --- Otimização ---
# Define o número de núcleos a serem usados. Pega automaticamente todos os disponíveis.
NUM_NUCLEOS = os.cpu_count()
# Parâmetros de matching
MARGEM_NOTA = 15
PESO_NOTA = 0.35
PESO_UF = 0.20
PESO_MUNICIPIO = 0.15
PESO_DEPENDENCIA = 0.15
PESO_ENSINO_MEDIO = 0.10
PESO_DEFICIENCIA = 0.05
SCORE_MINIMO = 0.60
# ============================================================================
# FUNÇÕES AUXILIARES
# ============================================================================
def detectar_separador(arquivo, encoding='latin1'):
"""
Detecta o separador do arquivo CSV automaticamente
"""
try:
with open(arquivo, 'r', encoding=encoding) as f:
amostra = ''.join(f.readline() for _ in range(10))
sniffer = csv.Sniffer()
dialect = sniffer.sniff(amostra, delimiters=',;\t|')
return dialect.delimiter
except Exception as e:
print(f" ⚠ Não conseguiu detectar separador automaticamente: {e}")
with open(arquivo, 'r', encoding=encoding) as f:
primeira_linha = f.readline()
separadores = {',': primeira_linha.count(','),
';': primeira_linha.count(';'),
'\t': primeira_linha.count('\t'),
'|': primeira_linha.count('|')}
sep_detectado = max(separadores, key=separadores.get)
if separadores[sep_detectado] > 0:
return sep_detectado
return ','
def limpar_colunas_invalidas(df):
"""
Remove colunas inválidas
"""
colunas_remover = [col for col in df.columns if
str(col).startswith('<!') or
str(col).startswith('<html') or
str(col).strip() == '' or
'Unnamed' in str(col)]
if colunas_remover:
print(f" ⚠ Removendo {len(colunas_remover)} coluna(s) inválida(s)")
df = df.drop(columns=colunas_remover)
return df
def carregar_dados_otimizado(arquivo, tipo='sisu'):
"""
Carrega os dados testando encodings e detectando separador
"""
print(f"Carregando arquivo {tipo.upper()}...")
encodings = ['latin1', 'utf-8', 'iso-8859-1', 'cp1252']
df = None
for encoding in encodings:
try:
sep = detectar_separador(arquivo, encoding)
print(f" ℹ Tentando encoding={encoding}, sep='{sep}'...")
try:
df_temp = pd.read_csv(arquivo, encoding=encoding, sep=sep, low_memory=False, on_bad_lines='skip')
except ValueError:
df_temp = pd.read_csv(arquivo, encoding=encoding, sep=sep, on_bad_lines='skip', engine='python')
if len(df_temp.columns) > 1:
df = df_temp
print(f" ✓ Sucesso! Encoding: {encoding}, Separador: '{sep}'")
break
else:
print(f" ⚠ Apenas {len(df_temp.columns)} coluna detectada, tentando outro...")
except Exception:
continue
if df is None or len(df.columns) <= 1:
raise Exception(f"Não foi possível carregar o arquivo '{arquivo}' corretamente.")
df = limpar_colunas_invalidas(df)
col_map = {col: col.strip().lower() for col in df.columns}
df = df.rename(columns=col_map)
if tipo == 'sisu':
colunas_necessarias = ['sg_uf_ies', 'nu_nota_candidato', 'no_inscrito']
else: # enem
colunas_necessarias = ['nu_nota_cn', 'nu_nota_ch', 'nu_nota_lc', 'nu_nota_mt', 'sg_uf_prova']
faltando = [col for col in colunas_necessarias if col not in df.columns]
if faltando:
print(f" ⚠ AVISO: Colunas faltando para '{tipo}': {faltando}")
for col in df.select_dtypes(include=['object']).columns:
if df[col].nunique() / len(df) < 0.5 and len(df) > 1000:
if col not in ['no_ies', 'no_campus', 'no_curso', 'no_inscrito', 'no_modalidade_concorrencia']:
df[col] = df[col].astype('category')
print(f" ✓ {len(df):,} registros carregados")
print(f" ✓ {len(df.columns)} colunas encontradas\n")
return df
def calcular_media_enem(df):
"""
Calcula a média das notas do ENEM
"""
print("Calculando média das notas do ENEM...")
colunas_nota = ['nu_nota_cn', 'nu_nota_ch', 'nu_nota_lc', 'nu_nota_mt', 'nu_nota_redacao']
colunas_encontradas = [col for col in colunas_nota if col in df.columns]
if not colunas_encontradas:
raise ValueError("Nenhuma coluna de nota do ENEM encontrada para calcular a média.")
if len(colunas_encontradas) < 5:
print(f" ⚠ AVISO: Apenas {len(colunas_encontradas)}/5 colunas de nota encontradas: {colunas_encontradas}")
df['media_enem'] = df[colunas_encontradas].mean(axis=1, skipna=True)
antes = len(df)
df.dropna(subset=['media_enem'], inplace=True)
df = df[df['media_enem'] > 0]
depois = len(df)
print(f" ✓ Média calculada ({antes-depois:,} registros sem notas removidos)\n")
return df
def criar_blocos(df_sisu, df_enem):
"""
Cria blocos por UF para processamento paralelo
"""
print("Criando blocos de comparação (blocking)...")
if 'sg_uf_ies' not in df_sisu.columns or 'sg_uf_prova' not in df_enem.columns:
raise KeyError("Coluna de UF ('sg_uf_ies' ou 'sg_uf_prova') não encontrada.")
ufs_comuns = set(df_sisu['sg_uf_ies'].unique()) & set(df_enem['sg_uf_prova'].unique())
print(f" ℹ {len(ufs_comuns)} UFs em comum encontradas.")
blocos = [(uf, df_sisu[df_sisu['sg_uf_ies'] == uf], df_enem[df_enem['sg_uf_prova'] == uf])
for uf in ufs_comuns]
print(f" ✓ {len(blocos)} blocos criados\n")
return blocos
def calcular_score_match(row_sisu, candidatos_enem):
"""
Calcula o score de matching para um candidato do SISU contra uma lista de candidatos do ENEM.
Usa itertuples para maior performance.
"""
scores = []
for row_enem in candidatos_enem.itertuples():
score_total = 0
# 1. Score da NOTA (35%)
diff_nota = abs(row_sisu.nu_nota_candidato - row_enem.media_enem)
if diff_nota <= MARGEM_NOTA:
score_nota = 1 - (diff_nota / MARGEM_NOTA)
score_total += score_nota * PESO_NOTA
else: # Se a nota já está fora da margem, não tem chance, pula para o próximo
continue
# 2. Score da UF (20%) - Já garantido pelo blocking, então adicionamos o peso
score_total += PESO_UF
# 3. Score do MUNICÍPIO (15%)
if hasattr(row_enem, 'co_municipio_prova') and pd.notna(row_enem.co_municipio_prova):
score_total += PESO_MUNICIPIO * 0.5
# 4. Score da DEPENDÊNCIA (15%)
if hasattr(row_enem, 'tp_dependencia_adm_esc'):
modalidade = getattr(row_sisu, 'no_modalidade_concorrencia', '')
modalidade = str(modalidade).lower()
dependencia = row_enem.tp_dependencia_adm_esc
if pd.notna(dependencia):
if 'ampla' in modalidade:
if dependencia in [2, 4]: score_total += PESO_DEPENDENCIA
elif dependencia == 3: score_total += PESO_DEPENDENCIA * 0.5
else: # Cotas
if dependencia == 1: score_total += PESO_DEPENDENCIA
elif dependencia == 3: score_total += PESO_DEPENDENCIA * 0.8
# 5. Score ENSINO MÉDIO (10%)
if hasattr(row_sisu, 'ensino_medio') and pd.notna(row_sisu.ensino_medio):
if str(row_sisu.ensino_medio).upper() == 'S':
score_total += PESO_ENSINO_MEDIO
# 6. Score DEFICIÊNCIA (5%)
if hasattr(row_sisu, 'deficiente') and hasattr(row_enem, 'deficiente'):
if pd.notna(row_sisu.deficiente) and pd.notna(row_enem.deficiente):
if (str(row_sisu.deficiente).upper() == 'S') == bool(row_enem.deficiente):
score_total += PESO_DEFICIENCIA
if score_total >= SCORE_MINIMO:
scores.append((row_enem.Index, score_total))
if scores:
# Retorna o melhor match (maior score)
return max(scores, key=lambda x: x[1])
return None, 0
def processar_bloco(args):
"""
Função worker que processa um único bloco (UF).
Projetada para ser executada em um processo separado.
"""
uf, bloco_sisu, bloco_enem = args
resultados_bloco = []
# itertuples() é muito mais rápido que iterrows()
for row_sisu in bloco_sisu.itertuples():
candidatos_filtrados = bloco_enem[
(bloco_enem['media_enem'] >= row_sisu.nu_nota_candidato - MARGEM_NOTA) &
(bloco_enem['media_enem'] <= row_sisu.nu_nota_candidato + MARGEM_NOTA)
]
if not candidatos_filtrados.empty:
melhor_match = calcular_score_match(row_sisu, candidatos_filtrados)
if melhor_match[0] is not None:
melhor_idx, score = melhor_match
row_sisu_dict = row_sisu._asdict()
row_enem_series = bloco_enem.loc[melhor_idx]
row_combinada = {f'sisu_{k}': v for k, v in row_sisu_dict.items()}
row_combinada.update({f'enem_{k}': v for k, v in row_enem_series.items()})
row_combinada['match_score'] = score
row_combinada['match_uf'] = uf
resultados_bloco.append(row_combinada)
return resultados_bloco, len(bloco_sisu), len(resultados_bloco)
def processar_matching_em_paralelo(blocos):
"""
Processa o matching em paralelo, distribuindo os blocos por UF entre os núcleos da CPU.
"""
print(f"Processando matching em paralelo com {NUM_NUCLEOS} núcleo(s)...")
print("-" * 80)
resultados_finais = []
total_processado = 0
total_matches = 0
# Cria um pool de processos
with mp.Pool(processes=NUM_NUCLEOS) as pool:
# Usa imap_unordered para processar os blocos e obter resultados assim que ficam prontos
# Adiciona a barra de progresso com tqdm se estiver instalada
map_func = pool.imap_unordered(processar_bloco, blocos)
if TQD_INSTALADO:
map_func = tqdm(map_func, total=len(blocos), desc="Processando Blocos (UFs)")
for resultados_bloco, processado_bloco, matches_bloco in map_func:
if resultados_bloco:
resultados_finais.extend(resultados_bloco)
total_processado += processado_bloco
total_matches += matches_bloco
print("-" * 80)
print(f"\n✓ Total de registros SISU processados: {total_processado:,}")
print(f"✓ Total de matches encontrados: {total_matches:,}")
if total_processado > 0:
print(f"✓ Taxa de sucesso geral: {(total_matches/total_processado*100):.2f}%\n")
return pd.DataFrame(resultados_finais)
def gerar_relatorio(df_original_sisu, df_final):
"""
Gera um relatório de texto com as estatísticas do matching.
"""
print("Gerando relatório...")
relatorio = [
"=" * 80,
"RELATÓRIO DE MATCHING - SISU x ENEM",
"=" * 80,
f"\nData/Hora: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}",
f"\nParâmetros utilizados:",
f" - Margem de nota: ±{MARGEM_NOTA} pontos",
f" - Score mínimo: {SCORE_MINIMO}",
f"\n\nRESULTADOS:",
f" - Total de registros SISU: {len(df_original_sisu):,}",
f" - Total de matches encontrados: {len(df_final):,}"
]
if len(df_original_sisu) > 0:
relatorio.append(f" - Taxa de sucesso: {(len(df_final)/len(df_original_sisu)*100):.2f}%")
if not df_final.empty:
relatorio.extend([
f"\n\nQUALIDADE DOS MATCHES:",
f" - Score médio: {df_final['match_score'].mean():.4f}",
f" - Score mínimo: {df_final['match_score'].min():.4f}",
f" - Score máximo: {df_final['match_score'].max():.4f}",
f"\n\nDISTRIBUIÇÃO POR QUALIDADE:"
])
faixas = [(0.85, 1.01, "Excelente"), (0.75, 0.85, "Muito Bom"), (0.65, 0.75, "Bom"), (0.60, 0.65, "Aceitável")]
for min_s, max_s, label in faixas:
qtd = len(df_final[(df_final['match_score'] >= min_s) & (df_final['match_score'] < max_s)])
pct = (qtd / len(df_final) * 100) if len(df_final) > 0 else 0
relatorio.append(f" - {label} ({min_s:.2f}-{max_s:.2f}): {qtd:,} ({pct:.1f}%)")
if 'match_uf' in df_final.columns:
relatorio.append(f"\n\nTOP 5 UFs COM MAIS MATCHES:")
top_ufs = df_final['match_uf'].value_counts().head(5)
for uf, count in top_ufs.items():
relatorio.append(f" - {uf}: {count:,} matches")
relatorio.append("\n" + "=" * 80)
relatorio_str = '\n'.join(relatorio)
with open(ARQUIVO_RELATORIO, 'w', encoding='utf-8') as f:
f.write(relatorio_str)
print(relatorio_str)
print(f"\n✓ Relatório salvo em: {ARQUIVO_RELATORIO}\n")
# ============================================================================
# EXECUÇÃO PRINCIPAL
# ============================================================================
def main():
try:
df_sisu = carregar_dados_otimizado(ARQUIVO_SISU, tipo='sisu')
df_enem = carregar_dados_otimizado(ARQUIVO_ENEM, tipo='enem')
df_enem = calcular_media_enem(df_enem)
antes = len(df_sisu)
df_sisu.dropna(subset=['nu_nota_candidato'], inplace=True)
df_sisu = df_sisu[df_sisu['nu_nota_candidato'] > 0]
depois = len(df_sisu)
print(f"Pré-processamento SISU: {antes-depois:,} registros sem nota removidos\n")
blocos = criar_blocos(df_sisu, df_enem)
if not blocos:
print("❌ ERRO: Nenhum bloco foi criado. Verifique se as UFs coincidem entre as bases.")
return
df_final = processar_matching_em_paralelo(blocos)
if df_final.empty:
print("⚠ AVISO: Nenhum match foi encontrado com os parâmetros atuais. Considere ajustá-los.")
return
print(f"Salvando resultado em {ARQUIVO_SAIDA}...")
df_final.to_csv(ARQUIVO_SAIDA, index=False, encoding='utf-8-sig', sep=';')
print(f"✓ Arquivo salvo com sucesso!\n")
gerar_relatorio(df_sisu, df_final)
print("\nAMOSTRA DOS RESULTADOS (5 primeiros matches):")
print("-" * 80)
colunas_mostrar = ['sisu_no_inscrito', 'sisu_nu_nota_candidato', 'enem_media_enem', 'match_score', 'match_uf']
colunas_existentes = [col for col in colunas_mostrar if col in df_final.columns]
if colunas_existentes:
print(df_final[colunas_existentes].head().to_string())
except (FileNotFoundError, KeyError, ValueError, Exception) as e:
print(f"\n❌ OCORREU UM ERRO: {e}")
import traceback
traceback.print_exc()
finally:
print("\n" + "="*80)
print("PROCESSAMENTO CONCLUÍDO!")
print(f"Arquivo final: {ARQUIVO_SAIDA}")
print(f"Relatório: {ARQUIVO_RELATORIO}")
print(f"\nFim: {datetime.now().strftime('%H:%M:%S')}\n")
# Proteção para garantir que o código de multiprocessing só rode quando o script for executado diretamente
if __name__ == "__main__":
main()