Data Wrangling com Python: Preparando os dados do INEP para aplicação ao modelo de machine learning

Charles Júnior
Data Hackers
Published in
12 min readAug 29, 2023
Reprodução: geeksforgeeks

Contextualização

Dificilmente os dados de origem já estarão prontos para uso, devido a isso é necessário aplicar o Data Wrangling para preparar o dataframe para o modelo de machine learning. Data Wrangling é a coleta, limpeza e transformação de dados brutos em um formato adequado para análise. Isso inclui lidar com valores ausentes, padronizar formatos, codificar variáveis, remover duplicatas, entre outros. Embora essa etapa possa ser demorada, sua importância é indiscutível, pois assegura que os dados estejam adequadamente preparados.

Neste artigo foi aplicado o data wrangling nos indicadores de desempenho escolar do INEP a fim de prepará-los para aplicação a uma análise de componentes principais, PCA.

O INEP, órgão responsável pela avaliação da educação no Brasil, disponibiliza os resultados em arquivos Excel. Foram preparados sete indicadores de desempenho, ou seja, sete arquivos INEP.

Recursos

Os arquivos são de 2021, pois no momento da obtenção dos dados, os arquivos de 2022 ainda não estavam completos. Seguem os arquivos utilizados.

Arquivos do INEP com os indicadores de desempenhos escolar

Na ilustração abaixo, temos um exemplo de um dos arquivos do INEP que serão processados neste trabalho. Observe a estrutura composta por diversas colunas, além de cabeçalho, rodapé e a presença de valores como “ — “.

Estrutura de um dos arquivos do INEP

A tecnologia empregada para realizar o data wrangling foi a linguagem de programação Python, juntamente com o ambiente Jupyter Notebook. Além disso, foram utilizados pacotes como pandas, prettytable, sklearn, etc, para auxiliar no processo.

Aplicação

O primeiro passo consiste em baixar os sete arquivos do INEP. O link para acesso encontra-se na seção de referências, ao final deste artigo.

É necessário garantir que os pacotes utilizados, como pandas e prettytable, estejam devidamente instalados. Veja abaixo um exemplo de como realizar a instalação.

pip install pandas prettytable

Importar os pacotes utilizados no trabalho.

import pandas as pd
from prettytable import PrettyTable
from itertools import zip_longest
import gc
from sklearn.preprocessing import StandardScaler

Carregando os dados

No início do processo, criamos um dataframe para cada arquivo. Uma característica dos arquivos do INEP é a presença de cabeçalhos e rodapés, os quais foram descartados durante a leitura. Isso foi possível utilizando os parâmetros skiprows e nrows na função read_excel.

# Importando os dados já eliminando cabeçalho e rodapé
# Não foi informado path porque os arquivos estão no mesmo local do script
atu = pd.read_excel("ATU_ESCOLAS_2021.xlsx", skiprows=8, nrows=174179)
dsu = pd.read_excel("DSU_ESCOLAS_2021.xlsx", skiprows=9, nrows=178126)
had = pd.read_excel("HAD_ESCOLAS_2021.xlsx", skiprows=8, nrows=151777)
icg = pd.read_excel("ICG_ESCOLAS_2021.xlsx", skiprows=10, nrows=178370)
ird = pd.read_excel("IRD_ESCOLAS_2021.xlsx", skiprows=10, nrows=165853)
tdi = pd.read_excel("TDI_ESCOLAS_2021.xlsx", skiprows=8, nrows=130112)
txr = pd.read_excel("TX_REND_ESCOLAS_2021.xlsx", skiprows=8, nrows=130129)

Assim, dispomos de sete dataframes que abrangem os dados dos arquivos do INEP: atu, dsu, had, icg, ird, tdi e txr.

Criando uma função auxiliar

Um recurso fundamental das linguagens de programação é a reutilização de código. Foi implementada uma função para exibir a estrutura dos dataframes. Essa função estará presente ao longo deste artigo, permitindo-nos rastrear as mudanças que ocorreram. Ao invocar a função, você só precisa fornecer uma lista contendo os dataframes que deseja analisar, e uma lista de nome para o cabeçalho. Além disso, adicionei um parâmetro para permitir que a função limite a quantidade mínima de linhas a serem exibidas. Isso permitirá um nivelamento, caso seja desejado.

