<jupyter_start><jupyter_text>Feature engineering Nesta fase, será realizada a feature engineering. A partir dos insights obtidos na parte de análise exploratória, serão criadas as seguintes novas features:1. **Age:** categorizar de 10 em 10 anos e utilizando árvore de decisão. Aplicar log;2. **Capital gain e loss:** criação de uma indicadora se possui ou não;3. **hours per week:** aplicar log, categorizar utilizando árvore de decisão e criar uma indicadora se trabalha acima de 40 horas ou não4. **Race:** gerar duas novas indicadoras - se é branco ou não; se é branco, negro ou outras raças5. **native country:** gerar uma nova indicadora se é dos EUA ou não.6. Para as demais categóricas aplicar label ou ordinal encoder**Nos casos 1, 2 e 3**, como foram observados alguns outliers na análise exploratória, categorizar, ao invés de simplesmente excluir essas observações, é uma forma de não perder informação. Os valores extremos poderiam, também, ser imputados, entretanto como não há uma forma de obter um entendimento mais aprofundado do negócio, este método, bem como a exclusão, poderiam incorrer em substituição/perda de valores legítimos. Nos casos específicos das features **"capital-gain"** e **"capital-loss"**, como a grande maioria dos valores são zero, optou-se por apenas indicar se a pessoa possui ou não valor diferente de zero nestas variáveis.**Nos casos 4 e 5**, como foi observado que o número de observações em uma categoria era predominante em relação às outras, resolveu-se apenas binarizar as variáveis.  Importando as libraries necessárias<jupyter_code>import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns 
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
from sklearn import tree
from sklearn.preprocessing import LabelEncoder, OrdinalEncoder

from IPython.core.interactiveshell import InteractiveShell
#‘all’, ‘last’, ‘last_expr’ or ‘none’, ‘last_expr_or_assign. Default 'last_expr'
InteractiveShell.ast_node_interactivity = "all"

pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)
pd.set_option('max_colwidth', None)<jupyter_output><empty_output><jupyter_text>Carregando as bases de treino e teste<jupyter_code>df_train = pd.read_csv('df_train_cleaned.csv')
df_train.shape
df_train.head()
df_train['income'].value_counts()
df_test = pd.read_csv('df_test_cleaned.csv')
df_test.shape
df_test.head()
df_test['income'].value_counts()<jupyter_output><empty_output><jupyter_text>Categorizando a variável "age"  Realizando a categorização da variável "age" por faixas de 10 anos<jupyter_code># Definindo uma função para categorizar em faixas de 10 anos e aplicando às bases de treino e teste
def age_group(x):
    x = int(x)
    x = abs(x)
    if( 16 < x < 31 ):
        return 1 #"17-30"
    if( 30 < x < 41 ):
        return 2 #"31-40"
    if( 40 < x < 51 ):
        return 3 #"41-50"
    if( 50 < x < 61 ):
        return 4 #"51-60"
    if( 60 < x < 71 ):
        return 5 #"61-70"
    else:
        return 6 #"Maior que 70"

df_train['age_group'] = df_train['age'].apply(age_group)
df_test['age_group'] = df_test['age'].apply(age_group)<jupyter_output><empty_output><jupyter_text>Realizando a categorização da variável "age" pelo método tree basedO método tree based utiliza uma árvore de decisão para categorizar a variável preditora. Ele possui a vantagem de levar em conta a relação da variável preditora com a variável target ao sugerir as categorias, a desvantagem é que pode sugerir algumas categorias que não fazem sentido do ponto de vista negocial, então deve ser utlizado com cautela.<jupyter_code># Definindo uma função para encontrar a profundidade ótima (número de categorias) de acordo com algumas métricas de performance.
# Lógico que neste método pode-se tentar diversas variações tanto de parâmetros a serem testados quanto métricas de performance,
# contudo isto pode incorrer em custo de processamento.

def optimal_depth(dataframe, variavel, target, depth=[]):
    score_ls = []     # aqui serão guardados os valores de roc auc
    score_std_ls = [] # aqui serão guardados os valores de desvio padrão do roc_auc
    for tree_depth in depth:
        tree_model = DecisionTreeClassifier(max_depth=tree_depth, random_state = 0)

        scores = cross_val_score(tree_model, dataframe[variavel].to_frame(),       
        dataframe[target], cv=3, scoring='roc_auc')   

        score_ls.append(np.mean(scores))

        score_std_ls.append(np.std(scores))

    temp = pd.concat([pd.Series(depth), pd.Series(score_ls), pd.Series(score_std_ls)], axis=1)
    temp.columns = ['depth', 'roc_auc_mean', 'roc_auc_std']
    return temp
