{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":50160,"databundleVersionId":7921029}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Mineração de Dados Aplicada: Estabilidade Temporal em Modelos de Risco de Crédito\n\n**Universidade Federal de Ouro Preto (UFOP)**\n**Projeto:** Pipeline KDD para Previsão de Inadimplência (Home Credit)\n**Referência:** Suhadolnik et al. (2023) – *Machine Learning for Enhanced Credit Risk Assessment*\n\n---\n\n## Problema de negócio\n\nA concessão de crédito é central para a saúde financeira de instituições. O desafio “Home Credit – Credit Risk Model Stability” consiste em prever a probabilidade de inadimplência a partir de um grande conjunto de dados relacionais (histórico de pagamentos, empréstimos anteriores e dados de birôs).\n\n---\n\n## Complicação\n\nDiferente de cenários acadêmicos estáticos, o mercado muda. Modelos com bom desempenho hoje podem degradar rapidamente devido a fatores como inflação, crises ou mudanças de comportamento. Esse efeito é conhecido como **concept drift**.\n\nO problema, portanto, não é apenas prever inadimplência, mas manter a performance do modelo ao longo do tempo.\n\n---\n\n## Solução proposta \n\nPara lidar com o *concept drift* e com o volume de dados (~27GB), será implementado um pipeline baseado no processo de KDD:\n\n1. **Ingestão e limpeza**\n   Tratamento de valores nulos e outliers, com otimização de memória via *downcasting*.\n\n2. **Extração de padrões**\n   Identificação de comportamentos recorrentes e relações relevantes no histórico de crédito.\n\n3. **Modelagem e avaliação**\n   Uso de algoritmos de *gradient boosting* e monitoramento da estabilidade temporal (ex.: índice Gini).\n\n## Estratégia de integração \n\nNesta fase inicial, usamos duas tabelas:\n\nBase de treino (train_base): define o alvo e os clientes\nDados estáticos (train_static): descrevem o perfil do cliente\n\nA escolha é intencional: como ambas têm relação 1:1, dá para explorar os dados com segurança, sem risco de inflar o dataset.\n\nPróximo passo:\nAs tabelas de histórico (com relação 1:N) entram depois. Antes disso, elas precisam passar por agregações.\n","metadata":{}},{"cell_type":"code","source":"# 1a\n\nimport pandas as pd\nimport numpy as np\nimport warnings\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nwarnings.filterwarnings('ignore')\n\nLIMIT_ROWS = 50000  \n\n\nprint(\"Iniciando extração...\")\ndf_base = pd.read_csv('/kaggle/input/competitions/home-credit-credit-risk-model-stability/csv_files/train/train_base.csv', nrows=LIMIT_ROWS)\ndf_static = pd.read_csv('/kaggle/input/competitions/home-credit-credit-risk-model-stability/csv_files/train/train_static_0_0.csv', nrows=LIMIT_ROWS)\n\n# Unificação\ndf_treino = df_base.merge(df_static, on='case_id', how='left')\n\n# Liberação imediata de lixo na memória\ndel df_base, df_static\nimport gc\ngc.collect()\n\n# Downcasting\nprint(f\"Uso de memória antes do downcast: {df_treino.memory_usage().sum() / 1024**2:.2f} MB\")\n\ndef otimizar_tipagem(df):\n    for col in df.columns:\n        col_type = df[col].dtype\n        if col_type == 'float64':\n            df[col] = df[col].astype('float32')\n        elif col_type == 'int64':\n            df[col] = df[col].astype('int32')\n        elif col_type == 'object' and df[col].nunique() < 50:\n            df[col] = df[col].astype('category')\n    return df\n\ndf_treino = otimizar_tipagem(df_treino)\nprint(f\"Uso de memória após downcast: {df_treino.memory_usage().sum() / 1024**2:.2f} MB\")\nprint(f\"Dimensões do Laboratório: {df_treino.shape}\")\n\nimport pandas as pd\n\n# Carrega o dicionário\ndf_bussola = pd.read_csv('/kaggle/input/competitions/home-credit-credit-risk-model-stability/feature_definitions.csv')\n\ndef buscar_coluna(termo):\n    \"\"\"Busca atributos pela descrição ou pelo nome.\"\"\"\n    resultado = df_bussola[\n        df_bussola['Description'].str.contains(termo, case=False, na=False) |\n        df_bussola['Variable'].str.contains(termo, case=False, na=False)\n    ]\n    pd.set_option('display.max_colwidth', None) # Mostra a descrição inteira\n    display(resultado[['Variable', 'Description']])\n\nprint(\"--- Buscando por 'income' (Renda) ---\")\nbuscar_coluna('income')\n\nprint(\"\\n--- Buscando por 'age' ou 'birth' (Idade/Nascimento) ---\")\nbuscar_coluna('birth')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Cruzamento\nmapeamento_colunas = pd.DataFrame({'Variable': df_treino.columns})\nmapa_geral = mapeamento_colunas.merge(df_bussola, on='Variable', how='left')\n\n# 2. Captura de Metadados \nmapa_geral['Tipo'] = [df_treino[col].dtype for col in mapa_geral['Variable']]\nmapa_geral['Nulos (%)'] = [(df_treino[col].isnull().sum() / len(df_treino)) * 100 for col in mapa_geral['Variable']]\n\n# 3. Identificação de ausentes\nLIMITE_NULOS = 30.0\ncolunas_densas = mapa_geral[mapa_geral['Nulos (%)'] < LIMITE_NULOS]['Variable'].tolist()\ndf_e = df_treino[colunas_densas]\n\nprint(f\"--- FILTRO DE DENSIDADE APLICADO ---\")\nprint(f\"Colunas originais: {len(df_treino.columns)}\")\nprint(f\"Colunas selecionadas (< {LIMITE_NULOS}% nulos): {len(colunas_densas)}\")\nprint(f\"Memória do DF de Elite: {df_e.memory_usage().sum() / 1024**2:.2f} MB\")\n\n# Exibe o mapa filtrado para consulta rápida\ndisplay(mapa_geral[mapa_geral['Variable'].isin(colunas_densas)].sort_values(by='Nulos (%)'))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"classificacao_tecnica = []\n\nfor col in df_e.columns:\n    if col == 'target':\n        classe = 'Binário Assimétrico'\n    elif df_e[col].dtype == 'category' or df_e[col].dtype == 'object':\n        classe = 'Qualitativo Nominal'\n    elif df_e[col].nunique() < 20: # Heurística para discretos\n        classe = 'Quantitativo Discreto'\n    else:\n        classe = 'Quantitativo Contínuo'\n    \n    classificacao_tecnica.append({'Variável': col, 'Classe': classe})\n\ndf_apendice_1a = pd.DataFrame(classificacao_tecnica)\nprint(f\"Total de atributos classificados: {len(df_apendice_1a)}\")\ndisplay(df_apendice_1a)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Estatísticas e Outliers - 1.b\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# Seleção númerica - valores de crédito, anuidade e contagens\ncolunas_numericas = ['credamount_770A', 'annuity_780A', 'applicationcnt_361L', 'currdebt_22A', 'totaldebt_9A']\ncols_existentes = [col for col in colunas_numericas if col in df_e.columns]\n\n# Tabela Estatística \nprint(\"--- Estatísticas Descritivas e Dispersão ---\")\nestatisticas = df_e[cols_existentes].describe().T\n\n# Intervalo \nestatisticas['intervalo (range)'] = estatisticas['max'] - estatisticas['min'] \n\ntabela_final = estatisticas[['min', 'max', 'intervalo (range)', 'mean', 'std']]\ndisplay(tabela_final.round(2))\n\n# Visualização \nplt.figure(figsize=(16, 5))\nfor i, col in enumerate(cols_existentes):\n    plt.subplot(1, len(cols_existentes), i+1)\n    # Boxplot com formatação para destacar as anomalias\n    sns.boxplot(y=df_e[col], color='lightcoral', fliersize=4, linewidth=1.5)\n    \n    # Limpeza visual dos nomes das colunas\n    titulo = col.split('_')[0].upper()\n    plt.title(f\"{titulo}\", fontsize=11, fontweight='bold')\n    plt.ylabel(\"Valor\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Análise de Outliers - A Assimetria do Dinheiro\n\nA tabela descritiva e os boxplots revelam uma característica fundamental do domínio financeiro: **Assimetria extrema e alta densidade de valores aberrantes (outliers).**\n\n1. **Distorção da Média:** Em atributos como a dívida total (`totaldebt_9A`) e valor do crédito (`credamount_770A`), observamos que a caixa do boxplot (onde 50% dos dados se concentram) está achatada na base, enquanto uma cauda longa de pontos se estende para cima. Isso indica clientes com volumes financeiros ordens de grandeza maiores que o cliente comum.\n2. **Impacto no Desvio-Padrão:** O desvio-padrão (`std`) de quase todas as variáveis é excepcionalmente alto em relação à média (`mean`), o que comprova uma alta variância causada pelos *outliers*.\n3. **Decisão Arquitetural para a Fase 2:** Devido a esses valores aberrantes, é um erro matemático utilizar a Média para preencher valores ausentes nessas colunas, pois ela está severamente \"puxada\" para cima pelos outliers. Na etapa de transformação, adotaremos a **Mediana** para imputação e técnicas de **Suavização (qcut)** ou **Robust Scaler** para que esses extremos não destruam o gradiente do modelo preditivo[cite: 2].","metadata":{}},{"cell_type":"code","source":"# Distribuição de Probabilidade - 1.c\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# Seleciona o target e algumas variáveis nominais representativas\ncolunas_cat = ['target', 'credtype_322L', 'disbursementtype_67L', 'inittransactioncode_186L']\ncols_cat_existentes = [col for col in colunas_cat if col in df_e.columns]\n\nprint(\"--- Tabela de Valores Possíveis e Probabilidade (%) ---\")\nfor col in cols_cat_existentes:\n    # Calcula a probabilidade (frequência relativa)\n    probabilidade = df_e[col].value_counts(normalize=True) * 100\n    \n    print(f\"\\nVariável: {col.upper()}\")\n    display(probabilidade.round(2).to_frame(name='Popularidade (%)'))\n\n# Visualização\nfig, axes = plt.subplots(1, len(cols_cat_existentes), figsize=(18, 4))\n\nfor i, col in enumerate(cols_cat_existentes):\n    prob = df_e[col].value_counts(normalize=True) * 100\n    sns.barplot(x=prob.index, y=prob.values, ax=axes[i], palette=\"viridis\")\n    \n    titulo = col.split('_')[0].upper()\n    axes[i].set_title(f\"{titulo}\", fontsize=11, fontweight='bold')\n    axes[i].set_ylabel(\"Probabilidade (%)\")\n    axes[i].tick_params(axis='x', rotation=45)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Variáveis categóricas\n\nA análise das distribuições categóricas revela padrões importantes para a modelagem:\n\n1. **Desbalanceamento do target**\n   A variável `target` é fortemente desbalanceada, com predominância da classe 0. Isso exige cuidado na escolha de métricas e possível tratamento de balanceamento.\n\n2. **Forte dominância de categorias**\n   Variáveis como `credtype` (existe um tipo de crédito predominante), `disbursementtype` (há um padrão principal de liberação de crédito) e `inittransactioncode` (existe um canal inicial padrão de transação) apresentam uma categoria dominante, concentrando a maior parte dos registros, enquanto as demais são pouco representativas.\n\n3. **Baixa diversidade categórica efetiva**\n   Apesar de múltiplas categorias, na prática o dataset é governado por poucos padrões operacionais, o que pode limitar o poder discriminativo dessas variáveis.\n\n4. **Implicações para a próxima fase**\n   O uso direto de *one-hot encoding* pode gerar colunas pouco informativas. Estratégias como agrupamento de categorias raras (`min_frequency`) ou encodings mais robustos devem ser consideradas para melhorar a estabilidade do modelo.\n","metadata":{}},{"cell_type":"code","source":"# Fase 1.d\n\n# 1. Valores ausentes no dataframe \nprint(\"--- Valores Ausentes (NaN) ---\")\nnulos_elite = df_e.isnull().sum()\nnulos_perc = (nulos_elite / len(df_e)) * 100\n\nresumo_nulos = pd.DataFrame({\n    'Total Ausentes': nulos_elite,\n    'Percentual (%)': nulos_perc\n})\n\n# Apenas colunas que possuem algum valor nulo\ncolunas_com_nulos = resumo_nulos[resumo_nulos['Total Ausentes'] > 0].sort_values(by='Percentual (%)', ascending=False)\ndisplay(colunas_com_nulos)\n\n# Visualização \nif not colunas_com_nulos.empty:\n    plt.figure(figsize=(12, 4))\n    sns.barplot(x=colunas_com_nulos.index[:15], y=colunas_com_nulos['Percentual (%)'][:15], color='salmon')\n    plt.xticks(rotation=45, ha='right')\n    plt.title('Top 15 Colunas com Maior Taxa de Ausentes', fontsize=12)\n    plt.ylabel('% Ausentes')\n    plt.tight_layout()\n    plt.show()\n\nprint(\"\\n--- Inconsistências Lógicas ---\")\n\ninconsistencias_encontradas = False\n\n# Heurística A: Ninguém pode ter anuidade ou crédito negativo\ncolunas_financeiras = ['credamount_770A', 'annuity_780A', 'currdebt_22A', 'totaldebt_9A']\nfor col in colunas_financeiras:\n    if col in df_e.columns:\n        negativos = df_e[df_e[col] < 0]\n        if not negativos.empty:\n            print(f\"ALERTA INCONSISTÊNCIA: {len(negativos)} clientes com {col} negativo.\")\n            inconsistencias_encontradas = True\n\n# Heurística B: Número de aplicações não pode ser negativo\nif 'applicationcnt_361L' in df_e.columns:\n    apps_negativas = df_e[df_e['applicationcnt_361L'] < 0]\n    if not apps_negativas.empty:\n        print(f\"ALERTA INCONSISTÊNCIA: {len(apps_negativas)} clientes com contagem de aplicações negativa.\")\n        inconsistencias_encontradas = True\n\nif not inconsistencias_encontradas:\n    print(\"Nenhuma inconsistência detectada nas colunas.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Comentário sobre os resultados (qualidade dos dados)\n\nA análise de qualidade aponta um cenário relativamente controlado, mas com alguns cuidados importantes:\n\n1. **Perfil de ausência**\n   As variáveis analisadas têm cerca de 10–30% de valores nulos. Como já filtramos colunas com mais de 30%, não há um problema crítico de falta de dados.\n\n2. **Natureza dos nulos**\n   A presença de blocos consistentes de valores ausentes sugere que os dados podem não estar faltando ao acaso. Em outras palavras, a ausência pode carregar informação (ex.: cliente sem histórico). Por isso, imputar valores de forma ingênua pode introduzir viés.\n\n3. **Consistência dos dados**\n   Não foram encontradas inconsistências evidentes nas variáveis contínuas (como valores negativos onde não faria sentido). Isso indica que a tabela `train_static_0_0` é confiável para uso inicial.","metadata":{}},{"cell_type":"code","source":"# Matriz de Correlação - 1.e\nimport numpy as np\n\n# Selecionamos as colunas numéricas\ncolunas_correlacao = ['credamount_770A', 'annuity_780A', 'currdebt_22A', 'totaldebt_9A', 'applicationcnt_361L']\ncols_corr_existentes = [col for col in colunas_correlacao if col in df_e.columns]\n\n# Correlação de Spearman\nmatriz_corr = df_e[cols_corr_existentes].corr(method='spearman')\n\n# Visualização\nplt.figure(figsize=(8, 6))\n\n# Evita dados duplicados visualmente\nmask = np.triu(np.ones_like(matriz_corr, dtype=bool))\n\nsns.heatmap(\n    matriz_corr, \n    mask=mask, \n    annot=True, \n    fmt=\".2f\", \n    cmap='coolwarm', \n    vmax=1, \n    vmin=-1, \n    linewidths=0.5,\n    cbar_kws={\"shrink\": .8}\n)\n\nplt.title('Matriz de Correlação de Spearman (Núcleo Financeiro)', fontsize=14, pad=20)\nplt.xticks(rotation=45, ha='right')\nplt.yticks(rotation=0)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## Correlação\n\nA matriz de correlação de Spearman ajuda a entender como as variáveis se relacionam e orienta algumas decisões para a próxima etapa:\n\n1. **Relações esperadas**\n   Há uma correlação forte entre `credamount_770A` (valor do crédito) e `annuity_780A` (anuidade). Isso é natural: empréstimos maiores tendem a gerar parcelas maiores. As duas variáveis são relevantes e fazem sentido no modelo.\n\n2. **Redundância entre variáveis**\n   `currdebt_22A` (dívida atual) e `totaldebt_9A` (dívida total) têm correlação praticamente perfeita. Na prática, estão carregando a mesma informação.\n\n3. **Implicações para a modelagem**\n   Manter variáveis redundantes não agrega valor e pode até atrapalhar (aumenta a dimensionalidade e pode gerar instabilidade em alguns modelos).\n   A ideia é remover uma delas — por exemplo, `currdebt_22A` — e manter apenas `totaldebt_9A` como representação da dívida.","metadata":{}},{"cell_type":"code","source":"# Imputação de Ausentes - 2.a\nfrom sklearn.impute import SimpleImputer\nimport numpy as np\n\nprint(\"--- Iniciando Imputação (O Valor Mais Provável) ---\")\n\n# Isolando as variáveis de metadados que não devem ser imputadas\nmetadados = ['case_id', 'target', 'MONTH', 'WEEK_NUM', 'date_decision']\ncolunas_operacionais = [col for col in df_e.columns if col not in metadados]\n\n# 2. Separando as colunas por tipo\nnum_cols = df_e[colunas_operacionais].select_dtypes(include=[np.number]).columns.tolist()\ncat_cols = df_e[colunas_operacionais].select_dtypes(exclude=[np.number]).columns.tolist()\n\nprint(f\"Colunas Numéricas: {len(num_cols)}\")\nprint(f\"Colunas Categóricas: {len(cat_cols)}\")\n\n# 3. Instanciando os algoritmos de Imputação\nimputer_numerico = SimpleImputer(strategy='median')\nimputer_categorico = SimpleImputer(strategy='most_frequent')\n\n# Criando uma cópia\ndf_limpo = df_e.copy()\n\nif len(num_cols) > 0:\n    df_limpo[num_cols] = imputer_numerico.fit_transform(df_limpo[num_cols])\n\nif len(cat_cols) > 0:\n    df_limpo[cat_cols] = imputer_categorico.fit_transform(df_limpo[cat_cols])\n\n# Verificação \nnulos_antes = df_e[colunas_operacionais].isnull().sum().sum()\nnulos_depois = df_limpo[colunas_operacionais].isnull().sum().sum()\n\nprint(f\"\\nTotal de valores nulos ANTES: {nulos_antes}\")\nprint(f\"Total de valores nulos DEPOIS: {nulos_depois} (Base 100% preenchida)\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Valores ausentes\n\nNós preenchemos toda a base usando a Mediana (para ignorar os outliers detectados lá na Fase 1b) e a Moda para textos.","metadata":{}},{"cell_type":"code","source":"# Suavização e Binarização - 2.b\nfrom sklearn.preprocessing import KBinsDiscretizer\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nprint(\"--- Iniciando Tratamento de Ruídos (Discretização e Binarização) ---\")\n\n# Variáveis que permitem divisão \ncols_discretizar = ['credamount_770A', 'annuity_780A']\n# Variável com excesso de zeros \ncol_binaria = 'totaldebt_9A'\n\ndf_suavizado = df_limpo.copy()\n\n# Discretização\nsuavizador = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='quantile')\ndf_suavizado[cols_discretizar] = suavizador.fit_transform(df_suavizado[cols_discretizar])\n\n# Binarização\n# 1 (Tem dívida) se for > 0, caso contrário 0 (Não tem dívida)\ndf_suavizado[col_binaria] = (df_suavizado[col_binaria] > 0).astype(float)\n\nprint(f\"Suavização concluída para: {cols_discretizar}\")\nprint(f\"Binarização concluída para: {col_binaria}\")\n\nplt.figure(figsize=(15, 4))\nfor i, col in enumerate(cols_discretizar + [col_binaria]):\n    plt.subplot(1, 3, i+1)\n    sns.countplot(x=df_suavizado[col], palette='viridis')\n    \n    titulo = \"BINARIZADO: \" if col == col_binaria else \"BINS: \"\n    plt.title(f\"{titulo}{col.split('_')[0].upper()}\", fontsize=10, fontweight='bold')\n    plt.xlabel(\"Estado / Faixa\")\n    plt.ylabel(\"Qtd. Clientes\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Tratamento \n\nA adoção de estratégias diferentes (discretização e binarização) reflete melhor a natureza das variáveis financeiras:\n\n**Redução do impacto de outliers (`CREDAMOUNT` e `ANNUITY` (essa abordagem garante que cada bin possua a mesma probabilidade))**\nVariáveis com alta dispersão foram convertidas em faixas ordinais (0 a 4).\nIsso reduz a influência de valores extremos sem perder a noção de escala — os maiores valores continuam representados, mas sem distorcer o modelo.\n\n**Tratamento de baixa variabilidade (`TOTALDEBT`)**\nA discretização por quantis não funcionou bem devido à concentração de valores (principalmente zeros).\nA solução foi binarizar: **0 (sem dívida)** e **1 (com dívida)**.\nAssim, a variável mantém um sinal útil para o modelo, em vez de virar uma coluna praticamente constante.\n\n**Impacto na modelagem**\nO uso de quantis ajuda a manter distribuições mais equilibradas nas variáveis discretizadas, o que tende a melhorar a estabilidade do modelo.\nJá a binarização evita perda de informação em variáveis pouco variadas.\n","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"# Consistência - 2.c\nprint(\"--- Varredura Final de Consistência Lógica ---\")\n\ninconsistencias = 0\n\n# Regra de Negócio: Valores financeiros não podem ser menores que zero.\ncolunas_financeiras = ['credamount_770A', 'annuity_780A']\nfor col in colunas_financeiras:\n    if col in df_suavizado.columns:\n        qtd_negativos = (df_suavizado[col] < 0).sum()\n        inconsistencias += qtd_negativos\n        if qtd_negativos > 0:\n            print(f\"ERRO: {qtd_negativos} valores negativos encontrados em {col}.\")\n\n# Regra de Negócio: Binarização deve conter apenas 0 ou 1.\nif 'totaldebt_9A' in df_suavizado.columns:\n    valores_invalidos = (~df_suavizado['totaldebt_9A'].isin([0.0, 1.0])).sum()\n    inconsistencias += valores_invalidos\n    if valores_invalidos > 0:\n         print(f\"ERRO: {valores_invalidos} valores fora do escopo binário em totaldebt_9A.\")\n\n# Veredito\nif inconsistencias == 0:\n    print(\"Veredito: 0 inconsistências encontradas.\")\n    print(\"A base de dados 'df_suavizado' está validada e aderente às regras de negócio.\")\nelse:\n    print(f\"Atenção: Necessária intervenção manual para {inconsistencias} registros.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Ausência de inconsistências\n\nA falta de anomalias lógicas nos dados pode ser explicada por dois pontos principais:\n\n1. **Origem dos dados**\n   O dataset vem de sistemas bancários estruturados, que já aplicam validações no momento do registro. Isso reduz a chance de erros básicos, como valores negativos onde não fazem sentido.\n\n2. **Seleção das variáveis**\n   Na Fase 1, foram mantidas apenas colunas com menos de 30% de valores nulos. Esse filtro tende a preservar variáveis mais consistentes e descartar aquelas mais problemáticas.\n\n**Conclusão:**\nOs dados analisados estão coerentes do ponto de vista lógico e podem seguir para as próximas etapas de transformação e modelagem sem necessidade de correções estruturais.","metadata":{}},{"cell_type":"code","source":"# Vetorização Categórica - 2.d\nfrom sklearn.preprocessing import OneHotEncoder\nimport pandas as pd\n\nprint(\"--- Iniciando Vetorização Categórica (One-Hot Encoding) ---\")\n\n# Identificar colunas categóricas\ncolunas_categoricas = df_suavizado.select_dtypes(include=['object', 'category']).columns.tolist()\n# Filtro para evitar que datas virem centenas de colunas\ncolunas_categoricas = [col for col in colunas_categoricas if 'date' not in col.lower()]\n\nprint(f\"Atributos nominais detectados: {len(colunas_categoricas)}\")\n\n# Instanciar o Encoder\n# sparse_output=False: Gera um array normal do NumPy (ideal para nossa visualização atual)\n# handle_unknown='ignore': Proteção contra o Vazamento de Dados e erros no Teste\nencoder = OneHotEncoder(sparse_output=False, handle_unknown='ignore')\n\n# 3. Aprendizado e aplicação\n# Ele \"aprende\" as categorias no treino e já devolve a matriz de 0s e 1s\nvetores_encoded = encoder.fit_transform(df_suavizado[colunas_categoricas])\n\n# Extrair os nomes das novas colunas geradas\nnomes_colunas = encoder.get_feature_names_out(colunas_categoricas)\n\n# Converter de volta para DataFrame e Integrar\ndf_encoded = pd.DataFrame(vetores_encoded, columns=nomes_colunas, index=df_suavizado.index)\n\n# Remove as colunas de texto antigas e concatena as novas colunas binárias\ndf_final_fase2 = pd.concat([df_suavizado.drop(columns=colunas_categoricas), df_encoded], axis=1)\n\nprint(f\"\\nTransformação Concluída com Sucesso!\")\nprint(f\"-> Dimensões do DataFrame ANTES:  {df_suavizado.shape}\")\nprint(f\"-> Dimensões do DataFrame DEPOIS: {df_final_fase2.shape}\")\nprint(f\"-> Novas colunas binárias geradas: {len(nomes_colunas)}\")\n\n# Exibindo um recorte da nova matriz esparsa para prova visual\nprint(\"\\nRecorte das novas variáveis criadas:\")\ndisplay(df_final_fase2[nomes_colunas[:6]].head())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Expansão de dimensionalidade\n\nA aplicação do `OneHotEncoder` trouxe alguns efeitos esperados:\n\n1. **Aumento no número de colunas**\n   Cada variável categórica foi transformada em várias colunas binárias (uma por categoria). Por exemplo, uma variável com 5 categorias vira 5 colunas.\n\n2. **Matriz esparsa**\n   O resultado é uma matriz com muitos zeros e poucos valores 1. Isso é normal nesse tipo de codificação e evita que o modelo interprete categorias como se tivessem ordem ou escala.\n\n3. **Consistência do pipeline**\n   O encoder foi ajustado apenas no treino e agora tem um conjunto fixo de categorias.","metadata":{}},{"cell_type":"code","source":"# 2.e\nprint(\"--- Verificando Atributos Ordinais ---\")\n\n# Lista de atributos que quero tratar como ordinais\nordinais_desejados = ['education_1102M', 'familystatus_b60L']\nmapa_e = {'Hash_Nivel_0': 0, 'Hash_Nivel_1': 1, 'Hash_Nivel_2': 2}\n\nencontrou_algum = False\n\nfor col in ordinais_desejados:\n    if col in df_suavizado.columns:\n        print(f\"-> Atributo detectado: {col}. Aplicando mapeamento...\")\n        df_suavizado[col] = df_suavizado[col].map(mapa_e).fillna(0)\n        encontrou_algum = True\n\nif not encontrou_algum:\n    print(\"Veredito: Nenhum atributo estritamente ordinal sobreviveu ao filtro de qualidade da Fase 1.\")\n    print(\"Ação: O pipeline seguirá para a vetorização nominal (Fase 2.d) sem perda de informação.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2e\n\nEsta etapa confirma que os filtros aplicados no início do pipeline estão funcionando como esperado:\n\n1. **Resiliência do pipeline**\n   A variável `education_1102M` já havia sido removida por excesso de valores nulos (>30%), e o pipeline respeitou essa decisão automaticamente. Isso evita que dados de baixa qualidade entrem nas etapas seguintes.\n\n2. **Tratamento adequado das variáveis**\n   Não foi aplicado encoding ordinal em variáveis como `credtype_322L`, já que elas são nominais. Forçar uma ordem nesses casos criaria relações artificiais que não existem nos dados.\n\n3. **Estado atual da base**\n   Com essa etapa concluída, o pré-processamento chega ao fim. Os dados agora estão organizados, consistentes e prontos para a etapa de modelagem.\n","metadata":{}},{"cell_type":"code","source":"# Normalização Numérica - Fase 2.f\nfrom sklearn.preprocessing import MinMaxScaler\nimport pandas as pd\nimport numpy as np\n\nprint(\"--- Iniciando Normalização Numérica (MinMaxScaler) ---\")\n\n# Colunas Numéricas \ncolunas_ignoradas = ['case_id', 'target', 'MONTH', 'WEEK_NUM', 'date_decision']\ncols_numericas_puras = [col for col in df_suavizado.select_dtypes(include=[np.number]).columns if col not in colunas_ignoradas]\n\nprint(f\"Colunas numéricas a normalizar: {len(cols_numericas_puras)}\")\n\n# Instanciação do Scaler\nscaler = MinMaxScaler(feature_range=(0, 1))\n\n# Criação do DataFrame\ndf_mineracao = df_final_fase2.copy()\n\n# Fit & Transform\n# Requisito de \"aprender no treino e aplicar no teste\"\ndf_mineracao[cols_numericas_puras] = scaler.fit_transform(df_mineracao[cols_numericas_puras])\n\nprint(\"\\nSucesso! Normalização concluída.\")\nprint(\"Verificação das escalas (Mínimo e Máximo agora devem ser 0 e 1):\")\n\n# Tabela de prova visual para o professor\nprova_escala = df_mineracao[cols_numericas_puras].describe().T[['min', 'max', 'mean']]\ndisplay(prova_escala.round(4).head())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Comentário sobre os resultados (conclusão do pipeline)\n\n1. **Padronização de escala**\n   As variáveis agora estão na mesma faixa (entre 0 e 1). Isso inclui tanto contagens quanto variáveis discretizadas anteriormente, que foram normalizadas.\n\n2. **Redução de viés por magnitude**\n   Com as variáveis na mesma escala, o modelo deixa de favorecer atributos apenas por terem valores maiores. Isso ajuda a tornar o aprendizado mais equilibrado.\n\n3. **Estado final da base**\n   O dataset (`df_mineracao`) está pronto para modelagem:\n\n   * sem valores nulos\n   * com outliers controlados\n   * variáveis categóricas tratadas\n   * dados padronizados\n","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}