Prevendo nota de matemática do ENEM de 2016

 · 8 mins read

O Exame Nacional do Ensino Médio (ENEM) é uma das principais portas de entrada para o ensino superior no Brasil, por isso uma análise das notas precisa separar bem exploração de dados, modelagem preditiva e interpretação dos resultados.

Neste notebook está a análise completa e os tratamentos realizados. O deploy original no Heroku não está mais disponível, então o notebook passa a ser a principal referência reproduzível do projeto.

Vamos analisar a base do ENEM de 2016, prever a nota de matemática dos candidatos e observar padrões como distribuição por estado, sexo, idade, notas e variáveis socioeconômicas.

Para iniciar fiz a importação da base de treino e de teste.

train = pd.read_csv('https://dl.dropbox.com/s/1dcc81bthwmw47r/train.csv?dl=0', index_col=0)
test = pd.read_csv('https://dl.dropbox.com/s/ycp6it0mmndajco/test.csv?dl=0')

A base está distribuída da seguinte forma:

Treino: 13730 linhas e 166 colunas (features)
Teste: 4576 linhas e 47 colunas (features)

Analisando a base

Conseguimos identificar que o estado de São Paulo teve o maior número de participantes, seguido por Ceará e Minas Gerais

participantes

Fazendo a verificação por sexo, conseguimos observar que as mulheres tiveram uma maior participação na prova.

quantidade por sexo

Muitos dos candidatos que realizam esse exame são aqueles que estão concluindo o ensino médio, sendo assim, temos uma grande concentração da idade entre 17 e 18 anos. A probabilidade de termos um candidato com idade menor ou igual a 25 anos é de 69%.

histograma idade

Na prova e na redação, a pontuação máxima é de mil pontos. Na distribuição abaixo, observamos que as provas concentram notas próximas de 500, enquanto a redação fica mais próxima de 600. Dentro desta amostra, 6,22% dos registros tiveram nota maior ou igual a 600 na prova, e 13,25% tiveram nota maior ou igual a 700 na redação. Como estamos trabalhando com uma amostra específica, esses valores devem ser tratados como proporções observadas, não como probabilidades gerais do ENEM.

notas

Na redação temos alguns pontos que são observados no caso de fugir ao tema, for anulada, entre outros. A tabela abaixo nos mostra como que ficaram essas situações. obs.: Não incluí em gráfico pois ficaria difícil visualizar os valores menores

SituaçãoQuantidade
Sem problemas13195
Em branco133
Fuga ao tema105
Parte desconectada29
Cópia texto motivador17
Texto insuficiente12
Fere direitos autorais9
Não atendimento ao tipo7
Anulada3

Mostramos como ficaram as notas de toda a base de dados, agora vamos verificar como ficaram as notas das provas distribuídas por estado. Há uma boa quantidade de outliers (valores fora do comum), mas nossa mediana está bem próximo de 500, conforme citamos anteriormente no histograma.

notas por estado

Quando fazemos a inscrição no ENEM, há um questionário socioeconômico para respondermos. Com essas variáveis, podemos investigar associações entre contexto familiar e desempenho. Uma das perguntas é “Até que série seu pai, ou o homem responsável por você, estudou?”. No gráfico, observamos uma associação positiva entre maior escolaridade do responsável e maiores notas, mas isso não deve ser interpretado como efeito causal isolado.

questionario q001

O mesmo cuidado vale para a renda mensal da família. A amostra sugere que faixas de renda mais altas estão associadas a notas maiores, mas renda também se relaciona com outras variáveis, como acesso a escola, material de estudo, tempo disponível e infraestrutura.

questionario q006

Tratando os dados e realizando a previsão

Depois dessas análises, chegou a hora de preparar os dados para a previsão. Primeiro tratei as notas ausentes de candidatos com status diferente de “1 = Presente na prova”. A imputação com zero é uma escolha de modelagem que precisa ser documentada: ela representa ausência/eliminação como nota zero e pode alterar a distribuição do alvo. Em uma análise mais robusta, eu manteria também as variáveis de presença e avaliaria separadamente candidatos presentes e ausentes.