# Definindo uma função para categorizar a variável de acordo com a profundidade sugerida na função acima
def tree_based_categ(df_train, df_test, variavel, target, depth=1, sufix = '_cat_tree'):
    clf = DecisionTreeClassifier(max_depth=depth, random_state = 0)
    clf.fit(df_train[variavel].to_frame(), df_train[target])
    plt.figure(figsize=(7,7))
    tree.plot_tree(clf, filled=True);
    plt.title("Decision tree para a variável "+variavel)
    plt.show()
    df_train['prob_1']=clf.predict_proba(df_train[variavel].to_frame())[:,1]
    df_test['prob_1']=clf.predict_proba(df_test[variavel].to_frame())[:,1]
        
    categorias = pd.concat([df_train.groupby(['prob_1'])[variavel].min().astype(float), df_train.groupby(['prob_1'])[variavel].max().astype(float)], axis=1).reset_index()
    cat_name = variavel+sufix
    categorias[cat_name] = categorias.index
    categorias
    
    if cat_name in df_train.columns:
        df_train = df_train.drop(columns = [cat_name])
    else:
        pass
    if cat_name in df_test.columns:
        df_test = df_test.drop(columns = [cat_name])
    else:
        pass
   
    df_train = pd.merge(df_train,
                        categorias[['prob_1',cat_name]],
                        left_on=['prob_1'],
                        right_on=['prob_1'],
                        how='left')
    df_test = pd.merge(df_test,
                        categorias[['prob_1',cat_name]],
                        left_on=['prob_1'],
                        right_on=['prob_1'],
                        how='left')
    
    df_train = df_train.drop(columns = ['prob_1'])
    df_test = df_test.drop(columns = ['prob_1'])
    display(categorias)
    df_train.groupby([cat_name])[target].count().plot.bar(rot=0)
    
    return df_train, df_test<jupyter_output><empty_output><jupyter_text>Aplicando a função para definir a melhor profundidade **(número de categorias)**. No caso abaixo, será **selecionado o depth = 3**, pois após ele não existe um ganho tão alto de auc<jupyter_code>optimal_depth(df_train, 'age', 'income', depth=[1,2,3,4,5,6])<jupyter_output><empty_output><jupyter_text>Aplicando, na base de treino, o depth ótimo selecionado e **categorizando a variável "age"** nas bases de treino e teste<jupyter_code>df_train, df_test = tree_based_categ(df_train, df_test, 'age', 'income', depth=3)<jupyter_output><empty_output><jupyter_text>**A partir dos resultados acima, resolveu-se realizar uma outra categorização na variável "age", conforme abaixo.**<jupyter_code># Definindo uma função para categorizar em faixas de 10 anos e aplicando às bases de treino e teste
def age_group_2(x):
    x = int(x)
    x = abs(x)
    if( 16 < x <= 35 ):
        return 1 #"17-35"
    if( 35 < x <= 61 ):
        return 3 #"36-61"
    else:
        return 2 #"Maior que 61"

df_train['age_group_2'] = df_train['age'].apply(age_group_2)
df_test['age_group_2'] = df_test['age'].apply(age_group_2)
df_train["age_log"] = np.log(1 + df_train['age'])
df_test["age_log"] = np.log(1 + df_test['age'])
df_train.head()<jupyter_output><empty_output><jupyter_text>Criando a variável indicadora se possui ou não capital gain e capital loss<jupyter_code># Definindo uma função para binarizar as variáveis e aplicando às bases de treino e teste
def cont2dummy(x):
    x = int(x)
    x = abs(x)
    if( x == 0 ):
        return 0
    else:
        return 1

df_train['capital_gain_dummy'] = df_train['capital-gain'].apply(cont2dummy)
df_test['capital_gain_dummy'] = df_test['capital-gain'].apply(cont2dummy)

df_train['capital_loss_dummy'] = df_train['capital-loss'].apply(cont2dummy)
df_test['capital_loss_dummy'] = df_test['capital-loss'].apply(cont2dummy)
df_train.head()<jupyter_output><empty_output><jupyter_text>Hours per week: aplicando log, categorizando com árvore de decisão e criando uma indicadora se trabalha acima de 40 horas ou não  Aplicando log<jupyter_code>df_train["Hours_per_week_log"] = np.log(1 + df_train['hours-per-week'])
df_test["Hours_per_week_log"] = np.log(1 + df_test['hours-per-week'])<jupyter_output><empty_output><jupyter_text>Realizando a categorização pelo método tree basedAplicando a função para definir a melhor profundidade **(número de categorias)**. No caso abaixo, será **selecionado o depth = 2**, pois após ele não existe um ganho tão alto de auc<jupyter_code>optimal_depth(df_train, 'hours-per-week', 'income', depth=[1,2,3,4,5,6])<jupyter_output><empty_output><jupyter_text>Aplicando, na base de treino, o depth ótimo selecionado e **categorizando a variável "hours-per-week"** nas bases de treino e teste<jupyter_code>df_train, df_test = tree_based_categ(df_train, df_test, 'hours-per-week', 'income', depth=2)<jupyter_output><empty_output><jupyter_text>Criando a indicadora se trabalha acima de 40h ou nãoO insight de criar esta feature veio após analisar que 75% das pessoas que ganham acima de 50K anuais trabalham acima de 40h semanais, enquanto que 50% das pessoas que ganham abaixo de 50K trabalham acima de 40h semanais.<jupyter_code># Definindo uma função para binarizar a variável e aplicando às bases de treino e teste
def hpw2dummy(x):
    x = int(x)
    x = abs(x)
    if( x <= 40 ):
        return 0
    else:
        return 1

