Spaces:
Sleeping
Sleeping
Update model_utils.py
Browse files- model_utils.py +62 -56
model_utils.py
CHANGED
|
@@ -19,7 +19,7 @@ import re # Para auxiliar na limpeza de markdown para LaTeX
|
|
| 19 |
|
| 20 |
# Importações para LaTeX (pylatex)
|
| 21 |
from pylatex import Document, Section, Command, LongTable, Tabular, Figure, NoEscape, Math, LineBreak
|
| 22 |
-
from pylatex.utils import italic
|
| 23 |
from pylatex.base_classes import Environment
|
| 24 |
|
| 25 |
# --- DEFINIÇÃO DAS FEATURES PREDITIVAS E COLUNA ALVO PARA SEU data.csv ---
|
|
@@ -139,7 +139,7 @@ class ChurnModelPipeline:
|
|
| 139 |
|
| 140 |
plot_dir = tempfile.mkdtemp()
|
| 141 |
self.plot_paths = {}
|
| 142 |
-
dpi =
|
| 143 |
|
| 144 |
# --- 1. Correlation Heatmap ---
|
| 145 |
if self.df_raw_for_plots is not None and not self.df_raw_for_plots.empty:
|
|
@@ -189,12 +189,7 @@ class ChurnModelPipeline:
|
|
| 189 |
plt.tight_layout()
|
| 190 |
cm_path = os.path.join(plot_dir, 'confusion_matrix.png')
|
| 191 |
plt.savefig(cm_path)
|
| 192 |
-
|
| 193 |
-
if not os.path.exists(plot_dir):
|
| 194 |
-
os.makedirs(plot_dir)
|
| 195 |
-
cm_path = os.path.join(plot_dir, 'confusion_matrix.png')
|
| 196 |
-
plt.savefig(cm_path)
|
| 197 |
-
plt.close()
|
| 198 |
self.plot_paths['confusion_matrix'] = cm_path
|
| 199 |
|
| 200 |
# --- 4. ROC Curve ---
|
|
@@ -214,7 +209,7 @@ class ChurnModelPipeline:
|
|
| 214 |
plt.tight_layout()
|
| 215 |
roc_path = os.path.join(plot_dir, 'roc_curve.png')
|
| 216 |
plt.savefig(roc_path)
|
| 217 |
-
plt.close()
|
| 218 |
self.plot_paths['roc_curve'] = roc_path
|
| 219 |
|
| 220 |
def predict_churn(self, input_data: pd.DataFrame) -> Tuple[int, float]:
|
|
@@ -276,6 +271,7 @@ class ChurnModelPipeline:
|
|
| 276 |
latex_story.append(NoEscape(sample_display_df.to_latex(index=False, caption='Características do Cliente Simulado', label='tab:sim_customer', longtable=False)))
|
| 277 |
|
| 278 |
markdown_story.append(f"**Resultado da Simulação:** O cliente **{churn_status_sample}** (Probabilidade de Churn: **{prob_sample:.2%}**)\n")
|
|
|
|
| 279 |
latex_story.append(NoEscape(f'\textbf{{Resultado da Simulação:}} O cliente \textbf{{{churn_status_sample}}} (Probabilidade de Churn: \textbf{{{prob_sample:.2f}\%}})\n\n'))
|
| 280 |
else:
|
| 281 |
markdown_story.append("Não foi possível realizar uma simulação pois o DataFrame de teste ou dados interativos não estão disponíveis.\n")
|
|
@@ -295,12 +291,14 @@ class ChurnModelPipeline:
|
|
| 295 |
training_details_markdown += f"- **Shape X_train (antes pré-processamento):** `{self.training_details.get('X_train_shape', 'N/A')}`\n"
|
| 296 |
|
| 297 |
y_train_before_smote = self.training_details.get('y_train_value_counts_before_smote', {})
|
| 298 |
-
|
|
|
|
| 299 |
|
| 300 |
training_details_markdown += f"- **Shape X_train (após pré-processamento):** `{self.training_details.get('X_train_processed_shape', 'N/A')}`\n"
|
| 301 |
|
| 302 |
y_train_after_smote = self.training_details.get('y_train_resampled_value_counts_after_smote', {})
|
| 303 |
-
|
|
|
|
| 304 |
|
| 305 |
training_details_markdown += f"- **Modelo Treinado:** `{'Sim' if self.training_details.get('model_trained_successfully', False) else 'Não'}`\n"
|
| 306 |
|
|
@@ -309,10 +307,10 @@ class ChurnModelPipeline:
|
|
| 309 |
training_details_latex += fr'\item \textbf{{Dataset Carregado:}} {self.training_details.get("dataset_rows", "N/A")} linhas' + '\n'
|
| 310 |
training_details_latex += fr'\item \textbf{{Features Preditivas:}} \texttt{{{", ".join(self.training_details.get("predictor_features", ["N/A"]))}}}.' + '\n'
|
| 311 |
training_details_latex += fr'\item \textbf{{Coluna Alvo:}} \texttt{{{self.training_details.get("target_column", "N/A")}}}.' + '\n'
|
| 312 |
-
training_details_latex += fr'\item \textbf{{Shape $X_{train}$ (antes pré-processamento):}} {self.training_details.get("X_train_shape", "N/A")}.' + '\n'
|
| 313 |
-
training_details_latex += fr'\item \textbf{{Balanceamento \texttt{Exited} (antes SMOTE):}} Não Churn: {y_train_before_smote.get(0, "N/A")}, Churn: {y_train_before_smote.get(1, "N/A")}.' + '\n'
|
| 314 |
-
training_details_latex += fr'\item \textbf{{Shape $X_{train}$ (após pré-processamento):}} {self.training_details.get("X_train_processed_shape", "N/A")}.' + '\n'
|
| 315 |
-
training_details_latex += fr'\item \textbf{{Balanceamento \texttt{Exited} (após SMOTE):}} Não Churn: {y_train_after_smote.get(0, "N/A")}, Churn: {y_train_after_smote.get(1, "N/A")}.' + '\n'
|
| 316 |
training_details_latex += fr'\item \textbf{{Modelo Treinado:}} {"Sim" if self.training_details.get("model_trained_successfully", False) else "Não"}.' + '\n'
|
| 317 |
training_details_latex += r'\end{itemize}' + '\n\n'
|
| 318 |
|
|
@@ -445,9 +443,10 @@ class ChurnModelPipeline:
|
|
| 445 |
doc.append(Command('graphicspath', NoEscape(r'{./}'))) # Para imagens no mesmo diretório
|
| 446 |
|
| 447 |
# --- Cabeçalho Personalizado (com base nas informações do usuário) ---
|
| 448 |
-
|
| 449 |
-
doc.append(NoEscape(r'\
|
| 450 |
-
doc.append(NoEscape(r'\
|
|
|
|
| 451 |
|
| 452 |
doc.append(NoEscape(r'\begin{titlepage}'))
|
| 453 |
doc.append(Command('centering'))
|
|
@@ -457,7 +456,8 @@ class ChurnModelPipeline:
|
|
| 457 |
if os.path.exists(logo_filename):
|
| 458 |
with doc.create(Figure(position='h!')) as logo_fig:
|
| 459 |
logo_fig.add_image(logo_filename, width='0.25\textwidth')
|
| 460 |
-
|
|
|
|
| 461 |
else:
|
| 462 |
doc.append(Command('textbf', 'AVISO: Logo da UnB não encontrado! Certifique-se de que "marcador.png" esteja no mesmo diretório do arquivo .tex.'))
|
| 463 |
|
|
@@ -478,13 +478,38 @@ class ChurnModelPipeline:
|
|
| 478 |
|
| 479 |
# Título do Trabalho (do usuário, ajustado para LaTeX)
|
| 480 |
# Quebra de linha manual para o título
|
| 481 |
-
title_parts = header_info["titulo_trabalho"].replace('UTILIZANDO', r'\UTILIZANDO').split(r'\')
|
|
|
|
|
|
|
| 482 |
doc.append(Command('Huge'))
|
| 483 |
-
doc.append(Command('textbf', NoEscape(
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
|
| 489 |
doc.append(Command('vspace', '1.0cm'))
|
| 490 |
|
|
@@ -507,49 +532,30 @@ class ChurnModelPipeline:
|
|
| 507 |
doc.append(Command('vfill')) # Empurra o conteúdo para cima
|
| 508 |
doc.append(Command('end{titlepage}'))
|
| 509 |
|
| 510 |
-
doc.append(Command('maketitle')) #
|
|
|
|
|
|
|
| 511 |
doc.append(Command('clearpage'))
|
| 512 |
|
| 513 |
# Conteúdo do Resumo
|
| 514 |
for item in latex_content_parts:
|
| 515 |
-
|
| 516 |
-
# Handle DataFrame.to_latex output manually to fit pylatex's tabular environment
|
| 517 |
-
# This is a bit tricky. pylatex's tabular works best with specific object types.
|
| 518 |
-
# For simplicity, we'll embed the raw latex table string
|
| 519 |
-
doc.append(NoEscape(item))
|
| 520 |
-
elif isinstance(item, str) and (item.startswith('\section') or item.startswith('\subsection')):
|
| 521 |
-
doc.append(NoEscape(item + '\n'))
|
| 522 |
-
elif isinstance(item, str):
|
| 523 |
-
# Process common markdown-like elements in raw string for LaTeX
|
| 524 |
-
processed_str = item.replace('**', '\textbf{').replace('*', '\emph{').replace('`', '\texttt{')
|
| 525 |
-
processed_str = processed_str.replace('}', '}}') # close bold/emph/texttt
|
| 526 |
-
processed_str = processed_str.replace('}}', '}') # fix double close
|
| 527 |
-
processed_str = processed_str.replace('%', '\%').replace('&', '\&').replace('_', '_') # escape LaTeX special chars
|
| 528 |
-
doc.append(NoEscape(processed_str))
|
| 529 |
-
else:
|
| 530 |
-
doc.append(item) # Assume it's a pylatex object (Section, Math etc.)
|
| 531 |
-
doc.append(LineBreak()) # Add a line break after each item
|
| 532 |
|
| 533 |
# Adicionar imagens ao final do documento LaTeX
|
| 534 |
doc.append(NoEscape(r'\clearpage'))
|
| 535 |
-
doc.append(NoEscape(r'
|
| 536 |
-
doc.append(NoEscape(r'\addcontentsline{toc}{section}{Visualizações Gráficas do Modelo}')) # Adicionar ao sumário
|
| 537 |
|
|
|
|
| 538 |
for key, path in plot_paths.items():
|
| 539 |
if os.path.exists(path):
|
| 540 |
-
doc.
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
#
|
| 544 |
-
|
| 545 |
-
doc.append(NoEscape(f'\caption{{{key.replace("_", " ").title()}}}'))
|
| 546 |
-
doc.append(NoEscape(f'\label{{fig:{key}}}'))
|
| 547 |
-
doc.append(NoEscape(r'\end{figure}'))
|
| 548 |
-
doc.append(NoEscape(r'\clearpage')) # Cada imagem em uma nova página
|
| 549 |
-
|
| 550 |
# Salvar o arquivo .tex
|
| 551 |
latex_output_dir = tempfile.mkdtemp()
|
| 552 |
output_filename = os.path.join(latex_output_dir, 'relatorio_churn.tex')
|
| 553 |
doc.generate_tex(output_filename) # Salva o arquivo .tex
|
| 554 |
|
| 555 |
-
return output_filename
|
|
|
|
| 19 |
|
| 20 |
# Importações para LaTeX (pylatex)
|
| 21 |
from pylatex import Document, Section, Command, LongTable, Tabular, Figure, NoEscape, Math, LineBreak
|
| 22 |
+
from pylatex.utils import italic
|
| 23 |
from pylatex.base_classes import Environment
|
| 24 |
|
| 25 |
# --- DEFINIÇÃO DAS FEATURES PREDITIVAS E COLUNA ALVO PARA SEU data.csv ---
|
|
|
|
| 139 |
|
| 140 |
plot_dir = tempfile.mkdtemp()
|
| 141 |
self.plot_paths = {}
|
| 142 |
+
dpi = 150 # Aumentado DPI para melhor qualidade em relatórios
|
| 143 |
|
| 144 |
# --- 1. Correlation Heatmap ---
|
| 145 |
if self.df_raw_for_plots is not None and not self.df_raw_for_plots.empty:
|
|
|
|
| 189 |
plt.tight_layout()
|
| 190 |
cm_path = os.path.join(plot_dir, 'confusion_matrix.png')
|
| 191 |
plt.savefig(cm_path)
|
| 192 |
+
plt.close() # Fechar a figura para liberar memória
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
self.plot_paths['confusion_matrix'] = cm_path
|
| 194 |
|
| 195 |
# --- 4. ROC Curve ---
|
|
|
|
| 209 |
plt.tight_layout()
|
| 210 |
roc_path = os.path.join(plot_dir, 'roc_curve.png')
|
| 211 |
plt.savefig(roc_path)
|
| 212 |
+
plt.close() # Fechar a figura para liberar memória
|
| 213 |
self.plot_paths['roc_curve'] = roc_path
|
| 214 |
|
| 215 |
def predict_churn(self, input_data: pd.DataFrame) -> Tuple[int, float]:
|
|
|
|
| 271 |
latex_story.append(NoEscape(sample_display_df.to_latex(index=False, caption='Características do Cliente Simulado', label='tab:sim_customer', longtable=False)))
|
| 272 |
|
| 273 |
markdown_story.append(f"**Resultado da Simulação:** O cliente **{churn_status_sample}** (Probabilidade de Churn: **{prob_sample:.2%}**)\n")
|
| 274 |
+
# Corrigido o SyntaxWarning para '%' no f-string para LaTeX
|
| 275 |
latex_story.append(NoEscape(f'\textbf{{Resultado da Simulação:}} O cliente \textbf{{{churn_status_sample}}} (Probabilidade de Churn: \textbf{{{prob_sample:.2f}\%}})\n\n'))
|
| 276 |
else:
|
| 277 |
markdown_story.append("Não foi possível realizar uma simulação pois o DataFrame de teste ou dados interativos não estão disponíveis.\n")
|
|
|
|
| 291 |
training_details_markdown += f"- **Shape X_train (antes pré-processamento):** `{self.training_details.get('X_train_shape', 'N/A')}`\n"
|
| 292 |
|
| 293 |
y_train_before_smote = self.training_details.get('y_train_value_counts_before_smote', {})
|
| 294 |
+
# Corrigido o SyntaxWarning para '`' no f-string para Markdown
|
| 295 |
+
training_details_markdown += f"- **Balanceamento `Exited` (antes SMOTE):** `Não Churn: {y_train_before_smote.get(0, 'N/A')}, Churn: {y_train_before_smote.get(1, 'N/A')}`\n"
|
| 296 |
|
| 297 |
training_details_markdown += f"- **Shape X_train (após pré-processamento):** `{self.training_details.get('X_train_processed_shape', 'N/A')}`\n"
|
| 298 |
|
| 299 |
y_train_after_smote = self.training_details.get('y_train_resampled_value_counts_after_smote', {})
|
| 300 |
+
# Corrigido o SyntaxWarning para '`' no f-string para Markdown
|
| 301 |
+
training_details_markdown += f"- **Balanceamento `Exited` (após SMOTE):** `Não Churn: {y_train_after_smote.get(0, 'N/A')}, Churn: {y_train_after_smote.get(1, 'N/A')}`\n"
|
| 302 |
|
| 303 |
training_details_markdown += f"- **Modelo Treinado:** `{'Sim' if self.training_details.get('model_trained_successfully', False) else 'Não'}`\n"
|
| 304 |
|
|
|
|
| 307 |
training_details_latex += fr'\item \textbf{{Dataset Carregado:}} {self.training_details.get("dataset_rows", "N/A")} linhas' + '\n'
|
| 308 |
training_details_latex += fr'\item \textbf{{Features Preditivas:}} \texttt{{{", ".join(self.training_details.get("predictor_features", ["N/A"]))}}}.' + '\n'
|
| 309 |
training_details_latex += fr'\item \textbf{{Coluna Alvo:}} \texttt{{{self.training_details.get("target_column", "N/A")}}}.' + '\n'
|
| 310 |
+
training_details_latex += fr'\item \textbf{{Shape $X_{{train}}$ (antes pré-processamento):}} {self.training_details.get("X_train_shape", "N/A")}.' + '\n' # $X_{train}$ corrigido
|
| 311 |
+
training_details_latex += fr'\item \textbf{{Balanceamento \texttt{{Exited}} (antes SMOTE):}} Não Churn: {y_train_before_smote.get(0, "N/A")}, Churn: {y_train_before_smote.get(1, "N/A")}.' + '\n'
|
| 312 |
+
training_details_latex += fr'\item \textbf{{Shape $X_{{train}}$ (após pré-processamento):}} {self.training_details.get("X_train_processed_shape", "N/A")}.' + '\n' # $X_{train}$ corrigido
|
| 313 |
+
training_details_latex += fr'\item \textbf{{Balanceamento \texttt{{Exited}} (após SMOTE):}} Não Churn: {y_train_after_smote.get(0, "N/A")}, Churn: {y_train_after_smote.get(1, "N/A")}.' + '\n'
|
| 314 |
training_details_latex += fr'\item \textbf{{Modelo Treinado:}} {"Sim" if self.training_details.get("model_trained_successfully", False) else "Não"}.' + '\n'
|
| 315 |
training_details_latex += r'\end{itemize}' + '\n\n'
|
| 316 |
|
|
|
|
| 443 |
doc.append(Command('graphicspath', NoEscape(r'{./}'))) # Para imagens no mesmo diretório
|
| 444 |
|
| 445 |
# --- Cabeçalho Personalizado (com base nas informações do usuário) ---
|
| 446 |
+
# Removendo title, author, date pois o titlepage vai sobrescrevê-los
|
| 447 |
+
# doc.append(NoEscape(r'\title{MODELAGEM PREDITIVA DE CHURN DE CLIENTES BANCÁRIOS UTILIZANDO REGRESSÃO LOGÍSTICA}'))
|
| 448 |
+
# doc.append(NoEscape(r'\author{ÉDER MARCELO PONTES CUNHA}'))
|
| 449 |
+
# doc.append(NoEscape(r'\date{26 de Outubro de 2025}')) # Ajuste conforme necessário
|
| 450 |
|
| 451 |
doc.append(NoEscape(r'\begin{titlepage}'))
|
| 452 |
doc.append(Command('centering'))
|
|
|
|
| 456 |
if os.path.exists(logo_filename):
|
| 457 |
with doc.create(Figure(position='h!')) as logo_fig:
|
| 458 |
logo_fig.add_image(logo_filename, width='0.25\textwidth')
|
| 459 |
+
# A caption vazia ou um vspace garante que não haja texto extra colado no logo
|
| 460 |
+
logo_fig.add_caption(NoEscape(r'\vspace{-0.5cm}'))
|
| 461 |
else:
|
| 462 |
doc.append(Command('textbf', 'AVISO: Logo da UnB não encontrado! Certifique-se de que "marcador.png" esteja no mesmo diretório do arquivo .tex.'))
|
| 463 |
|
|
|
|
| 478 |
|
| 479 |
# Título do Trabalho (do usuário, ajustado para LaTeX)
|
| 480 |
# Quebra de linha manual para o título
|
| 481 |
+
# CORRIGIDO: title_parts = header_info["titulo_trabalho"].replace('UTILIZANDO', r'\UTILIZANDO').split(r'\')
|
| 482 |
+
# AQUI FOI O ERRO DE SYNTAX. Deve ser assim:
|
| 483 |
+
title_parts_raw = header_info["titulo_trabalho"].replace(' UTILIZANDO ', r'\ \large ').split(r'\')
|
| 484 |
doc.append(Command('Huge'))
|
| 485 |
+
doc.append(Command('textbf', NoEscape(title_parts_raw[0]))) # Primeira parte do título
|
| 486 |
+
|
| 487 |
+
# As partes restantes são separadas por `\` que adicionamos
|
| 488 |
+
# Iterar sobre as partes restantes e adicionar com quebra de linha
|
| 489 |
+
# A lógica de split mudou para apenas quebrar em ' ' e adicionar o comando LaTeX manualmente
|
| 490 |
+
title_words = header_info["titulo_trabalho"].split()
|
| 491 |
+
latex_title_lines = []
|
| 492 |
+
current_line = []
|
| 493 |
+
for word in title_words:
|
| 494 |
+
if word == 'UTILIZANDO':
|
| 495 |
+
if current_line:
|
| 496 |
+
latex_title_lines.append(" ".join(current_line))
|
| 497 |
+
current_line = []
|
| 498 |
+
latex_title_lines.append(r'\ \large UTILIZANDO') # Comando LaTeX para quebra de linha e tamanho da fonte
|
| 499 |
+
else:
|
| 500 |
+
current_line.append(word)
|
| 501 |
+
if current_line:
|
| 502 |
+
latex_title_lines.append(" ".join(current_line))
|
| 503 |
+
|
| 504 |
+
doc.append(Command('Huge'))
|
| 505 |
+
doc.append(Command('textbf', NoEscape(latex_title_lines[0]))) # Primeira linha do título
|
| 506 |
+
for line_idx in range(1, len(latex_title_lines)):
|
| 507 |
+
doc.append(LineBreak())
|
| 508 |
+
# Se for a linha com 'UTILIZANDO', já está formatada, caso contrário, use textbf
|
| 509 |
+
if 'UTILIZANDO' in latex_title_lines[line_idx]:
|
| 510 |
+
doc.append(NoEscape(latex_title_lines[line_idx]))
|
| 511 |
+
else:
|
| 512 |
+
doc.append(Command('textbf', NoEscape(latex_title_lines[line_idx])))
|
| 513 |
|
| 514 |
doc.append(Command('vspace', '1.0cm'))
|
| 515 |
|
|
|
|
| 532 |
doc.append(Command('vfill')) # Empurra o conteúdo para cima
|
| 533 |
doc.append(Command('end{titlepage}'))
|
| 534 |
|
| 535 |
+
# doc.append(Command('maketitle')) # Não precisamos de maketitle pois usamos titlepage
|
| 536 |
+
doc.append(Command('clearpage'))
|
| 537 |
+
doc.append(Command('tableofcontents')) # Sumário
|
| 538 |
doc.append(Command('clearpage'))
|
| 539 |
|
| 540 |
# Conteúdo do Resumo
|
| 541 |
for item in latex_content_parts:
|
| 542 |
+
doc.append(item) # pylatex objects (Section, Math, NoEscape) are directly appended
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
|
| 544 |
# Adicionar imagens ao final do documento LaTeX
|
| 545 |
doc.append(NoEscape(r'\clearpage'))
|
| 546 |
+
doc.append(Section(NoEscape(r'Visualizações Gráficas do Modelo')))
|
|
|
|
| 547 |
|
| 548 |
+
# Aumentado a largura para preencher mais a página e centralizar
|
| 549 |
for key, path in plot_paths.items():
|
| 550 |
if os.path.exists(path):
|
| 551 |
+
with doc.create(Figure(position='htbp')) as plot_fig:
|
| 552 |
+
plot_fig.add_image(path, width='0.8\textwidth')
|
| 553 |
+
plot_fig.add_caption(NoEscape(f'{key.replace("_", " ").title()}'))
|
| 554 |
+
doc.append(Command('clearpage')) # Cada imagem em uma nova página
|
| 555 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
# Salvar o arquivo .tex
|
| 557 |
latex_output_dir = tempfile.mkdtemp()
|
| 558 |
output_filename = os.path.join(latex_output_dir, 'relatorio_churn.tex')
|
| 559 |
doc.generate_tex(output_filename) # Salva o arquivo .tex
|
| 560 |
|
| 561 |
+
return output_filename
|