# Função para mostrar a estrutura de colunas do dataframe
# dataframes = Lista com dataframes
# cabeçalhos = Lista com cabeçalhos
# nlinhas = Quantidade máxima de linhas
def mostrar_estrutura(dataframes, cabecalhos, nlinhas):
tabelas = []
for df, cabecalho in zip(dataframes, cabecalhos):
colunas = df.columns.tolist()
tabela = PrettyTable()
tabela.field_names = [f"DataFrame {cabecalho}"]

# Adicione as colunas até o máximo de linhas
while len(colunas) < nlinhas:
colunas.append("") # Adicione uma linha vazia

tabela.add_row(["\n".join(colunas)])
tabelas.append(tabela)

# Combina as linhas das tabelas
linha_combinada = list(zip_longest(*[str(tabela).splitlines() for tabela in tabelas], fillvalue=''))

# Imprime as tabelas lado a lado
for linha in linha_combinada:
print(' '.join(linha))

Então, podemos chamar a execução da função para visualizar a estrutura inicial de cinco dataframes, por exemplo:

# Chamando a funcão para mostrar a estrutura de 5 arquivos
mostrar_estrutura([txr, atu, had, tdi, dsu], ["TXR", "ATU", "HAD", "TDI", "DSU"], 10)
Estrutura de colunas em 5 arquivos do INEP

A estrutura original de cinco dos sete arquivos importados foi exibida. Continuaremos a fazer alterações e executaremos a função novamente à medida que progredirmos. A formatação utilizada foi possível graças ao pacote prettytable.

Trabalhando as variáveis

Como observado, há muitas variáveis que não serão utilizadas. Uma das principais finalidades do data wrangling é justamente filtrar apenas as variáveis relevantes para o modelo.

#Deixando apenas as variáveis necessárias
atu = atu.iloc[:, [1,2,4,5,6,7,8,12,25]]
dsu = dsu.iloc[:, [1,2,4,5,6,7,8,12,15]]
had = had.iloc[:, [1,2,4,5,6,7,8,12,24]]
icg = icg.iloc[:, [1,2,4,5,6,7,8,9]]
ird = ird.iloc[:, [1,2,4,5,6,7,8,9]]
tdi = tdi.iloc[:, [1,2,4,5,6,7,8,9,21]]
txr = txr.iloc[:, [1,2,4,5,6,7,8,9,21]]

Vamos proceder com a renomeação das variáveis. Em todos os arquivos, há sete colunas comuns. Para simplificar o processo, foi criada uma lista contendo esses nomes. No Python, para renomear as variáveis, é suficiente fornecer a lista com os novos nomes para a propriedade columns. Segue o procedimento adotado.

# Renomear variáveis
# Foi criada uma lista com o nome das variáveis da chave composta
colunas = ['regiao', 'uf', 'municipio', 'id_escola', 'escola', 'categoria', 'dependencia']

atu.columns = colunas + ['vr_fun_atu', 'vr_med_atu']
dsu.columns = colunas + ['vr_fun_dsu', 'vr_med_dsu']
had.columns = colunas + ['vr_fun_had', 'vr_med_had']
icg.columns = colunas + ['nivel']
ird.columns = colunas + ['vr_ird']
tdi.columns = colunas + ['vr_fun_tdi', 'vr_med_tdi']
txr.columns = colunas + ['vr_fun_txr', 'vr_med_txr']

Seguimos um padrao de nomenclatura para o ensino fundamental e médio. Utilizamos os prefixos vr_fun_ ou vr_med_ com o nome do dataframe do arquivo. Essa abordagem é significativa, pois mais adiante iremos unir todos os dataframes de entrada em um único, e essa prática facilitará a identificação da origem dos valores.

Neste estágio, podemos aproveitar novamente a função para visualizar como a estrutura foi modificada.

# Chamando a funcão para mostrar a estrutura de todos os arquivos após os ajustes
mostrar_estrutura([txr, atu, had, tdi, dsu, icg, ird], ["TXR", "ATU", "HAD", "TDI", "DSU", "ICG", "IRD"], 10)
Estrutura dos dataframes txr, atu, had, tdi e dsu

É notável que a estrutura de colunas de cada dataframe está pronta. Com isso, podemos prosseguir unindo todos os dataframes em um único, que será nomeado como df_inep.

Juntado tudo em um único dataframe

Vamos proceder com a junção de todos os dataframes em apenas um.

# Unindo os dataframes em um único (juntando tudo)
# Utilizamos a variável "dataframes" criando anteriormente com a lista de dataframes

# Realizar o join dos DataFrames (usamos a lista com as colunas da chave composta)
colunas_para_merge = colunas