df_train['hours_per_week_acima40'] = df_train['hours-per-week'].apply(hpw2dummy)
df_test['hours_per_week_acima40'] = df_test['hours-per-week'].apply(hpw2dummy)
df_train.head()<jupyter_output><empty_output><jupyter_text>Race: gerando duas novas indicadoras - se é branco ou não; se é branco, negro ou outras raças  Gerando a indicadora se é branco ou não<jupyter_code># Definindo uma função para binarizar a variável e aplicando às bases de treino e teste
def race2dummy(x):
    if( x == 'White' ):
        return 1
    else:
        return 0

df_train['race_is_white'] = df_train['race'].apply(race2dummy)
df_test['race_is_white'] = df_test['race'].apply(race2dummy)<jupyter_output><empty_output><jupyter_text>Gerando a indicadora se é branco, negro ou outras raças<jupyter_code># Definindo uma função para gerar o label para a variável e aplicando às bases de treino e teste
def race2label(x):
    if( x == 'White' ):
        return 3
    if( x == 'Black' ):
        return 2    
    else:
        return 1

df_train['race_is_white_black'] = df_train['race'].apply(race2label)
df_test['race_is_white_black'] = df_test['race'].apply(race2label)
df_train.head()<jupyter_output><empty_output><jupyter_text>Native country: gerando uma nova indicadora se é dos EUA ou não<jupyter_code># Definindo uma função para binarizar a variável e aplicando às bases de treino e teste
def country2dummy(x):
    if( x == 'United-States' ):
        return 1
    else:
        return 0

df_train['native_country_is_eua'] = df_train['native-country'].apply(country2dummy)
df_test['native_country_is_eua'] = df_test['native-country'].apply(country2dummy)
df_test.head()<jupyter_output><empty_output><jupyter_text>Encoding de variáveis categóricasA variável "education" não será codificada pois já existe a feature "education-num" que é sua representação codificada. Para as outras será aplicado o label encoder pois não se observa uma ordenação natural entre suas categorias.<jupyter_code>var2encoding = ['workclass', 'marital-status', 'occupation', 'relationship', 'sex']
df_train_enc = df_train.copy()
df_test_enc = df_test.copy()
le = LabelEncoder()
for var in var2encoding:
    df_train_enc[var]=le.fit_transform(df_train_enc[var])
    df_test_enc[var]=le.transform(df_test_enc[var])<jupyter_output><empty_output><jupyter_text>Codificando, também, a variável target. É uma boa prática codificar como 1 a categoria do evento (ganhar acima de 50K, em nosso estudo), uma vez que será nosso evento a ser estimado. Assim:<jupyter_code>def codtarget(x):
    if( x == '<=50K' ):
        return 0
    else:
        return 1
df_train_enc['income'] = df_train_enc['income'].str.replace('.','')
df_test_enc['income'] = df_test_enc['income'].str.replace('.', '')
df_train_enc['income'] = df_train_enc['income'].apply(codtarget)
df_test_enc['income'] = df_test_enc['income'].apply(codtarget)
df_test_enc.head()<jupyter_output><empty_output><jupyter_text>Excluindo variáveis que não serão testadas no desenvolvimento do modelo, seja porque não fazem sentido ou porque já foram criadas outras que a representam<jupyter_code>vars_to_drop = ['age', 'fnlwgt', 'education', 'race', 'hours-per-week', 'native-country', 'capital-gain', 'capital-loss']
df_train_fe = df_train_enc.drop(vars_to_drop, axis=1)
df_test_fe = df_test_enc.drop(vars_to_drop, axis=1)
df_train_fe.shape
df_train_fe.head()<jupyter_output><empty_output><jupyter_text>Realizando mais uma rodada de EDA com as novas features que foram criadas<jupyter_code>plt.hist(df_train_fe['age_log'])
plt.show() 
plt.hist(df_train_fe['Hours_per_week_log'])
plt.show()<jupyter_output><empty_output><jupyter_text>A partir dos resultados acima, se observa que, após a transformação logarítimica as distribuições das variáveis "age" e "hours per week" ajustaram um pouco mais, ficando mais próximas de uma distribuição normal, embora esta última ainda apresente um pouco mais de assimetria.<jupyter_code>InteractiveShell.ast_node_interactivity = "last_expr"
def graph_dist (df, variavel):
    sns.countplot(x=df[variavel], hue = df['income'], palette='Greens_r', 
                  order = df[variavel].value_counts().index)
    plt.title('Distribuição da variável alvo por '+variavel, fontsize=14)
    plt.xticks(rotation = 90)
    plt.yticks()
    plt.legend()
