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:
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
411
helpers python/matcher.py
Normal 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()
|
||||
Loading…
Reference in a new issue