# Começar com o primeiro DataFrame
df_inep = dataframes[0]

# Realizar o join dos DataFrames usando as colunas em comum como chave
for df in dataframes[1:]:
df_inep = pd.merge(df_inep, df, on=colunas_para_merge, how='inner')

Utilizamos a função mais uma vez, agora para examinar exclusivamente a estrutura do dataframe df_inep.

# Verificar a estrutura do df_inep
mostrar_estrutura([df_inep], ["DF_INEP"], 19)
Estrutura do dataframe df_inep

Agora, todos os dados e a estrutura dos sete arquivos do INEP estão consolidados em um único dataframe.

Preparando os dados

O próximo passo é realizar ajustes nos dados. Mas antes disso, faremos uma verificação da quantidade de registros no dataframe, para que possamos comparar após as alterações:

# Verificando o total de registros antes da preparação dos dados
qt_antes = len(df_inep)
print(f"Qt antes: {qt_antes} Registros")
Quantidade de observações antes de realizar ajuste nos dados

Há muitas variáveis com “ — “ (dois traços) e durante o processo de Data Wrangling é essencial garantir que não hajam esses valores e que também não tenha valores NaN. Para abordar isso, executamos dois passos. Inicialmente, substituímos os valores “ — “ por NaN. Em seguida, procedemos à remoção de todas as observações que contenham pelo menos um valor NaN, independentemente da variável em que esteja localizado.

# Remover registros que contenham "--" e NaN
#Substituir '--' por NaN para facilitar a remoção
df_inep = df_inep.replace('--', pd.NA)
# Remover as linhas que contêm pelo menos um NaN (anteriormente '--')
df_inep = df_inep.dropna()

Modelos de machine learning como clustering e PCA precisam que não hajam registros duplicados. Para alcançar isso, usamos as variáveis que atuam como identificadores únicos de registros. No trecho de código abaixo, utilizamos a função drop_duplicatescom o parâmetro keep=’first’ para assegurar que, em caso de registros duplicados, o primeiro seja mantido. A eliminação de duplicatas também é importante devido à futura junção dos dataframes.

# Remover possíveis linhas duplicadas,mantendo a primeira ocorrência
df_inep = df_inep.drop_duplicates(subset= colunas, keep='first')

Os ajustes nos dados foram concluídos. Agora, procederemos à análise da quantidade de registros antes e depois dos ajustes, e também verificaremos a inexistência de duplicados:

# Verificando o total de registros após a preparação dos dados
qt_apos = len(df_inep)
qt_removido = qt_antes - qt_apos
qt_duplicados = qt_apos - df_inep['id_escola'].nunique()
print(f"Qt. antes: {qt_antes}, Qt após: {qt_apos}, Qt removidos: {qt_removido}, Qt duplicados: {qt_duplicados}")
Análise das quantidades de observações após ajustes nos dados

Em resumo, após os ajustes nos dados, o dataframe agora contém 19.644 observações, o que representa uma redução de 85.331. Além disso, não há observações duplicadas no dataframe resultante.

O problema da falta de dados

Foram excluídos 85.331 registros devido a um problema frequente na fonte dos dados: a presença de valores faltantes. A abordagem para preparar os dataframes poderia ter sido diferente, dependendo do tipo de análise e modelo a serem aplicados.

Ao usar PCA para criar um ranking, é preferível que todas as variáveis tenham valores para todas as observações. Se houver valores faltantes, você pode considerar:

  1. Eliminar as observações com valores ausentes, o que garante que todos os dados estejam completos, mas pode resultar na perda de informações.
  2. Preencher os valores ausentes com medidas como média, mediana ou moda, mantendo todas as observações, mas introduzindo algum ruído nos dados.
  3. Usar técnicas avançadas de imputação, dependendo da natureza dos seus dados e objetivos.

A escolha depende do contexto e dos objetivos, mas é crucial avaliar o impacto das decisões na análise de PCA. Em projetos reais, a preparação dos dados requer atenção especial. A opção neste trabalho foi pela exclusão. No entanto, é essencial destacar que uma análise profunda do contexto desempenha um papel crucial na tomada de decisões adequadas durante a preparação dos dados.

Ajustando o tipo da variável

Outra ação importante é verificar os tipos de dados presentes em cada variável. Muitas vezes, é necessário ajustar os tipos de dados do dataframe para melhor refletir a natureza dos dados. Para examinar se os tipos de dados estão apropriados, procedemos com o código abaixo.

