<jupyter_start><jupyter_text>PMR3508 - Aprendizado de Máquina e Reconhecimento de PadrõesAnálise e classificação com a base de dados [Adult](https://www.kaggle.com/c/adult-pmr3508), disponível também em [UCI Repository](https://archive.ics.uci.edu/ml/index.php).Autor: <NAME>---  Sumário- [1 Iniciatlização](inicializacao)    - [1.1 Importando pacotes necessários](importando)    - [1.2 Leitura de dados](leitura)- [2 Análise e compreensão](analise)    - [2.1 Dados numéricos](analise-num)    - [2.2 Dados categóricos](analise-cat)- [3 Preparação dos dados](prep)    - [3.1 Dados faltantes](missing)    - [3.2 Tratamento de features](features)- [4 Modelo](modelo)- [5 Conclusão](conclusao) --- 1 Inicialização 1.1 Importando pacotes necessáriosPrimeiramente, vamos instalar os pacotes seaborn e statsmodels na versão necessária para evitar problemas de compatibilidade das versões mais recentes. Em seguida, importaremos todos os pacotes que serão necessários durante as fases de análise, tratamento e treinamento.<jupyter_code>!pip install seaborn==0.9.0
!pip install statsmodels==0.10.1
import numpy as np
import pandas as pd

import seaborn as sns
import matplotlib.pyplot as plt

import sklearn
from sklearn import preprocessing as prep

from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score

%matplotlib inline<jupyter_output>/opt/conda/lib/python3.7/site-packages/statsmodels/tools/_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.
  import pandas.util.testing as tm<jupyter_text>1.2 Leitura dos dados<jupyter_code>x_columns = ['ID', 'Age', 'Workclass', 'Final Weight', 'Education', 'Education Num', 'Marital Status', 'Occupation', 'Relationship', 'Race', 'Sex', 'Capital Gain', 'Capital Loss', 'Hours per Week', 'Native Country']
y_column = ['Income']

train_data_raw = pd.read_csv('data/train_data.csv', names = (x_columns + y_column), na_values = '?', header = 0)
test_data_raw = pd.read_csv('data/test_data.csv', names = x_columns, na_values = '?', header = 0)
print(train_data_raw.shape)
train_data_raw.head()<jupyter_output>(32560, 16)<jupyter_text>--- 2 Análise e compreensãoAntes de tudo, é importante visualizar melhor os dados disponíveis para um melhor entendimento do problema. A seguir, faremos isso separadamnete para os dados numéricos e categóricos. 2.1 Dados Numéricos<jupyter_code>numeric_columns = list(train_data_raw.select_dtypes(include = np.number).columns)
train_data_raw[numeric_columns].describe()<jupyter_output><empty_output><jupyter_text>A feature ID contém apenas um número de identificação, ou seja, não é uma informação que faça sentido analisar, e, por isso, será removida durante o tratamento dos dados.Em seguida, criaremos um DataFrame apenas com os dados numéricos e o Income para melhor visualização.<jupyter_code>numeric_columns.remove('ID')

view_num_df = train_data_raw[numeric_columns].copy()
for column in numeric_columns:
    view_num_df[column] = pd.to_numeric(view_num_df[column])
    
view_num_df['Income'] = train_data_raw['Income']
pp_graph = sns.pairplot(view_num_df, hue="Income", palette="Set2", diag_kind="kde", height=2.5)<jupyter_output><empty_output><jupyter_text>Vamos primeiro analisar a diagonal principal, que fornece uma aproximação da distribuição de densidade para cada uma das variáveis numéricas. A primeira coisa a se notar é como as curvas de Final Weight são praticamente iguais. Essa variável não parece ajudar muito a diferenciar a renda das pessoas. Por outro lado, Age, Education Num e Hours per Week mostram variações mais significativas para cada classe observada, e devem ser úteis para o classificador.Analisando agora os gráficos fora da diagonal principal, pode-se perceber que a correlação entre as variáveis não parece ser muito alta, já que  todos eles parecem possuir pontos bem dispersos. Além disso, novamente os gráficos de correlação entre Age, Education Num e Hours per Week parecem fronecer limites mais claros para regiões onde o Income é predominantemente maior ou menor que 50k.Por fim, uma última observação que pode-se fazer é que, independente de outras variáveis, indivíduos com Capital Gain ou com Capital Loss nulo tendem a possuir um Income >50k.<jupyter_code>def plot_feature_frequencies(data, feature, kind='line'):
    '''
    Plot frequencie for income <=50k and >50k for a specific feature
    '''
    
    less_50 = data.loc[data['Income'] == '<=50K', feature].value_counts().rename('<=50K')
    more_50 = data.loc[data['Income'] == '>50K', feature].value_counts().rename('>50K')
    plot_data = pd.concat([less_50, more_50], axis=1)
    plot_data.plot(xlabel = feature, ylabel = 'Frequency', kind=kind)
    return plot_data<jupyter_output><empty_output><jupyter_text>Vamos plotar agora os gráficos com a frequência aboluta das 3 variáveis que pareceram mais relevante a partir da análise anterior.<jupyter_code>for feature in ['Age', 'Education Num', 'Hours per Week']:
    plot_feature_frequencies(view_num_df, feature)<jupyter_output><empty_output><jupyter_text>É possível ver claramente como a proporção de pessoas que ganham mais que 50 mil aumenta drásticamente com a idade. Entre 20 e 40 anos, há um crescimento acelerado, e a partir daí, a proporção se mantém mais estável.Em relação ao Education Number, todos os indivíduos que ganhavam mais que 50k tem nível de escolaridade maior que 8, ou seja, pelo menos um diploma no nível técnico. Além disso, apenas a partir do nível 14, ou seja, a partir do bacharelado, a proporção de indivíduos que ganahvam mais que 50k superou a dos que ganahvam menos. Logo, grau de escolaridade seria um fator importante para fazer predições em relação à renda.Por fim, em Hours per Week, percebe-se claramente que a maior parte das pessoas possui a jornada padrão de 40h por semana. Porém, quanto mais aumenta-se o número de horas trabalhadas, maior é a proporção de pessoas que ganham mais que 50k, enquanto para valores menores que 40h, a quantidade é bem pequena.  2.2 Dados categóricos<jupyter_code>categoric_columns = list(train_data_raw.select_dtypes(exclude = np.number).columns)
train_data_raw[categoric_columns].describe()<jupyter_output><empty_output><jupyter_text>Vamos remover a coluna de Education, pois a mesma análise já foi feita para Education Num, e, novamente, criaremos um novo DataFrame para facilitar a visualização.<jupyter_code>categoric_columns.remove('Education')
view_cat_df = train_data_raw[categoric_columns].copy()
plot_feature_frequencies(view_cat_df, 'Workclass', 'bar').head()<jupyter_output><empty_output><jupyter_text>Aqui pode-se perceber um leve desbalanceamento, pois a maior parte dos indicíduos trabalha no setor privado. Mesmo assim, é interessante notar como a única categoria em que a proporção dos índividuos que recebem mais que 50k é maior que os que recebem menos é a de "Incorporated self employment".<jupyter_code>plot_feature_frequencies(view_cat_df, 'Occupation', 'bar').head()<jupyter_output><empty_output><jupyter_text>Nesse exemplo, as ocupações que recebem mais pretígio social estão diretamente relacionadas com uma maior renda, como é o caso de Prof especialista e gerente executivo.<jupyter_code>plot_feature_frequencies(view_cat_df, 'Marital Status', 'bar').head()<jupyter_output><empty_output><jupyter_text>Nesse exemplo é interessante notar como o fato de estar casado e vivendo com o(a) parceiro(a) tem grande relação com a renda. A única categoria em que os indivíduos com Income maior q 50k possuem representação relevante é a de casado com parceiro civil, por isso, talvez seja interessante construir uma feature que represente exatamente essa situação.<jupyter_code>plot_feature_frequencies(view_cat_df, 'Relationship', 'bar').head()<jupyter_output><empty_output><jupyter_text>Aqui, a questão de casamento se apresenta novamente, com as proporções de renda maior que 50k bem equilibradas para maridos e esposas, enquanto para os outros tipos de relacionamento, a quantidade de indivíduos com renda alta é bem menor.<jupyter_code>plot_feature_frequencies(view_cat_df, 'Sex', 'bar').head()<jupyter_output><empty_output><jupyter_text>Com esse gráfico, a grande diferença entre a renda das mulheres e dos homens fica bem clara. Isso só comprova a diferença salarial e de oportunidades para as mulheres causada pelo machismo da sociedade contemporânea.<jupyter_code>plot_feature_frequencies(view_cat_df, 'Race', 'bar').head()<jupyter_output><empty_output><jupyter_text>Aqui, a questão do racismo fica evidente, pois pelos dados, aproximadamente 25% da população branca teria renda maior que 50k, enquanto para a população negra, esse número cai para 12%. Outro fator importante é que a população hipânica não parece ter sido representada nessa pesquisa, mesmo compondo aproximadamente 15,4% da população americana, ou seja, é a segunda etnia mais representativa dos EUA (dados de 2008).<jupyter_code>plot_feature_frequencies(view_cat_df, 'Native Country', 'bar').head()<jupyter_output><empty_output><jupyter_text>Em relação ao país de origem, há um desbalanceamento extremo, que pode ser claramente visto pelo gráfico. Dessa forma, a alternativa escolhida para lidar com esse problema será não utilizar esses dados. --- 3 Preparação dos dados 3.1 Dados faltantes<jupyter_code>def count_missing_values(data):
    '''
    Count missing values for each feature and return a sorted DataFrame with the resuls
    '''

    missing_count = []
    for column in data.columns:
        missing_count.append(data[column].isna().sum())
    missing_count = np.asarray(missing_count)
    missing_count = pd.DataFrame({'feature': data.columns, 'count': missing_count,
                                'freq. [%]': 100*missing_count/data.shape[0]}, index=None)
    missing_count.sort_values('count', ascending=False, inplace=True, ignore_index=True)
    return missing_count
display(count_missing_values(train_data_raw).head())
display(count_missing_values(test_data_raw).head())<jupyter_output><empty_output><jupyter_text>Como pode-se perceber, as únicas variáveis que possuem dados faltantes, tanto na base de treino quanto de testes, são Occupation, Workclass e Native Country. Assim, vamos usar a função a seguir para imputar valores nessas variáveis.<jupyter_code>def handle_missing_values(original_data, fill_options = None, drop_rest = False):
    '''
    Choose what to do with the missing values.
    fill_options is a dictionary where de features are keys, and the values are how to fill the missing data,
    the options are: unknown (fill with 'unknown'), mean (complete with the mean value), most_frequent (complete with most frequent value).
    The rest of the missing data will be dropped.
    '''

    data = original_data.copy()
    if fill_options is not None:
        for feature, action in fill_options.items():
            # print(feature, action)
            if feature not in data.columns:
                # print(feature)
                continue
            if action == 'unknown':
                data[feature].fillna('unknown', inplace=True)
            elif action == 'mean':
                data[feature].fillna(data[feature].mean(), inplace=True)
            elif action == 'most_frequent':
                top = data[feature].describe().top
                data[feature].fillna(top, inplace=True)
                
    data.dropna()

    return data<jupyter_output><empty_output><jupyter_text>3.2 Tratamento de featuresAqui, useremos a função que trata os dados faltantes, além de outros métodos úteis, para preparar as bases de treino e teste.Para isso, vamos selecionar as features relevantes, depois codificar as variáveis categóricas, e por fim, gerar X_test, X_train e Y_train para nosso classificador.<jupyter_code>def data_pipeline(train_raw, test_raw, fill_options = None, drop_columns = ['ID', 'Education']):
    '''
    Prepare the data to be used in the classifier
    '''

    train_data = train_raw.copy()
    test_data = test_raw.copy()

    # Remove duplicate itens from training data
    train_data.drop_duplicates(keep='first', inplace=True)
    
    # Remove unwanted columns
    if drop_columns is not None:
        train_data.drop(drop_columns, axis = 1, inplace=True)
        test_data.drop(drop_columns, axis = 1, inplace=True)

    # Handle the missing values
    train_data = handle_missing_values(train_data, fill_options, drop_rest=True)
    test_data = handle_missing_values(test_data, fill_options)

    # Separate columns types
    numeric_columns = list(test_data.select_dtypes(include = np.number).columns)
    categoric_columns = list(test_data.select_dtypes(exclude = np.number).columns)
    label_column = 'Income'

    # Apply scaler to numeric features
    scaler = prep.RobustScaler()
    scaler.fit(train_data[numeric_columns])
    train_num = scaler.transform(train_data[numeric_columns])
    test_num = scaler.transform(test_data[numeric_columns])

    # Encode the categoric feature into One Hot
    cat_encoder = prep.OneHotEncoder(sparse=False)
    cat_encoder.fit(train_data[categoric_columns])
    train_cat = cat_encoder.transform(train_data[categoric_columns])
    test_cat = cat_encoder.transform(test_data[categoric_columns])

    # Concatenate X arrays
    X_train = np.concatenate((train_num, train_cat), axis=1)
    X_test = np.concatenate((test_num, test_cat), axis=1)

    # Encode the labels
    label_encoder = prep.LabelEncoder()
    Y_train = label_encoder.fit_transform(train_data[label_column])
    
    # Make sure the test and train data have the same number of features
    assert X_train.shape[1] == X_test.shape[1]
    
    # Make sure the train data has the same number of examples in X and Y
    assert X_train.shape[0] == Y_train.shape[0]

    return X_train, Y_train, X_test, label_encoder<jupyter_output><empty_output><jupyter_text>Para gerar nossos dados em bases X e Y, precisamos primeiro escolher o método para imputar os dados faltantes. Nesse caso, utilizaremos uma nova classe: 'unknown' como classe significativa para Occupation e Workclass.Além disso, precisamos decidir quais features serão utilizadas no classificador. Já foi discutido previamente que ID e Education serão removidos. Além disso, optei por retirar Final Weight, que não tinham muita correlação com o Income, e Native Country, cujos dados estavam muito desbalanceados.<jupyter_code>fill_options = {'Occupation': 'unknown', 'Workclass': 'unknown'}
drop_columns = ['ID', 'Education', 'Final Weight', 'Native Country']
X_train, Y_train, X_test, label_encoder = data_pipeline(train_data_raw, test_data_raw, fill_options, drop_columns)
print(f'X_train tem shape: {X_train.shape}')
print(f'Y_train tem shape: {Y_train.shape}')
print(f'X_test tem shape: {X_test.shape}')<jupyter_output>X_train tem shape: (32560, 49)
Y_train tem shape: (32560,)
X_test tem shape: (16280, 49)<jupyter_text>--- 4 ModeloO modelo utilizado nas predições do Income é o KNN (K-Nearest Neighbors), estudado em sala. Para treiná-lo, é preciso fazer algumas escolhas de hiperparâmetros, sendo que duas das principais são o número de vizinhos (k), e a métrica utilizada para cálculo da distância (normalmente a \\(L_2\\) ou euclidiana)Para começar a escolha desses hiperparâmetros, testei com alguns valores de números de vizinhos com diferentes métricas. A seguir, vamos visualizar alguns resultados usando a distância de Minkowski com \\(p = 1\\), que corresponde à métrica \\(L_1\\) ou Manhattan, para vários valores de k.<jupyter_code>for k_value in range(5, 45, 5):
    clf = KNeighborsClassifier(k_value, p=1, weights='uniform', n_jobs=-1)
    score = np.mean(cross_val_score(clf, X_train, Y_train, cv=10))
    
    print(f'Para {k_value} vizinhos, a acurácia foi de {100*score:.5f}%')<jupyter_output>Para 5 vizinhos, a acurácia foi de 85.44840%
Para 10 vizinhos, a acurácia foi de 86.31143%
Para 15 vizinhos, a acurácia foi de 86.48649%
Para 20 vizinhos, a acurácia foi de 86.57248%
Para 25 vizinhos, a acurácia foi de 86.67998%
Para 30 vizinhos, a acurácia foi de 86.69226%
Para 35 vizinhos, a acurácia foi de 86.59705%
Para 40 vizinhos, a acurácia foi de 86.53563%<jupyter_text>A partir dos resultados preliminares obtidos a cima, vamos agora procurar ao redor dos valores mais promissores, e escolher o classificador cujo número de vizinhos fornece a maior acurácia.<jupyter_code>max_k = 0
max_score = 0.
knn_clf = None

for k_value in range(20, 35):
    clf = KNeighborsClassifier(k_value, p=1, weights='uniform', n_jobs=-1)
    score = np.mean(cross_val_score(clf, X_train, Y_train, cv=10))
    
    if score > max_score:
        max_score = score
        max_k = k_value
        knn_clf = clf
    
print(f'O melhor número de vizinhos encontrado foi {max_k}, com acurácia de {max_score*100:.5f}%')<jupyter_output>O melhor número de vizinhos encontrado foi 28, com acurácia de 86.72297%<jupyter_text>Utilizando o modelo selecionado anteriormente, vamos treiná-lo e fazer nossas predições.<jupyter_code>knn_clf.fit(X_train, Y_train)
Y_hat_test = knn_clf.predict(X_test)
Y_hat_test = label_encoder.inverse_transform(Y_hat_test)
result_data = pd.DataFrame({'income': Y_hat_test})
display(result_data.head())
result_data.to_csv('submission.csv', index = True, index_label = 'Id')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08430737183196389, 'fraction_numerical': 0.015507464413840991, 'mean_word_length': 5.128723404255319, 'pattern_counts': {'":': 0, '<': 84, '<?xml version=': 0, '>': 86, 'https://': 2, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '21', 'dir': 'jupyter-structured-clean-dedup', 'id': '241354', 'license': 'MIT', 'n_tokens_mistral': 6047, 'n_tokens_neox': 5687, 'n_words': 1806, 'path': 'adult_classifier.ipynb', 'repo_name': 'lucastrschneider/PMR3508-EP1', 'size': '17308'}