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_uniqueEm 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.



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.


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 R², 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.
| Modelo | Granularidade ou função |
|---|---|
fct_resultado_enem | participante × edição × área da prova |
dim_tempo | edição, ano e datas de aplicação |
dim_localidade | município, unidade federativa e região |
dim_perfil_participante | atributos demográficos permitidos para análise |
dim_situacao_prova | presenç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:
- Raw: arquivos originais, imutáveis e versionados por edição do ENEM;
- Trusted: tipos padronizados, regras de presença aplicadas e registros inválidos isolados;
- 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.