Do CSV ao modelo: preparando dados do ENEM 2016

 · 5 min de leitura  ·  Atualizado em

Série Analytics Engineering na Prática

Dados públicos só geram valor quando chegam ao consumo com contexto, qualidade e transformações reproduzíveis. Neste projeto, usei os dados do ENEM 2016 para analisar o perfil dos participantes e preparar uma base para estimar a nota de Matemática.

O notebook do projeto contém a exploração e os tratamentos realizados. O estudo parte de dois arquivos: treino, com 13.730 linhas e 166 colunas, e teste, com 4.576 linhas e 47 colunas.

Desafio de dados

Antes do modelo, havia três questões de engenharia importantes:

  • conciliar arquivos com esquemas diferentes;
  • interpretar ausências de nota a partir do status de presença;
  • transformar respostas categóricas sem perder seu significado.

Esse contexto determina se uma ausência é um erro, um valor desconhecido ou uma regra de negócio. Preencher todos os nulos de forma indiscriminada produziria uma base tecnicamente completa, mas semanticamente incorreta.

Ingestão e validação inicial

Os arquivos foram carregados separadamente para preservar a distinção entre treino e teste.

train = pd.read_csv("train.csv", index_col=0)
test = pd.read_csv("test.csv")

assert train.shape == (13730, 166)
assert test.shape == (4576, 47)
assert train.index.is_unique

Em um pipeline produtivo, essas verificações fariam parte de um contrato de dados. Além das dimensões e da unicidade, eu validaria tipos, domínio das categorias, faixas possíveis das notas e presença das colunas obrigatórias.

Regras de qualidade

As notas ausentes foram tratadas com base no indicador de presença. Quando o participante não compareceu à prova, a ausência da nota recebeu zero conforme a regra adotada no projeto.

exam_rules = {
    "TP_PRESENCA_CH": "NU_NOTA_CH",
    "TP_PRESENCA_CN": "NU_NOTA_CN",
    "TP_PRESENCA_MT": "NU_NOTA_MT",
    "TP_PRESENCA_LC": "NU_NOTA_LC",
}

for presence_col, score_col in exam_rules.items():
    absent = train_df[presence_col] != 1
    train_df.loc[absent, score_col] = train_df.loc[absent, score_col].fillna(0)

Centralizar a regra em um dicionário reduz repetição e facilita manutenção. Também torna explícito por que um valor foi alterado — um detalhe essencial para rastreabilidade.

Para as variáveis categóricas, o sexo foi mapeado para valores numéricos e as respostas do questionário socioeconômico foram codificadas para o experimento.

train_df["TP_SEXO"] = train_df["TP_SEXO"].map({"F": 0, "M": 1})

Uma evolução importante seria persistir os mapeamentos junto ao pipeline e usar codificação adequada à natureza de cada variável. Categorias nominais não devem ganhar uma ordem artificial só porque foram convertidas em números.

O que os dados mostraram

São Paulo concentrou o maior número de participantes, seguido por Ceará e Minas Gerais. A participação feminina foi maior e as idades se concentraram entre 17 e 18 anos.

Participantes por estado

Distribuição de participantes por sexo

Distribuição de idade

As notas das provas ficaram concentradas próximas de 500 pontos, enquanto a redação se concentrou perto de 600. O questionário socioeconômico também revelou associação entre desempenho, escolaridade familiar e renda — uma relação descritiva que não deve ser interpretada isoladamente como causal.

Distribuição das notas

Notas por estado

Camada de consumo para Machine Learning

Após as transformações, separei os atributos da variável-alvo e criei conjuntos de treino e validação.

target = train["NU_NOTA_MT"]
features = train.drop(columns=["NU_NOTA_MT", "TP_PRESENCA_MT"])

X_train, X_valid, y_train, y_valid = train_test_split(
    features,
    target,
    test_size=0.25,
    random_state=42,
)

No experimento, a Regressão Linear obteve R² = 0,917 e a Random Forest, R² = 0,925. Como o método score desses regressores retorna o coeficiente de determinação, o resultado deve ser apresentado como , não como “acurácia”. A pequena diferença também não basta para declarar um algoritmo universalmente melhor: custo, estabilidade, explicabilidade e desempenho fora da amostra precisam entrar na decisão.

Modelo analítico proposto

Para disponibilizar os dados a análises e dashboards, eu definiria a granularidade da tabela fato como uma linha por participante, edição e área de conhecimento.

ModeloGranularidade ou função
fct_resultado_enemparticipante × edição × área da prova
dim_tempoedição, ano e datas de aplicação
dim_localidademunicípio, unidade federativa e região
dim_perfil_participanteatributos demográficos permitidos para análise
dim_situacao_provapresença, ausência e motivo associado

Na camada de métricas, eu publicaria total_participantes, taxa_presenca, nota_media, nota_mediana e percentis por edição. Cada indicador teria filtro, denominador e política de nulos documentados. Dados socioeconômicos exigiriam acesso controlado e agregação suficiente para reduzir risco de exposição individual.

Os testes incluiriam unicidade da chave da fato, relacionamento com as dimensões, notas dentro da faixa permitida e reconciliação entre o total da camada tratada e o total publicado.

Como eu levaria este fluxo para produção

Eu separaria a solução em três camadas:

  1. Raw: arquivos originais, imutáveis e versionados por edição do ENEM;
  2. Trusted: tipos padronizados, regras de presença aplicadas e registros inválidos isolados;
  3. Curated: tabelas analíticas e conjunto de atributos prontos para consumo.

O pipeline teria testes de esquema e faixa, métricas de nulos, contagem de registros por etapa e versionamento dos mapeamentos categóricos. Assim, uma nova edição do ENEM poderia ser processada com o mesmo fluxo, mantendo histórico e facilitando auditoria.

Aprendizados

O principal aprendizado foi que o resultado do modelo começa na interpretação correta do dado. Ingestão, regras de qualidade, documentação e reprodutibilidade não são etapas auxiliares: são a base para qualquer análise confiável.