#Listando os types das colunas
df_inep.infer_objects()
df_inep.dtypes

Será necessário alterar o tipo das variáveis, por exemplo, a variável id_escola está como valor quantitativo e na verdade o correto é que ela seja qualitativa. Vamos então aproveitar e já ajustar o type para todas as variáveis.

# Defina as colunas de texto como tipo string, usaremos a lista já existente em colunas
colunas_texto = colunas + ['nivel']
df_inep[colunas_texto] = df_inep[colunas_texto].astype(str)
# Defina as demais colunas numéricas para terem duas casas decimais
colunas_numericas = [coluna for coluna in df_inep.columns if coluna not in colunas_texto]
df_inep[colunas_numericas] = df_inep[colunas_numericas].round(2).astype(float)
Como estavam e como ficaram os tipos das variáveis

Dummyzação de variáveis

Alguns modelos, como a PCA, trabalham apenas com variáveis quantitativas, pois internamente são utilizadas técnicas onde é necessário que os valores seja numéricos, por exemplo: a matriz de correlação de Pearson, cálculos de eigenvalues, eigenvectors, scores fatoriais, etc.

O indicador de complexidade da gestão escola, do INEP, é uma variável categórica com valores que vão do nível 1 ao nível 6.

Houve, então, a necessidade de realizar a dummyzação dessa variável. Não entrarei em detalhes sobre a técnica, porém, o mais relevante a saber é que a dummyzação aumenta a dimensionalidade do dataframe. No caso do indicador do INEP, que possui 6 categorias, serão adicionadas 6–1 variáveis extras, ou seja, 5 no total (porém a variável original é removida), e os valores dessas novas colunas representarão combinações de 0s e 1s.

No Python, a dummyzação se aplica da seguinte forma:

# Dummyzando a variável nivel
df_inep = pd.get_dummies(df_inep, columns=['nivel'], prefix=['d'])
# Renomeando as novas colunas
df_inep.rename(columns={
'd_Nível 2': 'nivel2',
'd_Nível 3': 'nivel3',
'd_Nível 4': 'nivel4',
'd_Nível 5': 'nivel5',
'd_Nível 6': 'nivel6'
}, inplace=True)
Antes e depois da dummyzação. 6 categorias, 5 novas colunas.

Observe que a variável nivel, que tinha 6 categorias, não existe mais e no lugar dela foram criadas 6–1 novas variáveis, ou seja 5.

Padronização das variáveis

Padronização de variáveis é a transformação de dados para que tenham média zero e desvio padrão um, facilitando a comparação entre variáveis com escalas diferentes.

No Python, padronizamos as variáveis da seguinte forma:

# Padronização das variáveis
# Atenção, pacote: from sklearn.preprocessing import StandardScaler
# Inicialize o objeto StandardScaler
scaler = StandardScaler()
# Ajuste e transforme o DataFrame (aqui usamos as duas listas que contém o que precisamos)
colunas_a_padronizar = colunas_numericas + colunas_dummies
# Padronize as colunas selecionadas
df_inep[colunas_a_padronizar] = scaler.fit_transform(df_inep[colunas_a_padronizar])
Antes e depois da Padronização. Agora as variáveis estão na mesma escala

Invertendo o valor da variável

Inverter o sinal de uma variável é útil quando o maior valor positivo não representa a realidade dos dados, pois deveria ser o menor. Essa prática corrige distorções e permite que os dados sejam interpretados de maneira mais precisa, refletindo a relação desejada.

# invertendo a coluna para negativo
df_inep["vr_fun_tdi"] = -df_inep["vr_fun_tdi"]
df_inep["vr_med_tdi"] = -df_inep["vr_med_tdi"]

Criando os dataframes para o ensino fundamental e médio

Nos dataframes para a PCA, deixaremos apenas o ‘id_escola’ como variável qualitativa, transformando-a em índice de registro. O dataframe df_inep será mantido para análise exploratória e ao final auxiliar na apresentação do ranking para as localidades.

# Criar os datraframes para aplicar a PCA para os ensinos fundamental e médio
df_fundamental = df_inep[['id_escola', 'vr_fun_atu','vr_fun_dsu','vr_fun_had','vr_ird','vr_fun_tdi','vr_fun_txr'] + colunas_dummies]
df_medio = df_inep[['id_escola', 'vr_med_atu','vr_med_dsu','vr_med_had','vr_ird','vr_med_tdi','vr_med_txr'] + colunas_dummies]

