<jupyter_start><jupyter_text>**Entendimento do problema** O entendimento do problema é de suma importância para a construção de um bom modelo.  **Detecção de aderência em campanha de callcenter** Você foi contratado por uma empresa do ramo de Call Center para desenvolver um modelo preditivo de detecção de aderência em uma campanha feita para um determinado banco.Para esse trabalho iremos utilizar um dataset, enviado em anexo, que contém aproximadamente 41 mil linhas e 19 colunas. Esse dataset traz os registros pessoais dos clientes, como idade, profissão, educação entre outros, dados das campanha de telemarketing realizadas, como número de contatos, duração, alguns índices financeiros e a coluna aderencia_campanha, que mostra se o cliente aderiu ou não a campanha.Você deverá construir um modelo para detecção de aderência dessa campanha, percorrendo todas as etapas da construção de um modelo de machine learning e documentando os passos seguidos e resultados obtidos em cada uma das etapas do processo, até chegar a etapa final dos resultados encontrados com a predição feita em cima do modelo. **Utilização de aprendizagem supervisionada por clasificação**  **Informações iniciais**<jupyter_code>print('\033[36m\033[1m'+'.: Dataset Info :.')
print('\033[0m\033[36m*' * 20)
print('\033[0m'+'Total Rows:'+'\033[36m\033[1m', df.shape[0])
print('\033[0m'+'Total Columns:'+'\033[36m\033[1m', df.shape[1])
print('\033[0m\033[36m*' * 20)
print('\n')