sns.set(rc={"figure.figsize": (15, 23)})

plt.subplot(5,2,1)
graph_dist (df_train_fe, 'age_group')

plt.subplot(5,2,2)
graph_dist (df_train_fe, 'age_cat_tree')

plt.subplot(5,2,3)
graph_dist (df_train_fe, 'age_group_2')

plt.subplot(5,2,4)
graph_dist (df_train_fe, 'capital_gain_dummy')

plt.subplot(5,2,5)
graph_dist (df_train_fe, 'capital_loss_dummy')

plt.subplot(5,2,6)
graph_dist (df_train_fe, 'hours-per-week_cat_tree')

plt.subplot(5,2,7)
graph_dist (df_train_fe, 'hours_per_week_acima40')

plt.subplot(5,2,8)
graph_dist (df_train_fe, 'race_is_white')

plt.subplot(5,2,9)
graph_dist (df_train_fe, 'race_is_white_black')

plt.subplot(5,2,10)
graph_dist (df_train_fe, 'native_country_is_eua')

plt.tight_layout()
plt.show()
def graph_perc (df, variavel):
    x,y = variavel, 'income'

    count_data = (df
                .groupby(x)[y]
                .value_counts(normalize=True)
                .mul(100)
                .rename('percentual')
                .reset_index())
    sns.barplot(x=variavel, y="percentual", hue="income", data=count_data, palette='Greens_r')
    
    plt.title('Percentual de pessoas que ganham abaixo e acima de 50K de acordo com\n as categorias da variável '+variavel, fontsize=14)
    plt.xticks(rotation = 90)
    plt.yticks()
    plt.legend()
sns.set(rc={"figure.figsize": (15, 23)})

plt.subplot(5,2,1)
graph_perc (df_train_fe, 'age_group')

plt.subplot(5,2,2)
graph_perc (df_train_fe, 'age_cat_tree')

plt.subplot(5,2,3)
graph_perc (df_train_fe, 'age_group_2')

plt.subplot(5,2,4)
graph_perc (df_train_fe, 'capital_gain_dummy')

plt.subplot(5,2,5)
graph_perc (df_train_fe, 'capital_loss_dummy')

plt.subplot(5,2,6)
graph_perc (df_train_fe, 'hours-per-week_cat_tree')

plt.subplot(5,2,7)
graph_perc (df_train_fe, 'hours_per_week_acima40')

plt.subplot(5,2,8)
graph_perc (df_train_fe, 'race_is_white')

plt.subplot(5,2,9)
graph_perc (df_train_fe, 'race_is_white_black')

plt.subplot(5,2,10)
graph_perc (df_train_fe, 'native_country_is_eua')

plt.tight_layout()
plt.show()<jupyter_output><empty_output><jupyter_text>A partir dos resultados acima, nota-se que praticamente todas as categorias apresentam mais representatividade em relação às variáveis originais. Ainda, a maioria das novas features parecem promissoras em diferenciar os grupos dos que ganham acima e abaixo de 50K anuais .  Realizando uma última visão geral nos dados e salvando para a fase de modelagem<jupyter_code>InteractiveShell.ast_node_interactivity = "all"
df_train_fe.shape
df_train_fe.head()
df_test_fe.shape
df_test_fe.head()
df_train_fe.describe()
df_test_fe.describe()
# Distribuição de eventos e não eventos nas bases de treino e teste
perc_target_train = df_train_fe.groupby(['income']).agg({'income': 'count'}).apply(lambda x:100 * x / float(x.sum()), axis=0).transpose()
print("\033[1m" + '% de eventos e não eventos na base de treino'+ "\033[0m")
perc_target_train

perc_target_test = df_test_fe.groupby(['income']).agg({'income': 'count'}).apply(lambda x:100 * x / float(x.sum()), axis=0).transpose()
print("\033[1m" +'% de eventos e não eventos na base de teste'+ "\033[0m"+ "\033[0m")
perc_target_test
df_train_fe.to_csv('df_train2modelling.csv', index=False)
df_test_fe.to_csv('df_test2modelling.csv', index=False)<jupyter_output><empty_output>
StarcoderdataJupyterStructuredCleanDedup