Vamos chamar novamente a função para visualizar os dataframes df_fundamental e df_medio, juntamente com o df_inep.

mostrar_estrutura([df_inep, df_fundamental, df_medio], ["DF_INEP", "DF_FUNDAMENTAL", "DF_MEDIO"], 10)
Como estão as estruturas do df_inep, df_fundamental e df_medio

Perfeito, agora podemos observar que as colunas com o prefixo “vr_fun_” estão presentes no dataframe df_fundamental, enquanto as colunas com o prefixo "vr_med_" estão no dataframe df_medio.

Transformando a coluna id_escola em índice de registro

Para aplicar a PCA é necessário que o id_escola seja transporto para ser índice de registro.

id_escola_f = df_fundamental['id_escola'].values
id_escola_m = df_medio['id_escola'].values
# Removendo a coluna
df_fundamental.drop('id_escola', axis=1, inplace=True)
df_medio.drop('id_escola', axis=1, inplace=True)
# Nomeando os registros com o id_escola
df_fundamental.index = id_escola_f
df_medio.index = id_escola_m
Antes e depois de transformar o id_escola para índice de registro

Observe que existia um índice sequencial e a coluna id_escola era apenas uma variável comum. Após a transformação o índice sequencial sumiu e este passou a ser a variável id_escola.

Revisões na Preparação dos Dados

Em meio ao processo de preparação dos dados, foi constatado que o indicador de complexidade da gestão escola não será mais incluído no dataframe. Isso pode parecer frustrante, considerando que empregamos esforços para a dummyzação dessa variável. No entanto, desfazer algo que tinha sido realizado é comum durante o processo de data wrangling, à medida que compreendemos melhor a análise que será aplicada. Nesse caso, foi constatado que esse indicador seria irrelevante para a análise. Procederemos com a remoção para finalizarmos.

# Removendo o indicador de desempenho de complexidade da gestão escola
df_fundamental = df_fundamental.drop(columns=colunas_dummies)
df_medio = df_medio.drop(columns=colunas_dummies)
df_inep = df_inep.drop(columns=colunas_dummies)

Vamos chamar novamente a execução da função para vermos como ficou os dataframes finais.

mostrar_estrutura([df_inep, df_fundamental, df_medio], ["DF_INEP", "DF_FUNDAMENTAL", "DF_MEDIO"], 6)
Resultado final dos dataframes

Para visualizar o df_fundamental e df_medio, juntamente com os dados das primeiras linhas, você pode usar o seguinte código:

#Explorando como ficaram os dataframes para o ensino fundamental e médio
dataframes = [df_fundamental, df_medio]
for idx, df in enumerate(dataframes, start=1):
print(f"DataFrame {idx}:\n")
print(df.head())
print("\n" + "="*30 + "\n")
Estrutura dos dataframes com as primeiras linhas

É recomendável liberar a memória, quando dataframes não forem mais necessários, para assim evitar o consumo excessivo de recursos. Para deletar os dataframes auxiliares, usados no processo, você pode utilizar o comando del da seguinte maneira:

# Deletando os dataframes auxiliares
del atu
del dsu
del had
del icg
del ird
del tdi
del txr
# Forçar a coleta de lixo para liberar a memória
gc.collect()

Salvar os dataframes em arquivos Excel:

# Salvar os arquivos em formato Excel
df_inep.to_excel('df_inep.xlsx', index=False)
df_fundamental.to_excel('df_fundamental.xlsx', index=False)
df_medio.to_excel('df_medio.xlsx', index=False)

Concluímos a atividade de data wrangling com a preparação dos dataframes para a análise da PCA.

Considerações finais

O processo de data wrangling apresentado aqui representa a etapa de preparação dos dados provenientes dos arquivos do INEP, preparando-os para a aplicação de uma Análise de Componentes Principais (PCA). É importante destacar que, em projetos reais, a abordagem pode variar, podendo inclusive haver um único dataframe geral. No entanto, para fins didáticos e de apresentação do processo de data wrangling, optamos por adotar essa estratégia específica.

Onde obter os dados do INEP

Código completo no GIT

O Notebook Python com o código está disponível em: https://github.com/charlesjuniorx/artigos_python_datawrangling_inep.git

Referências

Mckinney, Wes. Python for Data Analysis: Data Wrangling with Pandas, Numpy and IPython. O’Reilly. USA, 2017.

Grus, Joel. Data Science do Zero. Primeiras Regras com o Python. Rio de Janeiro. Alta Books, 2016

--

--