print('\033[1m'+'.: Dataset Details :.')
print('\033[0m\033[36m*' * 22 +'\033[0m')
df.info(memory_usage = False)<jupyter_output>[36m[1m.: Dataset Info :.
[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*
[0mTotal Rows:[36m[1m 41188
[0mTotal Columns:[36m[1m 19
[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*


[1m.: Dataset Details :.
[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m[36m*[0m
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 41188 entries, 0 to 41187
Data columns (total 19 columns):
 #   Column                       Non-Null Count  Dtype  
---  ------                       --------------  -----  
 0   idade                    [...]<jupyter_text>**Pré-processamento e manipulação dos dados**<jupyter_code>#Ordenando e entendendo o dataset pela coluna idade
df.sample(20).sort_values(by='idade', ascending=False)<jupyter_output><empty_output><jupyter_text>A ideia inicial foi entender de um modo geral, como a idade está sendo relevante para a aderência da campanha. E percebe-se que em sua maioria para essa amostra de dados, as pessoas não aderiram a campanha, indicando um desbalanceamento dos dados, porém irei deixar para verificar o desbalanceamento depois de verificar outros problemas em relação ao conjunto de dados.<jupyter_code>df.describe()<jupyter_output><empty_output><jupyter_text>**Verificar colunas**<jupyter_code>df.columns
df.dtypes
df.dtypes.value_counts()<jupyter_output><empty_output><jupyter_text>**Alterando o tipo das colunas**<jupyter_code>df['indice_confianca_consumidor'] = df['indice_confianca_consumidor'].astype(int)
df['taxa_juros_media'] = df['taxa_juros_media'].astype(int)
df['indice_precos_consumidor'] = df['indice_precos_consumidor'].astype(int)<jupyter_output><empty_output><jupyter_text>**Dados ausentes**<jupyter_code>df.isnull().sum()<jupyter_output><empty_output><jupyter_text>Existem muitos dados nulos em relação ao tamanho do dataset nas colunas educação, profissão, inadimplente, emprestimo moradia, emprestimo pessoal e estado civil, que apesar de serem colunas categóricas, podemos realizar tranformações desses dados e precisar delas durante o estudo.  Verificado que não existe problema em apagar os valores nulos dessas colunas, tendo em vista que tais variáveis não iram impactar tanto o modelo como as outras variáveis disponíveis, em seguida poderia apagar estes dados.Todavida, irei aplicar a técnica de Simple Imputer para preencher os dados ausentes pelos valores que ocorre o maior número de vezes em uma determinada coluna. Essa abordagem é bastante interessante e aplicável para colunas categóricas, sendo este o problema dos dados ausentes.<jupyter_code>#Criando objeto para substituir os valores ausentes
preenche_por_frequencia = SimpleImputer(strategy = "most_frequent")

imputer_frequencia = preenche_por_frequencia.fit_transform(df)
df = pd.DataFrame(imputer_frequencia, columns=df.columns)
df.head()
df.isnull().sum()<jupyter_output><empty_output><jupyter_text>**Transformando colunas categóricas** As colunas que possuem linhas com sim e não, consequentemente pode-se transformar facilmente em colunas com classes binárias para utilizar no processo de modelagem, então estas colunas são:<jupyter_code>df.inadimplente.value_counts()
colunas_classe_binaria = df[['inadimplente','emprestimo_moradia', 'emprestimo_pessoal', 'aderencia_campanha']].copy()
# Definindo as classes
sim_nao_map = {'sim': 1, 'nao': 0}
# Aplicando o mapeamento ao dataset com a função map
for coluna in colunas_classe_binaria:
    df[coluna] = df[coluna].map(sim_nao_map)<jupyter_output><empty_output><jupyter_text>Por outro lado, apesar de não ter classificação dos registros como sim ou não, as colunas de meio de contato e campanha anterior, também podem ser transformar em classificação binaria e multiclassificação respectivamente.<jupyter_code>def make_contato(x):
    if x == 'telefone':
        return 0
    else: #celular
        return 1
df['meio_contato'] = df['meio_contato'].apply(make_contato)
def make_campanha_anterior(x):
    if x == 'fracasso':
        return 0
    if x == 'sucesso':
        return 1
    else: #não existente
        return 2
df['campanha_anterior'] = df['campanha_anterior'].apply(make_campanha_anterior)
df.mes.unique()<jupyter_output><empty_output><jupyter_text>As colunas categóricas dia da semana e mês que contém os dados diários e mensais também podem acabar sendo muito relevantes para este problema em questão, tendo em vista que em muito casos o dia ou mês pode ter um alto impacto para o cliente aderir uma campanha.<jupyter_code>dias_semanais = {'seg': 1, 'ter':2, 'qua':3, 'qui':4, 'sex':5}
df['dia_da_semana'] = df['dia_da_semana'].map(dias_semanais)
meses = {'mar':3, 'abr':4, 'mai': 5, 'jun':6, 'jul':7, 'ago':8, 'set': 9, 'out':10, 'nov':11, 'dez': 12}
df['mes'] = df['mes'].map(meses)
df.head()<jupyter_output><empty_output><jupyter_text>Uma outra forma interessantes de se trabalhar com variáveis categóricas é através da utilização do One-hot encoding. Com ele, uma variável categórica com  categorias é transformada em  novas variáveis binárias (0 ou 1), onde a presença do 1 (hot) significa que aquela observação pertence àquela categoria, e 0 (cold) que não pertence.  O objetivo com o one-hot encoding é transformar determinada variável categórica em uma sequência de variáveis numéricas binárias, cada uma descrevendo uma classe da variável. Algumas variáveis que podemos aplicar está técnica e seja útil para o modelo são:* estado_civil * profissão* educação Porém, está ténica só será aplicada as variáveis Educação e Profissão, para a variável estado civil não será necessário, então o próximo passo é treinar e transformar o encoder. O motivo é que possivelmente as variáveis educação e profissão tenha um impacto maior, tendo em vista que possivelmente existe uma tendência para as pessoas que tem uma educação e profissão melhores aderir mais a campanha.<jupyter_code>one_hot_encoder_educacao = OneHotEncoder(sparse=False, dtype=np.int)
df_encoded_educacao = one_hot_encoder_educacao.fit_transform(df[["educacao"]])
one_hot_encoder_profissao = OneHotEncoder(sparse=False, dtype=np.int)
df_encoded_profissao = one_hot_encoder_profissao.fit_transform(df[["profissao"]])<jupyter_output><empty_output><jupyter_text>Através do atributo categories_ do encoder, pode-se visualizar como ficou as categorias de cada variável que foi transformada.<jupyter_code>one_hot_encoder_educacao.categories_
one_hot_encoder_profissao.categories_<jupyter_output><empty_output><jupyter_text>Agora posso criar as novas colunas que descrevem cada categoria. Repare que, para qualquer linha, apenas uma das colunas contém um 1, indicando a qual categoria aquela observação pertence. Isso acontece, obviamente, se as categorias forem mutuamente exclusivas (uma observação não pode pertencer a mais de uma categoria simultaneamente).<jupyter_code>columns_encoded_educacao = one_hot_encoder_educacao.categories_[0]

df = pd.concat([df, pd.DataFrame(df_encoded_educacao, columns=columns_encoded_educacao)], axis=1)

df[['analfabeto','curso_tecnico','ensino_medio','fundamental_4a','fundamental_6a','fundamental_9a','graduacao_completa']].head(10)
columns_encoded_profissao = one_hot_encoder_profissao.categories_[0]

df = pd.concat([df, pd.DataFrame(df_encoded_profissao, columns=columns_encoded_profissao)], axis=1)
df[['admin.','aposentado','colarinho_azul','desempregado','dona_casa','empreendedor','estudante','gerente','informal','servicos','tecnico']].head()<jupyter_output><empty_output><jupyter_text>A maior parte da matriz retornada é composta por zeros, sendo apenas alguns elementos compostos de um.Dizemos que essa matriz é esparsa. O que pode acabar sendo um grande desperdício de memória trabalhar diretamente como uma matriz esparsa assim. Por isso, existe o default do OneHotEncoder que retorna uma sparse matrix do NumPy, economizando espaço em memória, mas para este problema foi decidido não aplicar.  **Como os dados estão distribuidos?**  **Teste de shapiro-wilk** O teste de Shapiro-Wilk é um teste de aderência à distribuição normal, que é abreviado para teste de normalidade. O intuito será verificar se algumas variáveis númericas vem ou não de uma distribuição normal. A hipótese nula, é a normalidade dos dados. Se o valor-p for menor que o nível de significância , então temos evidências de que os dados não vêm de uma distribuição normal. Se o valor-p for maior que alpha, então não podemos afimar que os dados não vêm de uma distribuição normal (o que é sutilmente diferente de afirmar que eles vêm de uma distribuição normal). Irei aplicar o teste de shapiro wilk e também verificar a distribuição dos dados para as colunas númericas que considerei importantes para o problema e consequentemente o desempenho do modelo.<jupyter_code>def teste_shapiro_wilk(sample):
    # Retorne aqui o resultado da questão 1.

    # Teste de normalidade:
    stat, p = sct.shapiro(sample)
    print(f'statistics:{stat}, p-value:{p}')

    # Interpretação:
    alpha = 0.05
    if p > alpha:
        return True
    else:
        return False
    
df.head()
teste_shapiro_wilk(df['indice_precos_consumidor'])
teste_shapiro_wilk(df['indice_confianca_consumidor'])
teste_shapiro_wilk(df['taxa_juros_media'])
sns.distplot(df['taxa_juros_media'], bins=25)
sns.distplot(df['indice_precos_consumidor'], bins=25)
sns.distplot(df['indice_confianca_consumidor'], bins=25)<jupyter_output><empty_output><jupyter_text>*Notas** É possível observar que a média da taxa de juros das pessoas está mais concentrada em 4 e não está tão distribuida.  Apesar do histograma apresentar uma distribuição que aparenta os dados está normalmente distribuidos. O teste de Shapiro-Wilk diz que os dados não são normalmente distruibuidos. Isso significa que é possivel de p-value ser maior que alpha, mostrando que há evidências de que a amostra não se trata de uma distruibição normal.  **Normalizar ou padronizar os dados?** Normalmente usamos a normalização quando você não sabe a distribuição dos dados, ou sabe que não é uma gaussiana, e é útil em algoritmos que não fazem suposições sobre a distribuição, como KNN ou redes neurais. Já a padronização a gente usa quando sabemos que a distribuição dos dados tem uma distribuição gaussiana, ou muito parecido com a curva da gaussiana. Normalização é o processo de colocar uma variável numérica em uma escala [0,1] pré-determinada, geralmente , mas também é comum ser [-1,1].Para colocar no intervalo [0,1], basta subtrair cada valor do valor mínimo e dividir pela diferença do valor máximo e mínimo: Neste caso iremos normalizar os dados da coluna X que não estão distribuidos normalmente. Tendo em vista que são colunas númericas importantes para o problema em questão.<jupyter_code>minmax_scaler = MinMaxScaler(feature_range=(0, 1))
standard_scaler = StandardScaler()
df['taxa_juros_media_normalizado'] = minmax_scaler.fit_transform(df[['taxa_juros_media']])
df['indice_precos_consumidor_normalizado'] = minmax_scaler.fit_transform(df[['indice_precos_consumidor']])
df['indice_confianca_consumidor_normalizado'] = minmax_scaler.fit_transform(df[['indice_confianca_consumidor']])
print('Taxa de juros media normalizada: ', df['taxa_juros_media_normalizado'].min(), df['taxa_juros_media_normalizado'].max())
print('Indice de preços de consumidor normalizado: ', df['indice_precos_consumidor_normalizado'].min(), df['indice_precos_consumidor_normalizado'].max())
print('Indice de confiança de consumidor normalizado: ', df['indice_confianca_consumidor_normalizado'].min(), df['indice_confianca_consumidor_normalizado'].max())
sns.distplot(df['taxa_juros_media_normalizado'], bins=25)
sns.distplot(df['indice_precos_consumidor_normalizado'], bins=25)
sns.distplot(df['indice_confianca_consumidor_normalizado'], bins=25)<jupyter_output><empty_output><jupyter_text>Se comparar com as distribuições anteriores, é possível notar uma pequena diferença na distribuição dos dados.  **Outliers** Devido a algumas concatenações o tipo de dados acabou alterando, então decidi alterar o tipo das colunas novamente, apesar de não ser tão necessário.<jupyter_code>df['indice_confianca_consumidor'] = df['indice_confianca_consumidor'].astype(int)
df['taxa_juros_media'] = df['taxa_juros_media'].astype(int)
df['indice_precos_consumidor'] = df['indice_precos_consumidor'].astype(int)

df['dias_ultimo_contato'] = df['dias_ultimo_contato'].astype(int)
df['qtd_contatos_campanha'] = df['qtd_contatos_campanha'].astype(int)
df['qtd_contatos_total'] = df['qtd_contatos_total'].astype(int)

df['idade'] = df['idade'].astype(int)
df.describe()<jupyter_output><empty_output><jupyter_text>Irei continuar a investigação para as mesmas variáveis númericas que foi verificado como os dados estavam distribuidos.<jupyter_code>#Plotando gráfico de boxplot para o lado azul
sns.boxplot(x=df["taxa_juros_media_normalizado"], orient="vertical");
sns.boxplot(x=df["indice_confianca_consumidor_normalizado"], orient="vertical");
sns.boxplot(x=df["indice_precos_consumidor_normalizado"], orient="vertical");<jupyter_output><empty_output><jupyter_text>Como estas variáveis através da interpretação do boxplot não aparenta ter outliers, seguiremos para a próxima etapa. Outras variáveis não foram avaliadas pois serão descartadas.   **Dados desbalanceados** Para finalizar esta etapa de tratamento dos dados, eu poderia deletar algumas colunas, com o objetivo de trabalhar com mais facilidade e diminuir o tamanho do conjunto de dados, entretanto, acredito que todas elas serão muito utéis tanto para a análise exploratória, como o desenvolvimento do modelo. Também irei definir inicialmente as variáveis de entrada e a variável de saída, mas essas variáveis podem ser alteradas na etapa de seleção de variáveis e engenharia de recursos. O objetivo é apenas ter uma divisão inicial e corrigir problemas de desbalanceamento dos dados.<jupyter_code>X = df.drop(['profissao', 'estado_civil', 'educacao'],1) #A variável targe por enquanto vai permanecer nos dados de entrada para usarmos na análise exploratória
y = df["aderencia_campanha"]
df['aderencia_campanha'].value_counts()
aderencia_distribuicao = df['aderencia_campanha'].value_counts()
# plotar gráfico de waffle
fig = plt.figure(
                FigureClass=Waffle,
                rows=5,
                columns=10,
                colors=("#000000", "#983D3D"),
                values=aderencia_distribuicao,
                title={
                'label': 'Aderência da campanha',
                'loc': 'left',
                'fontdict': {
                'fontsize': 15
                }},
                icons='child',
                font_size=50,
                legend={
                'loc': 'lower left',
                'bbox_to_anchor': (-0.04, -0.2),
                'ncol': len(aderencia_distribuicao),
                'framealpha': 0,
                'fontsize': 15
                },
                labels=['Cliente não aderiu (88%)', 'Cliente aderiu (11%)'],
                icon_legend=True, 
                figsize=(15, 5)
)
plt.tight_layout()
plt.show()<jupyter_output><empty_output><jupyter_text>Nota-se então que os dados estão bastante desbalanceados, o que acaba influenciando negativamente o modelo para um lado, que seria os dados pessoas que não aderiram a campanha, por esse motivo existem algumas ténicas de balanceamento de dados.  **Smote** SMOTE é uma das técnicas mais populares de reamostragem em casos de classes desbalanceadas. Trata-se de uma técnica de oversampling que adiciona dados sintéticos à classe minitoritária através de uma heurística relativamente simples. O SMOTE pode ser pensado como um oversampling aleatório adicionado de uma perturbação direcionada. Isso evita os problemas de overfitting existentes no oversampling aleatório simples.Esse algoritmo não está disponível por padrão no sklearn, mas pode ser encontrado nos seus projetos de contribuição na biblioteca imblearn.<jupyter_code>from imblearn.over_sampling import SMOTE
from sklearn.decomposition import PCA<jupyter_output><empty_output><jupyter_text>Para definir o X e Y irei descartar apenas as variáveis de entrada que acredito não serem tão relevantes para o problema. E a variável de saída será a classe binária da coluna aderencia da campanha.<jupyter_code>smote = SMOTE(sampling_strategy="minority")

X_smote, y_smote = smote.fit_resample(X, y)
df.shape
X_smote.shape<jupyter_output><empty_output><jupyter_text>Dessa forma a quantidade dados positivos e negativos se torna igual:<jupyter_code>sum(y_smote == True)/sum(y_smote == False)
#SALVANDO BASE DE DADOS FINAL
df.to_csv("dados_tratados.csv",index= False)
X_smote.to_csv("X_dados_balanceados.csv",index= False)
y_smote.to_csv('y.csv', index=False)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10052002358869887, 'fraction_numerical': 0.02149788237816973, 'mean_word_length': 5.83046503112413, 'pattern_counts': {'":': 0, '<': 85, '<?xml version=': 0, '>': 86, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, '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': '504138', 'license': 'MIT', 'n_tokens_mistral': 7062, 'n_tokens_neox': 6552, 'n_words': 1827, 'path': 'Case interview/processamento_dos_dados.ipynb', 'repo_name': 'luislauriano/Data-Science', 'size': '18653'}