train_df.loc[train_df['TP_PRESENCA_CH'] != 1, 'NU_NOTA_CH'] = train_df.loc[train_df['TP_PRESENCA_CH'] != 1, 'NU_NOTA_CH'].fillna(0)
train_df.loc[train_df['TP_PRESENCA_CN'] != 1, 'NU_NOTA_CN'] = train_df.loc[train_df['TP_PRESENCA_CN'] != 1, 'NU_NOTA_CN'].fillna(0)
train_df.loc[train_df['TP_PRESENCA_MT'] != 1, 'NU_NOTA_MT'] = train_df.loc[train_df['TP_PRESENCA_MT'] != 1, 'NU_NOTA_MT'].fillna(0)
train_df.loc[train_df['TP_PRESENCA_LC'] != 1, 'NU_NOTA_LC'] = train_df.loc[train_df['TP_PRESENCA_LC'] != 1, 'NU_NOTA_LC'].fillna(0)
train_df.loc[train_df['TP_PRESENCA_LC'] != 1, 'NU_NOTA_REDACAO'] = train_df.loc[train_df['TP_PRESENCA_LC'] != 1, 'NU_NOTA_REDACAO'].fillna(0)
...

Como a maioria dos estimadores do scikit-learn trabalha com entradas numéricas, converti a variável de sexo de ‘F’ e ‘M’ para 0 e 1, respectivamente.

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

Também utilizei Label Encoder para transformar respostas do questionário socioeconômico em números. Esse ponto merece cuidado técnico: para variáveis nominais, o Label Encoder pode criar uma ordem artificial entre categorias. Quando não existe ordem real, a alternativa mais apropriada costuma ser One-Hot Encoding. Para variáveis ordinais, o ideal é definir explicitamente a ordem das categorias.

train_df['Q001'] = label_encoder.fit_transform(train_df['Q001'])
train_df['Q002'] = label_encoder.fit_transform(train_df['Q002'])
train_df['Q006'] = label_encoder.fit_transform(train_df['Q006'])
train_df['Q025'] = label_encoder.fit_transform(train_df['Q025'])
train_df['Q047'] = label_encoder.fit_transform(train_df['Q047'])

Para o treino, utilizei dois modelos supervisionados de regressão: Linear Regression e Random Forest Regressor. Regressão é adequada porque o alvo é uma variável contínua, a nota de matemática. Outros modelos poderiam ser avaliados, mas o objetivo aqui é comparar uma abordagem linear simples com um modelo não linear baseado em árvores.

Como no desafio tive que submeter um arquivo csv com os dados, vou fazer aqui uma separação dos dados de treino para realizarmos a validação dos dados e verificar como está o desempenho do nosso modelo.

target = train['NU_NOTA_MT']
train.drop(['NU_NOTA_MT', 'TP_PRESENCA_MT'], axis=1, inplace=True)
X_train, X_test, y_train, y_test = train_test_split(train, target, test_size=0.25, random_state=42)
lr = LinearRegression()
lr_model = lr.fit(X_train, y_train)
y_pred = lr_model.predict(X_test)

print(f'R² do modelo: {round(lr_model.score(X_test, y_test), 3)}')

R² do modelo: 0.917

rf = RandomForestRegressor(n_jobs=-1)
rf_model = rf.fit(X_train, y_train)
y_pred_rf = rf_model.predict(X_test)

print(f'R² do modelo: {round(rf_model.score(X_test, y_test), 3)}')

R² do modelo: 0.925

O modelo Random Forest ficou com R² maior que o Linear Regression. Isso quer dizer que ele é melhor em qualquer situação? Não. Esse resultado indica apenas que, nessa divisão treino-teste e com esse conjunto de variáveis, o Random Forest explicou uma fração ligeiramente maior da variação da nota.

Para uma comparação mais sólida, eu complementaria o R² com MAE/RMSE, validação cruzada, análise de resíduos e checagem de vazamento de dados. Se o objetivo for prever a nota antes da prova, usar notas de outras áreas como variáveis explicativas pode não ser válido operacionalmente; se o objetivo for estimar matemática a partir de resultados já observados nas demais áreas, essa escolha passa a fazer sentido.

Então é isso pessoal, espero que tenham gostado e até a próxima.